Skip to content

Commit

Permalink
Improve builds
Browse files Browse the repository at this point in the history
  • Loading branch information
ofek committed Jul 7, 2024
1 parent 72e0942 commit 1b0561c
Show file tree
Hide file tree
Showing 46 changed files with 2,728 additions and 977 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ jobs:
run: hatch run types:check

- name: Run tests
run: hatch test --python ${{ matrix.python-version }} --cover-quiet --randomize --parallel --retries 5 --retry-delay 3
run: hatch test --python ${{ matrix.python-version }} tests/cli/build/test_build.py::TestOtherBackend::test_standard -vv
# run: hatch test --python ${{ matrix.python-version }} --cover-quiet --randomize --parallel --retries 5 --retry-delay 3

- name: Disambiguate coverage filename
run: mv .coverage ".coverage.${{ matrix.os }}.${{ matrix.python-version }}"
Expand Down
5 changes: 5 additions & 0 deletions docs/history/hatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

***Added:***

- The `version` and `project metadata` commands now support projects that do not use Hatchling as the build backend
- Build environments can now be configured, the default build environment is now `hatch-build`

## [1.12.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.12.0) - 2024-05-28 ## {: #hatch-v1.12.0 }

***Changed:***
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/environment/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ requires = [

Whenever an environment is used, the following logic is performed:

::: hatch.cli.application.Application.prepare_environment
::: hatch.project.Project.prepare_environment
options:
show_root_heading: false
show_root_toc_entry: false
Expand Down
2 changes: 2 additions & 0 deletions hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ post-install-commands = [
[envs.hatch-test]
extra-dependencies = [
"filelock",
"flit-core",
"hatchling",
"pyfakefs",
"trustme",
# Hatchling dynamic dependency
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies = [
"packaging>=23.2",
"pexpect~=4.8",
"platformdirs>=2.5.0",
"pyproject-hooks",
"rich>=11.2.0",
"shellingham>=1.4.0",
"tomli-w>=1.0",
Expand Down Expand Up @@ -116,6 +117,7 @@ omit = [
"backend/src/hatchling/ouroboros.py",
"src/hatch/__main__.py",
"src/hatch/cli/new/migrate.py",
"src/hatch/project/frontend/scripts/*",
"src/hatch/utils/shells.py",
]

Expand Down
12 changes: 10 additions & 2 deletions src/hatch/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from __future__ import annotations

import os
from typing import cast

import click

Expand Down Expand Up @@ -159,13 +162,16 @@ def hatch(ctx: click.Context, env_name, project, verbose, quiet, color, interact
app.cache_dir = Path(cache_dir or app.config.dirs.cache).expand()

if project:
app.project = Project.from_config(app.config, project)
if app.project is None or app.project.root is None:
potential_project = Project.from_config(app.config, project)
if potential_project is None or potential_project.root is None:
app.abort(f'Unable to locate project {project}')

app.project = cast(Project, potential_project)
app.project.set_app(app)
return

app.project = Project(Path.cwd())
app.project.set_app(app)

if app.config.mode == 'local':
return
Expand All @@ -182,6 +188,7 @@ def hatch(ctx: click.Context, env_name, project, verbose, quiet, color, interact
else:
app.project = possible_project

app.project.set_app(app)
return

if app.config.mode == 'aware' and app.project.root is None:
Expand All @@ -195,6 +202,7 @@ def hatch(ctx: click.Context, env_name, project, verbose, quiet, color, interact
else:
app.project = possible_project

app.project.set_app(app)
return


Expand Down
176 changes: 17 additions & 159 deletions src/hatch/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import sys
from functools import cached_property
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, cast

from hatch.cli.terminal import Terminal
from hatch.config.user import ConfigFile, RootConfig
Expand Down Expand Up @@ -45,114 +45,11 @@ def plugins(self):
def config(self) -> RootConfig:
return self.config_file.model

def expand_environments(self, env_name: str) -> list[str]:
if env_name in self.project.config.internal_matrices:
return list(self.project.config.internal_matrices[env_name]['envs'])

if env_name in self.project.config.matrices:
return list(self.project.config.matrices[env_name]['envs'])

if env_name in self.project.config.internal_envs:
return [env_name]

if env_name in self.project.config.envs:
return [env_name]

return []

def get_environment(self, env_name: str | None = None) -> EnvironmentInterface:
if env_name is None:
env_name = self.env

if env_name in self.project.config.internal_envs:
config = self.project.config.internal_envs[env_name]
elif env_name in self.project.config.envs:
config = self.project.config.envs[env_name]
else:
self.abort(f'Unknown environment: {env_name}')

environment_type = config['type']
environment_class = self.plugins.environment.get(environment_type)
if environment_class is None:
self.abort(f'Environment `{env_name}` has unknown type: {environment_type}')

from hatch.env.internal import is_isolated_environment
return self.project.get_environment(env_name)

if self.project.location.is_file():
data_directory = isolated_data_directory = self.data_dir / 'env' / environment_type / '.scripts'
elif is_isolated_environment(env_name, config):
data_directory = isolated_data_directory = self.data_dir / 'env' / '.internal' / env_name
else:
data_directory = self.get_env_directory(environment_type)
isolated_data_directory = self.data_dir / 'env' / environment_type

self.project.config.finalize_env_overrides(environment_class.get_option_types())

return environment_class(
self.project.location,
self.project.metadata,
env_name,
config,
self.project.config.matrix_variables.get(env_name, {}),
data_directory,
isolated_data_directory,
self.platform,
self.verbosity,
self,
)

# Ensure that this method is clearly written since it is
# used for documenting the life cycle of environments.
def prepare_environment(self, environment: EnvironmentInterface):
if not environment.exists():
self.env_metadata.reset(environment)

with environment.app_status_creation():
environment.create()

if not environment.skip_install:
if environment.pre_install_commands:
with environment.app_status_pre_installation():
self.run_shell_commands(
ExecutionContext(
environment,
shell_commands=environment.pre_install_commands,
source='pre-install',
show_code_on_error=True,
)
)

with environment.app_status_project_installation():
if environment.dev_mode:
environment.install_project_dev_mode()
else:
environment.install_project()

if environment.post_install_commands:
with environment.app_status_post_installation():
self.run_shell_commands(
ExecutionContext(
environment,
shell_commands=environment.post_install_commands,
source='post-install',
show_code_on_error=True,
)
)

with environment.app_status_dependency_state_check():
new_dep_hash = environment.dependency_hash()

current_dep_hash = self.env_metadata.dependency_hash(environment)
if new_dep_hash != current_dep_hash:
with environment.app_status_dependency_installation_check():
dependencies_in_sync = environment.dependencies_in_sync()

if not dependencies_in_sync:
with environment.app_status_dependency_synchronization():
environment.sync_dependencies()
new_dep_hash = environment.dependency_hash()

self.env_metadata.update_dependency_hash(environment, new_dep_hash)
self.project.prepare_environment(environment)

def run_shell_commands(self, context: ExecutionContext) -> None:
with context.env.command_context():
Expand All @@ -173,6 +70,13 @@ def run_shell_commands(self, context: ExecutionContext) -> None:
continue_on_error = True
command = command[2:]

if 'requires' in command:
self.platform.check_command_output(command, shell=True) # noqa: S604
# script = command.split(' ')[-1]
# with open(script, encoding='utf-8') as f:
# script_content = f.read()
# message = f'{process.returncode} | {script_content} | {process.stdout} | {process.stderr}'
# raise ValueError(message)
process = context.env.run_shell_command(command)
if process.returncode:
first_error_code = first_error_code or process.returncode
Expand All @@ -194,8 +98,6 @@ def runner_context(
ignore_compat: bool = False,
display_header: bool = False,
) -> Generator[ExecutionContext, None, None]:
from hatch.utils.structures import EnvVars

if self.verbose or len(environments) > 1:
display_header = True

Expand All @@ -222,8 +124,7 @@ def runner_context(
yield context

self.prepare_environment(environment)
with EnvVars(context.env_vars):
self.run_shell_commands(context)
self.execute_context(context)

if incompatible:
num_incompatible = len(incompatible)
Expand All @@ -234,6 +135,12 @@ def runner_context(
for env_name, reason in incompatible.items():
self.display_warning(f'{env_name} -> {reason}')

def execute_context(self, context: ExecutionContext) -> None:
from hatch.utils.structures import EnvVars

with EnvVars(context.env_vars):
self.run_shell_commands(context)

def ensure_environment_plugin_dependencies(self) -> None:
self.ensure_plugin_dependencies(
self.project.config.env_requires_complex, wait_message='Syncing environment plugin requirements'
Expand Down Expand Up @@ -299,56 +206,7 @@ def shell_data(self) -> tuple[str, str]:

return detect_shell(self.platform)

@cached_property
def env_metadata(self) -> EnvironmentMetadata:
return EnvironmentMetadata(self.data_dir / 'env' / '.metadata', self.project.location)

def abort(self, text='', code=1, **kwargs):
if text:
self.display_error(text, **kwargs)
self.__exit_func(code)


class EnvironmentMetadata:
def __init__(self, data_dir: Path, project_path: Path):
self.__data_dir = data_dir
self.__project_path = project_path

def dependency_hash(self, environment: EnvironmentInterface) -> str:
return self._read(environment).get('dependency_hash', '')

def update_dependency_hash(self, environment: EnvironmentInterface, dependency_hash: str) -> None:
metadata = self._read(environment)
metadata['dependency_hash'] = dependency_hash
self._write(environment, metadata)

def reset(self, environment: EnvironmentInterface) -> None:
self._metadata_file(environment).unlink(missing_ok=True)

def _read(self, environment: EnvironmentInterface) -> dict[str, Any]:
import json

metadata_file = self._metadata_file(environment)
if not metadata_file.is_file():
return {}

return json.loads(metadata_file.read_text())

def _write(self, environment: EnvironmentInterface, metadata: dict[str, Any]) -> None:
import json

metadata_file = self._metadata_file(environment)
metadata_file.parent.ensure_dir_exists()
metadata_file.write_text(json.dumps(metadata))

def _metadata_file(self, environment: EnvironmentInterface) -> Path:
from hatch.env.internal import is_isolated_environment

if is_isolated_environment(environment.name, environment.config):
return self.__data_dir / '.internal' / f'{environment.name}.json'

return self._storage_dir / environment.config['type'] / f'{environment.name}.json'

@cached_property
def _storage_dir(self) -> Path:
return self.__data_dir / self.__project_path.id
Loading

0 comments on commit 1b0561c

Please sign in to comment.