- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 348
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.