-
-
Notifications
You must be signed in to change notification settings - Fork 347
Claude based experiment - completely remove pluggy usage for entrypoints #2076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
RonnyPfannschmidt
wants to merge
15
commits into
pypa:master
Choose a base branch
from
RonnyPfannschmidt:ronny-with-claude/from-pluggy-to-entrypoints
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
7f6aaeb
Add PluginFinder with entrypoint-first plugin discovery
RonnyPfannschmidt 00d8160
Replace pluggy PluginManager with PluginFinder-based registry
RonnyPfannschmidt eb99b92
Make hookimpl lazy-load pluggy with deprecation warnings
RonnyPfannschmidt 419ee64
Remove all internal @hookimpl usage from codebase
RonnyPfannschmidt d79da76
Register all built-in plugins via direct entrypoints
RonnyPfannschmidt 78a4124
Add built-in plugin fallback for development/bootstrap
RonnyPfannschmidt 09e4e7c
Update plugin documentation for entrypoint-based system
RonnyPfannschmidt fd31ddb
Remove silent error handling for built-in plugins and fix linting
RonnyPfannschmidt 6ba995f
Use stdlib importlib.metadata instead of importlib_metadata
RonnyPfannschmidt 8f20b12
Load built-in plugins from pyproject.toml to avoid duplication
RonnyPfannschmidt e1e5309
Simplify hook name lookup by inlining the pattern
RonnyPfannschmidt 5ca391c
Fix pyproject.toml path calculation in plugin finder
RonnyPfannschmidt d578f9a
fix fmt with plain ruff
RonnyPfannschmidt c66c9f9
Remove diaper antipattern from pyproject.toml reading
RonnyPfannschmidt 530f575
Remove redundant FileNotFoundError handler
RonnyPfannschmidt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"permissions": { | ||
"allow": [ | ||
"Bash(hatch fmt:*)" | ||
], | ||
"deny": [], | ||
"ask": [] | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be committed, this is a settings file for Claude Code it looks like.