/
niceSOFT
/
python3-hatchling
Обзор
Документация
Войти
/
niceSOFT
/
python3-hatchling
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
backend/src/hatchling/builders/plugin/interface.py
443 строки
16 KB
Cary Hawkins
Code clean up. (#2097)
15 ноя 2025, 18:42
Не верифицирован
15 ноя 2025, 18:42
ff4b404
Код
Авторство
О чём код?
from __future__ import annotations import os import re from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Generic, cast from hatchling.builders.config import BuilderConfig, BuilderConfigBound, env_var_enabled from hatchling.builders.constants import EXCLUDED_DIRECTORIES, EXCLUDED_FILES, BuildEnvVars from hatchling.builders.utils import get_relative_path, safe_walk from hatchling.plugin.manager import PluginManagerBound if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable from hatchling.bridge.app import Application from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.metadata.core import ProjectMetadata class IncludedFile: __slots__ = ("distribution_path", "path", "relative_path") def __init__(self, path: str, relative_path: str, distribution_path: str) -> None: self.path = path self.relative_path = relative_path self.distribution_path = distribution_path class BuilderInterface(ABC, Generic[BuilderConfigBound, PluginManagerBound]): """ Example usage: ```python tab="plugin.py" from hatchling.builders.plugin.interface import BuilderInterface class SpecialBuilder(BuilderInterface): PLUGIN_NAME = "special" ... ``` ```python tab="hooks.py" from hatchling.plugin import hookimpl from .plugin import SpecialBuilder @hookimpl def hatch_register_builder(): return SpecialBuilder ``` """ PLUGIN_NAME = "" """The name used for selection.""" def __init__( self, root: str, plugin_manager: PluginManagerBound | None = None, config: dict[str, Any] | None = None, metadata: ProjectMetadata | None = None, app: Application | None = None, ) -> None: self.__root = root self.__plugin_manager = cast(PluginManagerBound, plugin_manager) self.__raw_config = config self.__metadata = metadata self.__app = app self.__config = cast(BuilderConfigBound, None) self.__project_config: dict[str, Any] | None = None self.__hatch_config: dict[str, Any] | None = None self.__build_config: dict[str, Any] | None = None self.__build_targets: list[str] | None = None self.__target_config: dict[str, Any] | None = None # Metadata self.__project_id: str | None = None def build( self, *, directory: str | None = None, versions: list[str] | None = None, hooks_only: bool | None = None, clean: bool | None = None, clean_hooks_after: bool | None = None, clean_only: bool | None = False, ) -> Generator[str, None, None]: # Fail early for invalid project metadata self.metadata.validate_fields() if directory is None: directory = ( self.config.normalize_build_directory(os.environ[BuildEnvVars.LOCATION]) if BuildEnvVars.LOCATION in os.environ else self.config.directory ) if not os.path.isdir(directory): os.makedirs(directory) version_api = self.get_version_api() versions = versions or self.config.versions if versions: unknown_versions = set(versions) - set(version_api) if unknown_versions: message = ( f"Unknown versions for target `{self.PLUGIN_NAME}`: {', '.join(map(str, sorted(unknown_versions)))}" ) raise ValueError(message) if hooks_only is None: hooks_only = env_var_enabled(BuildEnvVars.HOOKS_ONLY) configured_build_hooks = self.get_build_hooks(directory) build_hooks = list(configured_build_hooks.values()) if clean_only: clean = True elif clean is None: clean = env_var_enabled(BuildEnvVars.CLEAN) if clean: if not hooks_only: self.clean(directory, versions) for build_hook in build_hooks: build_hook.clean(versions) if clean_only: return if clean_hooks_after is None: clean_hooks_after = env_var_enabled(BuildEnvVars.CLEAN_HOOKS_AFTER) for version in versions: self.app.display_debug(f"Building `{self.PLUGIN_NAME}` version `{version}`") build_data = self.get_default_build_data() self.set_build_data_defaults(build_data) # Allow inspection of configured build hooks and the order in which they run build_data["build_hooks"] = tuple(configured_build_hooks) # Execute all `initialize` build hooks for build_hook in build_hooks: build_hook.initialize(version, build_data) if hooks_only: self.app.display_debug(f"Only ran build hooks for `{self.PLUGIN_NAME}` version `{version}`") continue # Build the artifact with self.config.set_build_data(build_data): artifact = version_api[version](directory, **build_data) # Execute all `finalize` build hooks for build_hook in build_hooks: build_hook.finalize(version, build_data, artifact) if clean_hooks_after: for build_hook in build_hooks: build_hook.clean([version]) yield artifact def recurse_included_files(self) -> Iterable[IncludedFile]: """ Returns a consistently generated series of file objects for every file that should be distributed. Each file object has three `str` attributes: - `path` - the absolute path - `relative_path` - the path relative to the project root; will be an empty string for external files - `distribution_path` - the path to be distributed as """ yield from self.recurse_selected_project_files() yield from self.recurse_forced_files(self.config.get_force_include()) def recurse_selected_project_files(self) -> Iterable[IncludedFile]: if self.config.only_include: yield from self.recurse_explicit_files(self.config.only_include) else: yield from self.recurse_project_files() def recurse_project_files(self) -> Iterable[IncludedFile]: for root, dirs, files in safe_walk(self.root): relative_path = get_relative_path(root, self.root) dirs[:] = sorted(d for d in dirs if not self.config.directory_is_excluded(d, relative_path)) files.sort() is_package = "__init__.py" in files for f in files: if f in EXCLUDED_FILES: continue relative_file_path = os.path.join(relative_path, f) distribution_path = self.config.get_distribution_path(relative_file_path) if self.config.path_is_reserved(distribution_path): continue if self.config.include_path(relative_file_path, is_package=is_package): yield IncludedFile( os.path.join(root, f), relative_file_path, self.config.get_distribution_path(relative_file_path) ) def recurse_forced_files(self, inclusion_map: dict[str, str]) -> Iterable[IncludedFile]: for source, target_path in inclusion_map.items(): external = not source.startswith(self.root) if os.path.isfile(source): yield IncludedFile( source, "" if external else os.path.relpath(source, self.root), self.config.get_distribution_path(target_path), ) elif os.path.isdir(source): for root, dirs, files in safe_walk(source): relative_directory = get_relative_path(root, source) dirs[:] = sorted(d for d in dirs if d not in EXCLUDED_DIRECTORIES) files.sort() for f in files: if f in EXCLUDED_FILES: continue relative_file_path = os.path.join(target_path, relative_directory, f) distribution_path = self.config.get_distribution_path(relative_file_path) if not self.config.path_is_reserved(distribution_path): yield IncludedFile( os.path.join(root, f), "" if external else relative_file_path, distribution_path, ) else: msg = f"Forced include not found: {source}" raise FileNotFoundError(msg) def recurse_explicit_files(self, inclusion_map: dict[str, str]) -> Iterable[IncludedFile]: for source, target_path in inclusion_map.items(): external = not source.startswith(self.root) if os.path.isfile(source): distribution_path = self.config.get_distribution_path(target_path) if not self.config.path_is_reserved(distribution_path): yield IncludedFile( source, "" if external else os.path.relpath(source, self.root), self.config.get_distribution_path(target_path), ) elif os.path.isdir(source): for root, dirs, files in safe_walk(source): relative_directory = get_relative_path(root, source) dirs[:] = sorted(d for d in dirs if d not in EXCLUDED_DIRECTORIES) files.sort() is_package = "__init__.py" in files for f in files: if f in EXCLUDED_FILES: continue relative_file_path = os.path.join(target_path, relative_directory, f) distribution_path = self.config.get_distribution_path(relative_file_path) if self.config.path_is_reserved(distribution_path): continue if self.config.include_path(relative_file_path, explicit=True, is_package=is_package): yield IncludedFile( os.path.join(root, f), "" if external else relative_file_path, distribution_path ) @property def root(self) -> str: """ The root of the project tree. """ return self.__root @property def plugin_manager(self) -> PluginManagerBound: if self.__plugin_manager is None: from hatchling.plugin.manager import PluginManager self.__plugin_manager = PluginManager() return self.__plugin_manager @property def metadata(self) -> ProjectMetadata: if self.__metadata is None: from hatchling.metadata.core import ProjectMetadata self.__metadata = ProjectMetadata(self.root, self.plugin_manager, self.__raw_config) return self.__metadata @property def app(self) -> Application: """ An instance of [Application](../utilities.md#hatchling.bridge.app.Application). """ if self.__app is None: from hatchling.bridge.app import Application self.__app = cast(Application, Application().get_safe_application()) return self.__app @property def raw_config(self) -> dict[str, Any]: if self.__raw_config is None: self.__raw_config = self.metadata.config return self.__raw_config @property def project_config(self) -> dict[str, Any]: if self.__project_config is None: self.__project_config = self.metadata.core.config return self.__project_config @property def hatch_config(self) -> dict[str, Any]: if self.__hatch_config is None: self.__hatch_config = self.metadata.hatch.config return self.__hatch_config @property def config(self) -> BuilderConfigBound: """ An instance of [BuilderConfig](../utilities.md#hatchling.builders.config.BuilderConfig). """ if self.__config is None: self.__config = self.get_config_class()( self, self.root, self.PLUGIN_NAME, self.build_config, self.target_config ) return self.__config @property def build_config(self) -> dict[str, Any]: """ ```toml config-example [tool.hatch.build] ``` """ if self.__build_config is None: self.__build_config = self.metadata.hatch.build_config return self.__build_config @property def target_config(self) -> dict[str, Any]: """ ```toml config-example [tool.hatch.build.targets.<PLUGIN_NAME>] ``` """ if self.__target_config is None: target_config: dict[str, Any] = self.metadata.hatch.build_targets.get(self.PLUGIN_NAME, {}) if not isinstance(target_config, dict): message = f"Field `tool.hatch.build.targets.{self.PLUGIN_NAME}` must be a table" raise TypeError(message) self.__target_config = target_config return self.__target_config @property def project_id(self) -> str: if self.__project_id is None: self.__project_id = f"{self.normalize_file_name_component(self.metadata.core.name)}-{self.metadata.version}" return self.__project_id def get_build_hooks(self, directory: str) -> dict[str, BuildHookInterface]: configured_build_hooks = {} for hook_name, config in self.config.hook_config.items(): build_hook = self.plugin_manager.build_hook.get(hook_name) if build_hook is None: from hatchling.plugin.exceptions import UnknownPluginError message = f"Unknown build hook: {hook_name}" raise UnknownPluginError(message) configured_build_hooks[hook_name] = build_hook( self.root, config, self.config, self.metadata, directory, self.PLUGIN_NAME, self.app ) return configured_build_hooks @abstractmethod def get_version_api(self) -> dict[str, Callable]: """ A mapping of `str` versions to a callable that is used for building. Each callable must have the following signature: ```python def ...(build_dir: str, build_data: dict) -> str: ``` The return value must be the absolute path to the built artifact. """ def get_default_versions(self) -> list[str]: """ A list of versions to build when users do not specify any, defaulting to all versions. """ return list(self.get_version_api()) def get_default_build_data(self) -> dict[str, Any]: # noqa: PLR6301 """ A mapping that can be modified by [build hooks](../build-hook/reference.md) to influence the behavior of builds. """ return {} def set_build_data_defaults(self, build_data: dict[str, Any]) -> None: # noqa: PLR6301 build_data.setdefault("artifacts", []) build_data.setdefault("force_include", {}) def clean(self, directory: str, versions: list[str]) -> None: """ Called before builds if the `-c`/`--clean` flag was passed to the [`build`](../../cli/reference.md#hatch-build) command. """ @classmethod def get_config_class(cls) -> type[BuilderConfig]: """ Must return a subclass of [BuilderConfig](../utilities.md#hatchling.builders.config.BuilderConfig). """ return BuilderConfig @staticmethod def normalize_file_name_component(file_name: str) -> str: """ https://peps.python.org/pep-0427/#escaping-and-unicode """ return re.sub(r"[^\w\d.]+", "_", file_name, flags=re.UNICODE)