Builder plugins¶
See the documentation for build configuration.
Known third-party¶
- hatch-aws - used for building AWS Lambda functions with SAM
- hatch-zipped-directory - used for building ZIP archives for installation into various foreign package installation systems
 BuilderInterface ¶
 Example usage:
from hatchling.builders.plugin.interface import BuilderInterface
class SpecialBuilder(BuilderInterface):
    PLUGIN_NAME = "special"
    ...
from hatchling.plugin import hookimpl
from .plugin import SpecialBuilder
@hookimpl
def hatch_register_builder():
    return SpecialBuilder
Source code in backend/src/hatchling/builders/plugin/interface.py
 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)
 PLUGIN_NAME = ''  class-attribute instance-attribute  ¶
 The name used for selection.
 app: Application  property  ¶
 An instance of Application.
 root: str  property  ¶
 The root of the project tree.
 build_config: dict[str, Any]  property  ¶
 [tool.hatch.build]
[build]
 target_config: dict[str, Any]  property  ¶
 [tool.hatch.build.targets.<PLUGIN_NAME>]
[build.targets.<PLUGIN_NAME>]
 config: BuilderConfigBound  property  ¶
 An instance of BuilderConfig.
 get_config_class() -> type[BuilderConfig]  classmethod  ¶
 Must return a subclass of BuilderConfig.
Source code in backend/src/hatchling/builders/plugin/interface.py
 @classmethod
def get_config_class(cls) -> type[BuilderConfig]:
    """
    Must return a subclass of [BuilderConfig](../utilities.md#hatchling.builders.config.BuilderConfig).
    """
    return BuilderConfig
 get_version_api() -> dict[str, Callable]  abstractmethod  ¶
 A mapping of str versions to a callable that is used for building. Each callable must have the following signature:
def ...(build_dir: str, build_data: dict) -> str:
The return value must be the absolute path to the built artifact.
Source code in backend/src/hatchling/builders/plugin/interface.py
 @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.
    """
 get_default_versions() -> list[str] ¶
 A list of versions to build when users do not specify any, defaulting to all versions.
Source code in backend/src/hatchling/builders/plugin/interface.py
 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())
 clean(directory: str, versions: list[str]) -> None ¶
 Called before builds if the -c/--clean flag was passed to the build command.
Source code in backend/src/hatchling/builders/plugin/interface.py
 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.
    """
 recurse_included_files() -> 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
Source code in backend/src/hatchling/builders/plugin/interface.py
 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())
 get_default_build_data() -> dict[str, Any] ¶
 A mapping that can be modified by build hooks to influence the behavior of builds.
Source code in backend/src/hatchling/builders/plugin/interface.py
 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 {}