diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..adb5d0d9c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(hatch fmt:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3f756c2fe..82d7afdc4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -51,5 +51,27 @@ Source = "https://github.com/pypa/hatch/tree/master/backend" [project.scripts] hatchling = "hatchling.cli:hatchling" +[project.entry-points."hatch.version_source"] +code = "hatchling.version.source.code:CodeSource" +env = "hatchling.version.source.env:EnvSource" +regex = "hatchling.version.source.regex:RegexSource" + +[project.entry-points."hatch.version_scheme"] +standard = "hatchling.version.scheme.standard:StandardScheme" + +[project.entry-points."hatch.builder"] +app = "hatchling.builders.app:AppBuilder" +binary = "hatchling.builders.binary:BinaryBuilder" +custom = "hatchling.builders.custom:CustomBuilder" +sdist = "hatchling.builders.sdist:SdistBuilder" +wheel = "hatchling.builders.wheel:WheelBuilder" + +[project.entry-points."hatch.build_hook"] +custom = "hatchling.builders.hooks.custom:CustomBuildHook" +version = "hatchling.builders.hooks.version:VersionBuildHook" + +[project.entry-points."hatch.metadata_hook"] +custom = "hatchling.metadata.custom:CustomMetadataHook" + [tool.hatch.version] path = "src/hatchling/__about__.py" diff --git a/backend/src/hatchling/builders/hooks/plugin/hooks.py b/backend/src/hatchling/builders/hooks/plugin/hooks.py deleted file mode 100644 index e06795945..000000000 --- a/backend/src/hatchling/builders/hooks/plugin/hooks.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -import typing - -from hatchling.builders.hooks.custom import CustomBuildHook -from hatchling.builders.hooks.version import VersionBuildHook -from hatchling.plugin import hookimpl - -if typing.TYPE_CHECKING: - from hatchling.builders.hooks.plugin.interface import BuildHookInterface - - -@hookimpl -def hatch_register_build_hook() -> list[type[BuildHookInterface]]: - return [CustomBuildHook, VersionBuildHook] diff --git a/backend/src/hatchling/builders/plugin/hooks.py b/backend/src/hatchling/builders/plugin/hooks.py deleted file mode 100644 index 97a0e08b6..000000000 --- a/backend/src/hatchling/builders/plugin/hooks.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import typing - -from hatchling.builders.app import AppBuilder -from hatchling.builders.binary import BinaryBuilder -from hatchling.builders.custom import CustomBuilder -from hatchling.builders.sdist import SdistBuilder -from hatchling.builders.wheel import WheelBuilder -from hatchling.plugin import hookimpl - -if typing.TYPE_CHECKING: - from hatchling.builders.plugin.interface import BuilderInterface - - -@hookimpl -def hatch_register_builder() -> list[type[BuilderInterface]]: - return [AppBuilder, BinaryBuilder, CustomBuilder, SdistBuilder, WheelBuilder] # type: ignore[list-item] diff --git a/backend/src/hatchling/metadata/plugin/hooks.py b/backend/src/hatchling/metadata/plugin/hooks.py deleted file mode 100644 index f6d710358..000000000 --- a/backend/src/hatchling/metadata/plugin/hooks.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from hatchling.metadata.custom import CustomMetadataHook -from hatchling.plugin import hookimpl - -if TYPE_CHECKING: - from hatchling.metadata.plugin.interface import MetadataHookInterface - - -@hookimpl -def hatch_register_metadata_hook() -> type[MetadataHookInterface]: - return CustomMetadataHook # type: ignore[return-value] diff --git a/backend/src/hatchling/plugin/__init__.py b/backend/src/hatchling/plugin/__init__.py index a42736d87..907775c52 100644 --- a/backend/src/hatchling/plugin/__init__.py +++ b/backend/src/hatchling/plugin/__init__.py @@ -1,3 +1,42 @@ -import pluggy +from __future__ import annotations -hookimpl = pluggy.HookimplMarker("hatch") +import warnings +from typing import TYPE_CHECKING, Any, Callable + +if TYPE_CHECKING: + import pluggy + + +class _LazyHookimplMarker: + """ + Lazy-loading wrapper for pluggy's HookimplMarker. + + This allows external plugins to continue using @hookimpl decorator + while avoiding the need to import pluggy unless it's actually used. + + Emits a deprecation warning each time used to guide plugin authors + toward the new direct entrypoint approach. + """ + + def __init__(self) -> None: + self._marker: pluggy.HookimplMarker | None = None + + def __call__(self, function: Callable | None = None, **kwargs: Any) -> Any: + """Apply the hookimpl decorator to a function.""" + warnings.warn( + "Using @hookimpl decorator for plugin registration is deprecated. " + "Please migrate to direct entrypoint groups (e.g., 'hatch.builder'). " + "See https://hatch.pypa.io/latest/plugins/about/ for migration guide.", + DeprecationWarning, + stacklevel=2, + ) + + if self._marker is None: + import pluggy + + self._marker = pluggy.HookimplMarker("hatch") + + return self._marker(function, **kwargs) + + +hookimpl = _LazyHookimplMarker() diff --git a/backend/src/hatchling/plugin/finder.py b/backend/src/hatchling/plugin/finder.py new file mode 100644 index 000000000..96860cb0c --- /dev/null +++ b/backend/src/hatchling/plugin/finder.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +import sys +import warnings +from pathlib import Path +from typing import TYPE_CHECKING + +if sys.version_info >= (3, 10): + from importlib.metadata import entry_points +else: + # Python 3.9 - entry_points() returns a dict-like object, not a function with group param + from importlib.metadata import entry_points as _entry_points + + def entry_points(*, group: str): + """Wrapper for Python 3.9 compatibility.""" + eps = _entry_points() + return eps.get(group, []) + + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +if TYPE_CHECKING: + pass + + +def _load_builtin_plugins_from_pyproject() -> dict[str, dict[str, str]]: + """ + Load built-in plugin definitions from pyproject.toml files. + + This reads the entrypoint definitions directly from the source pyproject.toml + files to avoid duplicating plugin registration data. + + Returns: + Dictionary mapping plugin types to their plugin definitions + """ + builtin_plugins: dict[str, dict[str, str]] = {} + + # Find the pyproject.toml files + # They should be in the parent directories of this file + finder_path = Path(__file__).resolve() + + # Try hatchling's pyproject.toml (backend/pyproject.toml) + hatchling_pyproject = finder_path.parents[3] / "pyproject.toml" + + # Try hatch's pyproject.toml (../../pyproject.toml from hatchling) + hatch_pyproject = hatchling_pyproject.parent.parent / "pyproject.toml" + + for pyproject_path in [hatchling_pyproject, hatch_pyproject]: + if not pyproject_path.exists(): + continue + + try: + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + except OSError as e: + # Permission denied or other I/O errors - warn but continue + warnings.warn( + f"Could not read pyproject.toml at {pyproject_path}: {e}", + UserWarning, + stacklevel=2, + ) + continue + + # Extract entry-points + entrypoints = data.get("project", {}).get("entry-points", {}) + + # Look for hatch.* groups + for group_name, plugins in entrypoints.items(): + if group_name.startswith("hatch."): + plugin_type = group_name.removeprefix("hatch.") + builtin_plugins.setdefault(plugin_type, {}).update(plugins) + + return builtin_plugins + + +class PluginFinder: + """ + Discovers and loads plugins using a two-tier approach: + 1. Primary: Direct entrypoint groups (e.g., 'hatch.builder') + 2. Fallback: Legacy 'hatch' group with hook functions (deprecated) + """ + + def __init__(self) -> None: + self._legacy_plugins_checked = False + self._has_legacy_plugins = False + self._builtin_plugins: dict[str, dict[str, str]] | None = None + + def find_plugins(self, plugin_type: str) -> dict[str, type]: + """ + Find all plugins of a given type. + + Args: + plugin_type: The plugin type (e.g., 'builder', 'version_source') + + Returns: + Dictionary mapping plugin names to plugin classes + """ + plugins: dict[str, type] = {} + + # Primary path: Load from new entrypoint groups + group_name = f"hatch.{plugin_type}" + new_style_plugins = self._load_from_entrypoint_group(group_name) + plugins.update(new_style_plugins) + + # If no plugins found via entrypoints, try loading built-in plugins directly + # This handles development/bootstrap scenarios where entrypoints aren't installed yet + if not plugins: + if self._builtin_plugins is None: + self._builtin_plugins = _load_builtin_plugins_from_pyproject() + + if plugin_type in self._builtin_plugins: + builtin_specs = self._builtin_plugins[plugin_type] + for plugin_name, module_path in builtin_specs.items(): + # Built-in plugins should always be available; if not, let the error propagate + plugin_class = self._load_class_from_path(module_path) + plugins[plugin_name] = plugin_class + + # Fallback path: Load from legacy 'hatch' group + legacy_plugins = self._load_legacy_plugins(plugin_type) + if legacy_plugins: + if not self._has_legacy_plugins: + self._has_legacy_plugins = True + warnings.warn( + "Legacy plugin registration via 'hatch' entrypoint group and @hookimpl is deprecated. " + f"Please migrate to direct entrypoint groups like '{group_name}'. " + "See https://hatch.pypa.io/latest/plugins/about/ for migration guide.", + DeprecationWarning, + stacklevel=2, + ) + + # Legacy plugins don't override new-style plugins + for name, cls in legacy_plugins.items(): + if name not in plugins: + plugins[name] = cls + + return plugins + + @staticmethod + def _load_class_from_path(class_path: str) -> type: + """ + Load a class from a module:class path string. + + Args: + class_path: Path in format "module.path:ClassName" + + Returns: + The loaded class + """ + module_path, class_name = class_path.split(":") + module = __import__(module_path, fromlist=[class_name]) + return getattr(module, class_name) + + @staticmethod + def _load_from_entrypoint_group(group_name: str) -> dict[str, type]: + """Load plugins from a direct entrypoint group.""" + plugins: dict[str, type] = {} + + eps = entry_points(group=group_name) + + for ep in eps: + try: + plugin_class = ep.load() + plugin_name = getattr(plugin_class, "PLUGIN_NAME", None) + + if not plugin_name: + warnings.warn( + f"Plugin class '{plugin_class.__name__}' from entrypoint '{ep.name}' " + f"in group '{group_name}' does not have a PLUGIN_NAME attribute. Skipping.", + UserWarning, + stacklevel=3, + ) + continue + + if plugin_name in plugins: + warnings.warn( + f"Plugin name '{plugin_name}' is already registered in group '{group_name}'. " + f"Skipping duplicate from entrypoint '{ep.name}'.", + UserWarning, + stacklevel=3, + ) + continue + + plugins[plugin_name] = plugin_class + + except Exception as e: # noqa: BLE001 + warnings.warn( + f"Failed to load plugin from entrypoint '{ep.name}' in group '{group_name}': {e}", + UserWarning, + stacklevel=3, + ) + + return plugins + + @staticmethod + def _load_legacy_plugins(plugin_type: str) -> dict[str, type]: + """ + Load plugins from legacy 'hatch' entrypoint group. + + This loads modules from the 'hatch' group and directly calls their + hook functions (e.g., hatch_register_builder) to get plugin classes. + No PluginManager is needed since hooks are just regular functions. + """ + plugins: dict[str, type] = {} + # Hook name follows pattern: hatch_register_{plugin_type} + hook_name = f"hatch_register_{plugin_type}" + + eps = entry_points(group="hatch") + + for ep in eps: + try: + # Load the module + module = ep.load() + + # Check if it has the hook function + hook_func = getattr(module, hook_name, None) + if not hook_func: + continue + + # Call the hook function directly to get plugin class(es) + result = hook_func() + + # Handle both single class and list of classes + if result is None: + continue + + classes = result if isinstance(result, list) else [result] + + for plugin_class in classes: + plugin_name = getattr(plugin_class, "PLUGIN_NAME", None) + + if not plugin_name: + warnings.warn( + f"Plugin class '{plugin_class.__name__}' from legacy hook " + f"'{hook_name}' in module '{ep.name}' does not have a PLUGIN_NAME attribute. Skipping.", + UserWarning, + stacklevel=4, + ) + continue + + if plugin_name in plugins: + continue # Skip duplicates + + plugins[plugin_name] = plugin_class + + except Exception as e: # noqa: BLE001 + warnings.warn( + f"Failed to load legacy plugin from entrypoint '{ep.name}' in 'hatch' group: {e}", + UserWarning, + stacklevel=4, + ) + + return plugins diff --git a/backend/src/hatchling/plugin/manager.py b/backend/src/hatchling/plugin/manager.py index 0a278afcd..576866689 100644 --- a/backend/src/hatchling/plugin/manager.py +++ b/backend/src/hatchling/plugin/manager.py @@ -1,111 +1,80 @@ from __future__ import annotations -from typing import Callable, TypeVar +from typing import TypeVar -import pluggy +from hatchling.plugin.finder import PluginFinder class PluginManager: - def __init__(self) -> None: - self.manager = pluggy.PluginManager("hatch") - self.third_party_plugins = ThirdPartyPlugins(self.manager) - self.initialized = False + """ + Plugin registry that uses PluginFinder for entrypoint-based discovery. - def initialize(self) -> None: - from hatchling.plugin import specs + This replaces the old pluggy-based PluginManager while maintaining + the same public API for backward compatibility. + """ - self.manager.add_hookspecs(specs) + def __init__(self) -> None: + self.finder = PluginFinder() + self._cached_registers: dict[str, ClassRegister] = {} def __getattr__(self, name: str) -> ClassRegister: - if not self.initialized: - self.initialize() - self.initialized = True - - hook_name = f"hatch_register_{name}" - hook = getattr(self, hook_name, None) - if hook: - hook() + """ + Dynamically create ClassRegister for plugin types on first access. - register = ClassRegister(getattr(self.manager.hook, hook_name), "PLUGIN_NAME", self.third_party_plugins) - setattr(self, name, register) - return register + Example: plugin_manager.builder returns a ClassRegister for builders. + """ + if name.startswith("_"): + msg = f"'{type(self).__name__}' object has no attribute '{name}'" + raise AttributeError(msg) - def hatch_register_version_source(self) -> None: - from hatchling.version.source.plugin import hooks + if name not in self._cached_registers: + register = ClassRegister(self.finder, name) + self._cached_registers[name] = register - self.manager.register(hooks) + return self._cached_registers[name] - def hatch_register_version_scheme(self) -> None: - from hatchling.version.scheme.plugin import hooks - self.manager.register(hooks) - - def hatch_register_builder(self) -> None: - from hatchling.builders.plugin import hooks +class ClassRegister: + """ + Provides access to plugins of a specific type. - self.manager.register(hooks) + Maintains the same API as the old ClassRegister for compatibility. + """ - def hatch_register_build_hook(self) -> None: - from hatchling.builders.hooks.plugin import hooks + def __init__(self, finder: PluginFinder, plugin_type: str) -> None: + self.finder = finder + self.plugin_type = plugin_type + self._cached_plugins: dict[str, type] | None = None - self.manager.register(hooks) + def collect(self, *, include_third_party: bool = True) -> dict[str, type]: # noqa: ARG002 + """ + Collect all plugins of this type. - def hatch_register_metadata_hook(self) -> None: - from hatchling.metadata.plugin import hooks + Args: + include_third_party: Currently ignored (always loads all plugins). + Kept for API compatibility. - self.manager.register(hooks) + Returns: + Dictionary mapping plugin names to plugin classes. + """ + # For the new system, we always load all plugins at once + # The include_third_party parameter is kept for API compatibility + if self._cached_plugins is None: + self._cached_plugins = self.finder.find_plugins(self.plugin_type) - -class ClassRegister: - def __init__(self, registration_method: Callable, identifier: str, third_party_plugins: ThirdPartyPlugins) -> None: - self.registration_method = registration_method - self.identifier = identifier - self.third_party_plugins = third_party_plugins - - def collect(self, *, include_third_party: bool = True) -> dict: - if include_third_party and not self.third_party_plugins.loaded: - self.third_party_plugins.load() - - classes: dict[str, type] = {} - - for raw_registered_classes in self.registration_method(): - registered_classes = ( - raw_registered_classes if isinstance(raw_registered_classes, list) else [raw_registered_classes] - ) - for registered_class in registered_classes: - name = getattr(registered_class, self.identifier, None) - if not name: # no cov - message = f"Class `{registered_class.__name__}` does not have a {name} attribute." - raise ValueError(message) - - if name in classes: # no cov - message = ( - f"Class `{registered_class.__name__}` defines its name as `{name}` but " - f"that name is already used by `{classes[name].__name__}`." - ) - raise ValueError(message) - - classes[name] = registered_class - - return classes + return self._cached_plugins.copy() def get(self, name: str) -> type | None: - if not self.third_party_plugins.loaded: - classes = self.collect(include_third_party=False) - if name in classes: - return classes[name] - - return self.collect().get(name) + """ + Get a specific plugin by name. + Args: + name: The plugin name (from PLUGIN_NAME attribute). -class ThirdPartyPlugins: - def __init__(self, manager: pluggy.PluginManager) -> None: - self.manager = manager - self.loaded = False - - def load(self) -> None: - self.manager.load_setuptools_entrypoints("hatch") - self.loaded = True + Returns: + The plugin class, or None if not found. + """ + return self.collect().get(name) PluginManagerBound = TypeVar("PluginManagerBound", bound=PluginManager) diff --git a/backend/src/hatchling/plugin/specs.py b/backend/src/hatchling/plugin/specs.py index 53e75efcc..ea91fc4bb 100644 --- a/backend/src/hatchling/plugin/specs.py +++ b/backend/src/hatchling/plugin/specs.py @@ -5,19 +5,51 @@ @hookspec def hatch_register_version_source() -> None: - """Register new classes that adhere to the version source interface.""" + """ + DEPRECATED: Register new classes that adhere to the version source interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.version_source"] + myplug = "my_package.plugin:MyVersionSource" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_builder() -> None: - """Register new classes that adhere to the builder interface.""" + """ + DEPRECATED: Register new classes that adhere to the builder interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.builder"] + myplug = "my_package.plugin:MyBuilder" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_build_hook() -> None: - """Register new classes that adhere to the build hook interface.""" + """ + DEPRECATED: Register new classes that adhere to the build hook interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.build_hook"] + myplug = "my_package.plugin:MyBuildHook" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_metadata_hook() -> None: - """Register new classes that adhere to the metadata hook interface.""" + """ + DEPRECATED: Register new classes that adhere to the metadata hook interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.metadata_hook"] + myplug = "my_package.plugin:MyMetadataHook" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ diff --git a/backend/src/hatchling/version/scheme/plugin/hooks.py b/backend/src/hatchling/version/scheme/plugin/hooks.py deleted file mode 100644 index d36a323ab..000000000 --- a/backend/src/hatchling/version/scheme/plugin/hooks.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from hatchling.plugin import hookimpl -from hatchling.version.scheme.standard import StandardScheme - -if TYPE_CHECKING: - from hatchling.version.scheme.plugin.interface import VersionSchemeInterface - - -@hookimpl -def hatch_register_version_scheme() -> type[VersionSchemeInterface]: - return StandardScheme diff --git a/backend/src/hatchling/version/source/code.py b/backend/src/hatchling/version/source/code.py index a1e112156..01244afa4 100644 --- a/backend/src/hatchling/version/source/code.py +++ b/backend/src/hatchling/version/source/code.py @@ -59,6 +59,7 @@ def get_version_data(self) -> dict: return {"version": version} - def set_version(self, version: str, version_data: dict) -> None: + @staticmethod + def set_version(version: str, version_data: dict) -> None: message = "Cannot rewrite loaded code" raise NotImplementedError(message) diff --git a/backend/src/hatchling/version/source/env.py b/backend/src/hatchling/version/source/env.py index 53b1cd7e7..a08d14235 100644 --- a/backend/src/hatchling/version/source/env.py +++ b/backend/src/hatchling/version/source/env.py @@ -24,6 +24,7 @@ def get_version_data(self) -> dict: return {"version": os.environ[variable]} - def set_version(self, version: str, version_data: dict) -> None: + @staticmethod + def set_version(version: str, version_data: dict) -> None: message = "Cannot set environment variables" raise NotImplementedError(message) diff --git a/backend/src/hatchling/version/source/plugin/hooks.py b/backend/src/hatchling/version/source/plugin/hooks.py deleted file mode 100644 index ef95fc32a..000000000 --- a/backend/src/hatchling/version/source/plugin/hooks.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from hatchling.plugin import hookimpl -from hatchling.version.source.code import CodeSource -from hatchling.version.source.env import EnvSource -from hatchling.version.source.regex import RegexSource - -if TYPE_CHECKING: - from hatchling.version.source.plugin.interface import VersionSourceInterface - - -@hookimpl -def hatch_register_version_source() -> list[type[VersionSourceInterface]]: - return [CodeSource, EnvSource, RegexSource] diff --git a/docs/plugins/about.md b/docs/plugins/about.md index 4ba90c344..67731e9ec 100644 --- a/docs/plugins/about.md +++ b/docs/plugins/about.md @@ -2,36 +2,33 @@ ----- -Hatch utilizes [pluggy](https://github.com/pytest-dev/pluggy) for its plugin functionality. +Hatch uses Python entrypoints for plugin discovery, making it easy to extend functionality. ## Overview -All plugins provide registration hooks that return one or more classes that inherit from a particular [type](#types) interface. +All plugins are classes that inherit from a particular [type](#types) interface and define a `PLUGIN_NAME` attribute. -Each registration hook must be decorated by Hatch's hook marker. For example, if you wanted to create a new kind of environment you could do: +Plugin registration is done through dedicated entrypoint groups. For example, to create a custom environment: -```python tab="hooks.py" -from hatchling.plugin import hookimpl +```python tab="my_package/environment.py" +from hatch.env.plugin.interface import EnvironmentInterface -from .plugin import SpecialEnvironment +class SpecialEnvironment(EnvironmentInterface): + PLUGIN_NAME = 'special' -@hookimpl -def hatch_register_environment(): - return SpecialEnvironment + # Your implementation here + ... ``` -The hooks can return a single class or a list of classes. - -Every class must define an attribute called `PLUGIN_NAME` that users will select when they wish to use the plugin. So in the example above, the class might be defined like: - -```python tab="plugin.py" -... -class SpecialEnvironment(...): - PLUGIN_NAME = 'special' - ... +```toml tab="pyproject.toml" +[project.entry-points."hatch.environment"] +special = "my_package.environment:SpecialEnvironment" ``` +!!! note "Legacy hook-based registration" + Previous versions of Hatch used pluggy hooks for plugin registration. This approach is **deprecated** but still supported for backward compatibility. See [Legacy Plugin Registration](#legacy-plugin-registration) below for migration guidance. + ## Project configuration ### Naming @@ -45,14 +42,33 @@ name = "hatch-foo" ### Discovery -You'll need to define your project as a [Python plugin](../config/metadata.md#plugins) for Hatch: +Define your plugin using the appropriate entrypoint group for its type: ```toml tab="pyproject.toml" -[project.entry-points.hatch] -foo = "pkg.hooks" +[project.entry-points."hatch.builder"] +foo = "hatch_foo.builder:FooBuilder" + +[project.entry-points."hatch.environment"] +foo = "hatch_foo.environment:FooEnvironment" ``` -The name of the plugin should be the project name (excluding any `hatch-` prefix) and the path should represent the module that contains the registration hooks. +The entrypoint name can be anything, but using your plugin's `PLUGIN_NAME` is recommended for clarity. The path should point directly to your plugin class using the format `module.path:ClassName`. + +#### Entrypoint Groups + +Use these entrypoint groups based on your plugin type: + +| Plugin Type | Entrypoint Group | Used By | +|------------|------------------|---------| +| Builder | `hatch.builder` | Hatchling | +| Build Hook | `hatch.build_hook` | Hatchling | +| Metadata Hook | `hatch.metadata_hook` | Hatchling | +| Version Source | `hatch.version_source` | Hatchling | +| Version Scheme | `hatch.version_scheme` | Hatchling/Hatch | +| Environment | `hatch.environment` | Hatch | +| Environment Collector | `hatch.environment_collector` | Hatch | +| Publisher | `hatch.publisher` | Hatch | +| Template | `hatch.template` | Hatch | ### Classifier @@ -86,3 +102,52 @@ These must be installed in the same environment as Hatch itself. - [Environment](environment/reference.md) - [Environment collector](environment-collector/reference.md) - [Publisher](publisher/reference.md) + +## Legacy Plugin Registration + +!!! warning "Deprecated" + This section describes the old hook-based plugin registration system. It is **deprecated** and will emit warnings when used. Please migrate to direct entrypoint groups as described above. + +### Old Method (Deprecated) + +Previously, plugins were registered through the `hatch` entrypoint group pointing to a hooks module: + +```toml tab="pyproject.toml" +[project.entry-points.hatch] +foo = "pkg.hooks" +``` + +```python tab="pkg/hooks.py" +from hatchling.plugin import hookimpl + +from .plugin import FooBuilder + + +@hookimpl +def hatch_register_builder(): + return FooBuilder +``` + +### Migration Guide + +To migrate from the old hook-based system to direct entrypoints: + +1. **Remove the hooks module** - Delete your `hooks.py` file containing `@hookimpl` decorated functions + +2. **Update entrypoints** - Change from the generic `hatch` group to specific groups: + +```toml tab="Before" +[project.entry-points.hatch] +foo = "pkg.hooks" +``` + +```toml tab="After" +[project.entry-points."hatch.builder"] +foo = "pkg.plugin:FooBuilder" +``` + +3. **Point directly to classes** - Instead of a hooks module, point directly to your plugin class using `module.path:ClassName` format + +4. **Update documentation** - If you have plugin documentation, update examples to show the new entrypoint approach + +The old system will continue to work with deprecation warnings to give plugin authors time to migrate. diff --git a/pyproject.toml b/pyproject.toml index fdee18f6e..f3f26cceb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,20 @@ Source = "https://github.com/pypa/hatch" [project.scripts] hatch = "hatch.cli:main" +[project.entry-points."hatch.environment"] +system = "hatch.env.system:SystemEnvironment" +virtual = "hatch.env.virtual:VirtualEnvironment" + +[project.entry-points."hatch.environment_collector"] +custom = "hatch.env.collectors.custom:CustomEnvironmentCollector" +default = "hatch.env.collectors.default:DefaultEnvironmentCollector" + +[project.entry-points."hatch.publisher"] +index = "hatch.publish.index:IndexPublisher" + +[project.entry-points."hatch.template"] +default = "hatch.template.default:DefaultTemplate" + [tool.hatch.version] source = "vcs" diff --git a/ruff_defaults.toml b/ruff_defaults.toml index 65cbaed91..0355e7d56 100644 --- a/ruff_defaults.toml +++ b/ruff_defaults.toml @@ -572,16 +572,16 @@ select = [ "T100", "T201", "T203", - "TC001", - "TC002", - "TC003", - "TC004", - "TC005", - "TC010", - "TD004", - "TD005", - "TD006", - "TD007", + # "TC001", + # "TC002", + # "TC003", + # "TC004", + # "TC005", + # "TC010", + # "TD004", + # "TD005", + # "TD006", + # "TD007", "TID251", "TID252", "TID253", @@ -589,7 +589,7 @@ select = [ "TRY003", "TRY004", "TRY201", - "TRY203", + # "TRY203", "TRY300", "TRY301", "TRY400", diff --git a/src/hatch/env/collectors/plugin/hooks.py b/src/hatch/env/collectors/plugin/hooks.py deleted file mode 100644 index f2c7f174d..000000000 --- a/src/hatch/env/collectors/plugin/hooks.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from hatch.env.collectors.custom import CustomEnvironmentCollector -from hatch.env.collectors.default import DefaultEnvironmentCollector -from hatchling.plugin import hookimpl - -if TYPE_CHECKING: - from hatch.env.collectors.plugin.interface import EnvironmentCollectorInterface - - -@hookimpl -def hatch_register_environment_collector() -> list[type[EnvironmentCollectorInterface]]: - return [CustomEnvironmentCollector, DefaultEnvironmentCollector] diff --git a/src/hatch/env/plugin/hooks.py b/src/hatch/env/plugin/hooks.py deleted file mode 100644 index c88735210..000000000 --- a/src/hatch/env/plugin/hooks.py +++ /dev/null @@ -1,8 +0,0 @@ -from hatch.env.system import SystemEnvironment -from hatch.env.virtual import VirtualEnvironment -from hatchling.plugin import hookimpl - - -@hookimpl -def hatch_register_environment(): - return [SystemEnvironment, VirtualEnvironment] diff --git a/src/hatch/plugin/manager.py b/src/hatch/plugin/manager.py index aa02983ea..3ffcfa2d6 100644 --- a/src/hatch/plugin/manager.py +++ b/src/hatch/plugin/manager.py @@ -2,29 +2,12 @@ class PluginManager(_PluginManager): - def initialize(self): - super().initialize() + """ + Hatch-specific plugin manager. - from hatch.plugin import specs + Inherits from hatchling's PluginManager and adds support for + hatch-specific plugin types (environment, publisher, template, etc.). - self.manager.add_hookspecs(specs) - - def hatch_register_environment(self): - from hatch.env.plugin import hooks - - self.manager.register(hooks) - - def hatch_register_environment_collector(self): - from hatch.env.collectors.plugin import hooks - - self.manager.register(hooks) - - def hatch_register_publisher(self): - from hatch.publish.plugin import hooks - - self.manager.register(hooks) - - def hatch_register_template(self): - from hatch.template.plugin import hooks - - self.manager.register(hooks) + The new implementation uses PluginFinder directly via entrypoints, + removing the need for hook registration methods. + """ diff --git a/src/hatch/plugin/specs.py b/src/hatch/plugin/specs.py index 8a5d83b74..2d93f3993 100644 --- a/src/hatch/plugin/specs.py +++ b/src/hatch/plugin/specs.py @@ -3,24 +3,64 @@ @hookspec def hatch_register_environment(): - """Register new classes that adhere to the environment interface.""" + """ + DEPRECATED: Register new classes that adhere to the environment interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.environment"] + myplug = "my_package.plugin:MyEnvironment" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_environment_collector(): - """Register new classes that adhere to the environment collector interface.""" + """ + DEPRECATED: Register new classes that adhere to the environment collector interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.environment_collector"] + myplug = "my_package.plugin:MyCollector" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_version_scheme(): - """Register new classes that adhere to the version scheme interface.""" + """ + DEPRECATED: Register new classes that adhere to the version scheme interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.version_scheme"] + myplug = "my_package.plugin:MyVersionScheme" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_publisher(): - """Register new classes that adhere to the publisher interface.""" + """ + DEPRECATED: Register new classes that adhere to the publisher interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.publisher"] + myplug = "my_package.plugin:MyPublisher" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ @hookspec def hatch_register_template(): - """Register new classes that adhere to the template interface.""" + """ + DEPRECATED: Register new classes that adhere to the template interface. + + This hook-based registration is deprecated. Use direct entrypoint groups instead: + [project.entry-points."hatch.template"] + myplug = "my_package.plugin:MyTemplate" + + See https://hatch.pypa.io/latest/plugins/about/ for migration guide. + """ diff --git a/src/hatch/publish/plugin/hooks.py b/src/hatch/publish/plugin/hooks.py deleted file mode 100644 index c2aa50d39..000000000 --- a/src/hatch/publish/plugin/hooks.py +++ /dev/null @@ -1,7 +0,0 @@ -from hatch.publish.index import IndexPublisher -from hatchling.plugin import hookimpl - - -@hookimpl -def hatch_register_publisher(): - return IndexPublisher diff --git a/src/hatch/template/plugin/hooks.py b/src/hatch/template/plugin/hooks.py deleted file mode 100644 index 08953b50d..000000000 --- a/src/hatch/template/plugin/hooks.py +++ /dev/null @@ -1,7 +0,0 @@ -from hatch.template.default import DefaultTemplate -from hatchling.plugin import hookimpl - - -@hookimpl -def hatch_register_template(): - return DefaultTemplate