/
niceSOFT
/
python3-hatchling
Обзор
Документация
Войти
/
niceSOFT
/
python3-hatchling
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/hatch/project/config.py
801 строка
32 KB
Cary Hawkins
Add sources to enable other types of local dependencies (#2313)
10 авг 2026, 06:24
Не верифицирован
10 авг 2026, 06:24
a885803
Код
Авторство
О чём код?
from __future__ import annotations import re from copy import deepcopy from functools import cached_property from itertools import product from os import environ from typing import TYPE_CHECKING, Any, cast from hatch.env.utils import ensure_valid_environment from hatch.project.constants import DEFAULT_BUILD_DIRECTORY, BuildEnvVars from hatch.project.env import apply_overrides from hatch.project.sources import merge_source_tables, parse_sources from hatch.project.utils import format_script_commands, parse_script_command if TYPE_CHECKING: from packaging.specifiers import SpecifierSet from hatch.dep.core import Dependency class ProjectConfig: def __init__(self, root, config, plugin_manager=None): self.root = root self.config = config self.plugin_manager = plugin_manager self._matrices = None self._env = None self._env_requires_complex = None self._env_requires = None self._env_collectors = None self._envs = None self._internal_envs = None self._internal_matrices = None self._matrix_variables = None self._publish = None self._scripts = None self._requires_hatch: str | None = None self._hatch_specifier_set: SpecifierSet | None = None self._cached_env_overrides = {} @cached_property def build(self): config = self.config.get("build", {}) if not isinstance(config, dict): message = "Field `tool.hatch.build` must be a table" raise TypeError(message) return BuildConfig(config) @cached_property def lock_envs(self) -> bool: lock_envs = self.config.get("lock-envs", False) if not isinstance(lock_envs, bool): message = "Field `tool.hatch.lock-envs` must be a boolean" raise TypeError(message) return lock_envs @cached_property def locker(self) -> str | None: locker = self.config.get("locker") if locker is None: return None if not isinstance(locker, str): message = "Field `tool.hatch.locker` must be a string" raise TypeError(message) return locker @property def env(self): if self._env is None: config = self.config.get("env", {}) if not isinstance(config, dict): message = "Field `tool.hatch.env` must be a table" raise TypeError(message) self._env = config return self._env @property def env_requires_complex(self) -> list[Dependency]: if self._env_requires_complex is None: from hatch.dep.core import Dependency, InvalidDependencyError requires = self.env.get("requires", []) if not isinstance(requires, list): message = "Field `tool.hatch.env.requires` must be an array" raise TypeError(message) requires_complex = [] for i, entry in enumerate(requires, 1): if not isinstance(entry, str): message = f"Requirement #{i} in `tool.hatch.env.requires` must be a string" raise TypeError(message) try: requires_complex.append(Dependency(entry)) except InvalidDependencyError as e: message = f"Requirement #{i} in `tool.hatch.env.requires` is invalid: {e}" raise ValueError(message) from None self._env_requires_complex = requires_complex return self._env_requires_complex @property def env_requires(self): if self._env_requires is None: self._env_requires = [str(r) for r in self.env_requires_complex] return self._env_requires @property def env_collectors(self): if self._env_collectors is None: collectors = self.env.get("collectors", {}) if not isinstance(collectors, dict): message = "Field `tool.hatch.env.collectors` must be a table" raise TypeError(message) final_config = {"default": {}} for collector, config in collectors.items(): if not isinstance(config, dict): message = f"Field `tool.hatch.env.collectors.{collector}` must be a table" raise TypeError(message) final_config[collector] = config self._env_collectors = final_config return self._env_collectors @property def matrices(self): if self._matrices is None: _ = self.envs return self._matrices @property def matrix_variables(self): if self._matrix_variables is None: _ = self.envs return self._matrix_variables @property def internal_envs(self): if self._internal_envs is None: _ = self.envs return self._internal_envs @property def internal_matrices(self): if self._internal_matrices is None: _ = self.envs return self._internal_matrices @property def envs(self): from hatch.env.internal import get_internal_env_config from hatch.utils.platform import get_platform_name if self._envs is None: env_config = self.config.get("envs", {}) if not isinstance(env_config, dict): message = "Field `tool.hatch.envs` must be a table" raise TypeError(message) config = {} environment_collectors = [] for collector, collector_config in self.env_collectors.items(): collector_class = self.plugin_manager.environment_collector.get(collector) if collector_class is None: message = f"Unknown environment collector: {collector}" raise ValueError(message) environment_collector = collector_class(self.root, collector_config) environment_collectors.append(environment_collector) for env_name, data in environment_collector.get_initial_config().items(): config.setdefault(env_name, data) for env_name, data in env_config.items(): if not isinstance(data, dict): message = f"Field `tool.hatch.envs.{env_name}` must be a table" raise TypeError(message) config.setdefault(env_name, {}).update(data) for environment_collector in environment_collectors: environment_collector.finalize_config(config) # Prevent plugins from removing the default environment ensure_valid_environment(config.setdefault("default", {})) _populate_sources(config, self.config.get("sources", {})) seen = set() active = [] for env_name, data in config.items(): _populate_default_env_values(env_name, data, config, seen, active) current_platform = get_platform_name() all_matrices = {} generated_envs = {} final_config = {} cached_overrides = {} for env_name, raw_initial_config in config.items(): current_cached_overrides = cached_overrides[env_name] = { "platform": [], "env": [], "matrix": [], "name": [], } # Only shallow copying is necessary since we just want to modify keys initial_config = raw_initial_config.copy() matrix_name_format = initial_config.pop("matrix-name-format", "{value}") if not isinstance(matrix_name_format, str): message = f"Field `tool.hatch.envs.{env_name}.matrix-name-format` must be a string" raise TypeError(message) if "{value}" not in matrix_name_format: message = ( f"Field `tool.hatch.envs.{env_name}.matrix-name-format` must " f"contain at least the `{{value}}` placeholder" ) raise ValueError(message) overrides = initial_config.pop("overrides", {}) if not isinstance(overrides, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides` must be a table" raise TypeError(message) # Apply any configuration based on the current platform platform_overrides = overrides.get("platform", {}) if not isinstance(platform_overrides, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.platform` must be a table" raise TypeError(message) for platform, options in platform_overrides.items(): if not isinstance(options, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.platform.{platform}` must be a table" raise TypeError(message) if platform != current_platform: continue apply_overrides(env_name, "platform", platform, current_platform, options, initial_config) current_cached_overrides["platform"].append((platform, current_platform, options)) # Apply any configuration based on environment variables env_var_overrides = overrides.get("env", {}) if not isinstance(env_var_overrides, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.env` must be a table" raise TypeError(message) for env_var, options in env_var_overrides.items(): if not isinstance(options, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.env.{env_var}` must be a table" raise TypeError(message) if env_var not in environ: continue apply_overrides(env_name, "env", env_var, environ[env_var], options, initial_config) current_cached_overrides["env"].append((env_var, environ[env_var], options)) if "matrix" not in initial_config: final_config[env_name] = initial_config continue matrices = initial_config.pop("matrix") if not isinstance(matrices, list): message = f"Field `tool.hatch.envs.{env_name}.matrix` must be an array" raise TypeError(message) matrix_overrides = overrides.get("matrix", {}) if not isinstance(matrix_overrides, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.matrix` must be a table" raise TypeError(message) name_overrides = overrides.get("name", {}) if not isinstance(name_overrides, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.name` must be a table" raise TypeError(message) matrix_data = all_matrices[env_name] = {"config": deepcopy(initial_config)} all_envs = matrix_data["envs"] = {} for i, raw_matrix in enumerate(matrices, 1): matrix = raw_matrix if not isinstance(matrix, dict): message = f"Entry #{i} in field `tool.hatch.envs.{env_name}.matrix` must be a table" raise TypeError(message) if not matrix: message = f"Matrix #{i} in field `tool.hatch.envs.{env_name}.matrix` cannot be empty" raise ValueError(message) for j, (variable, values) in enumerate(matrix.items(), 1): if not variable: message = ( f"Variable #{j} in matrix #{i} in field `tool.hatch.envs.{env_name}.matrix` " f"cannot be an empty string" ) raise ValueError(message) if not isinstance(values, list): message = ( f"Variable `{variable}` in matrix #{i} in field `tool.hatch.envs.{env_name}.matrix` " f"must be an array" ) raise TypeError(message) if not values: message = ( f"Variable `{variable}` in matrix #{i} in field `tool.hatch.envs.{env_name}.matrix` " f"cannot be empty" ) raise ValueError(message) existing_values = set() for k, value in enumerate(values, 1): if not isinstance(value, str): message = ( f"Value #{k} of variable `{variable}` in matrix #{i} in field " f"`tool.hatch.envs.{env_name}.matrix` must be a string" ) raise TypeError(message) if not value: message = ( f"Value #{k} of variable `{variable}` in matrix #{i} in field " f"`tool.hatch.envs.{env_name}.matrix` cannot be an empty string" ) raise ValueError(message) if value in existing_values: message = ( f"Value #{k} of variable `{variable}` in matrix #{i} in field " f"`tool.hatch.envs.{env_name}.matrix` is a duplicate" ) raise ValueError(message) existing_values.add(value) variables = {} # Ensure that any Python variable comes first python_selected = False for variable in ("py", "python"): if variable in matrix: if python_selected: message = ( f"Matrix #{i} in field `tool.hatch.envs.{env_name}.matrix` " f"cannot contain both `py` and `python` variables" ) raise ValueError(message) python_selected = True # Only shallow copying is necessary since we just want to remove a key matrix = matrix.copy() variables[variable] = matrix.pop(variable) variables.update(matrix) for result in product(*variables.values()): # Make a value mapping for easy referencing variable_values = dict(zip(variables, result, strict=False)) # Create the environment's initial configuration new_config = deepcopy(initial_config) cached_matrix_overrides = [] # Apply any configuration based on matrix variables for variable, options in matrix_overrides.items(): if not isinstance(options, dict): message = ( f"Field `tool.hatch.envs.{env_name}.overrides.matrix.{variable}` must be a table" ) raise TypeError(message) if variable not in variables: continue apply_overrides( env_name, "matrix", variable, variable_values[variable], options, new_config ) cached_matrix_overrides.append((variable, variable_values[variable], options)) # Construct the environment name final_matrix_name_format = new_config.pop("matrix-name-format", matrix_name_format) env_name_parts = [] for j, (variable, value) in enumerate(variable_values.items()): if j == 0 and python_selected: new_config["python"] = value env_name_parts.append(value if value.startswith("py") else f"py{value}") else: env_name_parts.append(final_matrix_name_format.format(variable=variable, value=value)) new_env_name = "-".join(env_name_parts) cached_name_overrides = [] # Apply any configuration based on the final name, minus the prefix for non-default environments for pattern, options in name_overrides.items(): if not isinstance(options, dict): message = f"Field `tool.hatch.envs.{env_name}.overrides.name.{pattern}` must be a table" raise TypeError(message) if not re.search(pattern, new_env_name): continue apply_overrides(env_name, "name", pattern, new_env_name, options, new_config) cached_name_overrides.append((pattern, new_env_name, options)) if env_name != "default": new_env_name = f"{env_name}.{new_env_name}" # Save the generated environment final_config[new_env_name] = new_config cached_overrides[new_env_name] = { "platform": current_cached_overrides["platform"], "env": current_cached_overrides["env"], "matrix": cached_matrix_overrides, "name": cached_name_overrides, } all_envs[new_env_name] = variable_values if "py" in variable_values: all_envs[new_env_name] = {"python": variable_values.pop("py"), **variable_values} # Remove the root matrix generator del cached_overrides[env_name] # Save the variables used to generate the environments generated_envs.update(all_envs) for environment_collector in environment_collectors: environment_collector.finalize_environments(final_config) self._matrices = all_matrices self._internal_matrices = {} self._envs = final_config self._matrix_variables = generated_envs self._cached_env_overrides.update(cached_overrides) # Extract the internal environments self._internal_envs = {} for internal_name in get_internal_env_config(): try: self._internal_envs[internal_name] = self._envs.pop(internal_name) # Matrix except KeyError: self._internal_matrices[internal_name] = self._matrices.pop(internal_name) for env_name in [env_name for env_name in self._envs if env_name.startswith(f"{internal_name}.")]: self._internal_envs[env_name] = self._envs.pop(env_name) return self._envs @property def publish(self): if self._publish is None: config = self.config.get("publish", {}) if not isinstance(config, dict): message = "Field `tool.hatch.publish` must be a table" raise TypeError(message) for publisher, data in config.items(): if not isinstance(data, dict): message = f"Field `tool.hatch.publish.{publisher}` must be a table" raise TypeError(message) self._publish = config return self._publish @property def scripts(self): if self._scripts is None: script_config = self.config.get("scripts", {}) if not isinstance(script_config, dict): message = "Field `tool.hatch.scripts` must be a table" raise TypeError(message) config = {} for name, data in script_config.items(): if " " in name: message = f"Script name `{name}` in field `tool.hatch.scripts` must not contain spaces" raise ValueError(message) commands = [] if isinstance(data, str): commands.append(data) elif isinstance(data, list): for i, command in enumerate(data, 1): if not isinstance(command, str): message = f"Command #{i} in field `tool.hatch.scripts.{name}` must be a string" raise TypeError(message) commands.append(command) else: message = f"Field `tool.hatch.scripts.{name}` must be a string or an array of strings" raise TypeError(message) config[name] = commands seen = {} active = [] for script_name, commands in config.items(): commands[:] = expand_script_commands(script_name, commands, config, seen, active) self._scripts = config return self._scripts @property def requires_hatch(self) -> str: if self._requires_hatch is None: from packaging.specifiers import InvalidSpecifier, SpecifierSet requires_hatch = self.config.get("requires-hatch", "") if not isinstance(requires_hatch, str): message = "Field `tool.hatch.requires-hatch` must be a string" raise TypeError(message) try: self._hatch_specifier_set = SpecifierSet(requires_hatch) except InvalidSpecifier as e: message = f"Field `tool.hatch.requires-hatch` is invalid: {e}" raise ValueError(message) from None self._requires_hatch = str(self._hatch_specifier_set) return self._requires_hatch @property def hatch_specifier_set(self) -> SpecifierSet: from packaging.specifiers import SpecifierSet if self._hatch_specifier_set is None: _ = self.requires_hatch return cast(SpecifierSet, self._hatch_specifier_set) def finalize_env_overrides(self, option_types): # We lazily apply overrides because we need type information potentially defined by # environment plugins for their options if not self._cached_env_overrides: return for environments in (self.envs, self.internal_envs): for env_name, config in environments.items(): for override_name, data in self._cached_env_overrides.get(env_name, {}).items(): for condition, condition_value, options in data: apply_overrides( env_name, override_name, condition, condition_value, options, config, option_types ) self._cached_env_overrides.clear() class BuildConfig: def __init__(self, config: dict[str, Any]) -> None: self.__config = config self.__targets: dict[str, BuildTargetConfig] = config @cached_property def directory(self) -> str: directory = self.__config.get("directory", DEFAULT_BUILD_DIRECTORY) if not isinstance(directory, str): message = "Field `tool.hatch.build.directory` must be a string" raise TypeError(message) return directory @cached_property def dependencies(self) -> list[str]: dependencies: list[str] = self.__config.get("dependencies", []) if not isinstance(dependencies, list): message = "Field `tool.hatch.build.dependencies` must be an array" raise TypeError(message) for i, dependency in enumerate(dependencies, 1): if not isinstance(dependency, str): message = f"Dependency #{i} in field `tool.hatch.build.dependencies` must be a string" raise TypeError(message) return list(dependencies) @cached_property def hook_config(self) -> dict[str, dict[str, Any]]: hook_config: dict[str, dict[str, Any]] = self.__config.get("hooks", {}) if not isinstance(hook_config, dict): message = "Field `tool.hatch.build.hooks` must be a table" raise TypeError(message) for hook_name, config in hook_config.items(): if not isinstance(config, dict): message = f"Field `tool.hatch.build.hooks.{hook_name}` must be a table" raise TypeError(message) return finalize_hook_config(hook_config) @cached_property def __target_config(self) -> dict[str, Any]: config = self.__config.get("targets", {}) if not isinstance(config, dict): message = "Field `tool.hatch.build.targets` must be a table" raise TypeError(message) return config def target(self, target: str) -> BuildTargetConfig: if target in self.__targets: return self.__targets[target] config = self.__target_config.get(target, {}) if not isinstance(config, dict): message = f"Field `tool.hatch.build.targets.{target}` must be a table" raise TypeError(message) target_config = BuildTargetConfig(target, config, self) self.__targets[target] = target_config return target_config class BuildTargetConfig: def __init__(self, name: str, config: dict[str, Any], global_config: BuildConfig) -> None: self.__name = name self.__config = config self.__global_config = global_config @cached_property def directory(self) -> str: directory = self.__config.get("directory", self.__global_config.directory) if not isinstance(directory, str): message = f"Field `tool.hatch.build.targets.{self.__name}.directory` must be a string" raise TypeError(message) return directory @cached_property def dependencies(self) -> list[str]: dependencies: list[str] = self.__config.get("dependencies", []) if not isinstance(dependencies, list): message = f"Field `tool.hatch.build.targets.{self.__name}.dependencies` must be an array" raise TypeError(message) for i, dependency in enumerate(dependencies, 1): if not isinstance(dependency, str): message = ( f"Dependency #{i} in field `tool.hatch.build.targets.{self.__name}.dependencies` must be a string" ) raise TypeError(message) all_dependencies = list(self.__global_config.dependencies) all_dependencies.extend(dependencies) return all_dependencies @cached_property def hook_config(self) -> dict[str, dict[str, Any]]: hook_config: dict[str, dict[str, Any]] = self.__config.get("hooks", {}) if not isinstance(hook_config, dict): message = f"Field `tool.hatch.build.targets.{self.__name}.hooks` must be a table" raise TypeError(message) for hook_name, config in hook_config.items(): if not isinstance(config, dict): message = f"Field `tool.hatch.build.targets.{self.__name}.hooks.{hook_name}` must be a table" raise TypeError(message) config = self.__global_config.hook_config.copy() config.update(hook_config) return finalize_hook_config(config) def expand_script_commands(script_name, commands, config, seen, active): if script_name in seen: return seen[script_name] if script_name in active: active.append(script_name) message = f"Circular expansion detected for field `tool.hatch.scripts`: {' -> '.join(active)}" raise ValueError(message) active.append(script_name) expanded_commands = [] for command in commands: possible_script, args, ignore_exit_code = parse_script_command(command) if possible_script in config: expanded_commands.extend( format_script_commands( commands=expand_script_commands(possible_script, config[possible_script], config, seen, active), args=args, ignore_exit_code=ignore_exit_code, ) ) else: expanded_commands.append(command) seen[script_name] = expanded_commands active.pop() return expanded_commands def _populate_sources(config, global_config): """The top-level `sources` table is an alias for the `default` environment.""" # Parse here so errors name the top-level field rather than the environment it merges into parse_sources(global_config) if global_config: default_config = config["default"] default_config["sources"] = merge_source_tables(global_config, default_config.get("sources", {})) def _populate_default_env_values(env_name, data, config, seen, active): if env_name in seen: return if data.pop("detached", False): data["template"] = env_name data["skip-install"] = True template_name = data.pop("template", "default") if template_name not in config: message = f"Field `tool.hatch.envs.{env_name}.template` refers to an unknown environment `{template_name}`" raise ValueError(message) if env_name in active: active.append(env_name) message = f"Circular inheritance detected for field `tool.hatch.envs.*.template`: {' -> '.join(active)}" raise ValueError(message) if template_name == env_name: ensure_valid_environment(data) seen.add(env_name) return active.append(env_name) template_config = config[template_name] _populate_default_env_values(template_name, template_config, config, seen, active) for key, value in template_config.items(): if key == "matrix": continue if key == "scripts": scripts = data["scripts"] if "scripts" in data else data.setdefault("scripts", {}) for script, commands in value.items(): scripts.setdefault(script, commands) elif key == "sources": data["sources"] = merge_source_tables(value, data.get("sources", {})) else: data.setdefault(key, value) seen.add(env_name) active.pop() def finalize_hook_config(hook_config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: if env_var_enabled(BuildEnvVars.NO_HOOKS): return {} all_hooks_enabled = env_var_enabled(BuildEnvVars.HOOKS_ENABLE) final_hook_config: dict[str, dict[str, Any]] = { hook_name: config for hook_name, config in hook_config.items() if ( all_hooks_enabled or config.get("enable-by-default", True) or env_var_enabled(f"{BuildEnvVars.HOOK_ENABLE_PREFIX}{hook_name.upper()}") ) } return final_hook_config def env_var_enabled(env_var: str, *, default: bool = False) -> bool: if env_var in environ: return environ[env_var] in {"1", "true"} return default