Skip to content

Conversation

@jlumpe
Copy link

@jlumpe jlumpe commented Nov 29, 2025

Note: this is built on top of #86, that should be merged first.

This adds the ability to register plugins through the entry point mechanism.

Adds a new entry_point property to PluginRegistryBase. Child classes may override it to return a string, which is the entry point group name. For example, if the logger plugin registry has entry_point value "loggers", the following pyproject.toml could be used to register a logger plugin:

[project.entry-points.'snakemake.loggers']
my-logger = "my_logger.submodule"

The plugin's name is my-logger and its classes or other required values will be imported from my_logger.submodule (a standard dotted module name that can be nested to any level).

Summary by CodeRabbit

  • Improvements
    • Enhanced plugin discovery with entry-point-based detection for more flexible plugin management.
    • Improved error handling and validation with clearer error messages when plugins are missing or misconfigured.
    • Strengthened plugin registry with better singleton management and attribute validation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 29, 2025

📝 Walkthrough

Walkthrough

The changes refactor the plugin registry system to implement a singleton pattern with dual discovery mechanisms—one by package name and another by entry point—while introducing abstract properties and methods for concrete implementations. Test infrastructure includes example plugins and comprehensive test scenarios covering valid and invalid plugin configurations.

Changes

Cohort / File(s) Summary
Plugin Registry Core
src/snakemake_interface_common/plugin_registry/__init__.py
Refactored to use singleton pattern with __new__ returning Self and storing _instance. Added abstract properties module_prefix and entry_point (optional), abstract methods load_plugin and expected_attributes. Introduced _collect_plugins_by_name and _collect_plugins_by_entry_point for dual discovery paths. Updated get_plugin return type to generic TPlugin, enhanced error reporting with package names, and improved validation flow. Added entry point support via importlib.metadata.
Test Infrastructure
tests/__init__.py, tests/example_plugin.py, tests/test_registry.py
Added package marker in tests/__init__.py. Introduced ExamplePlugin dataclass and ExamplePluginRegistry concrete implementation in example_plugin.py demonstrating abstract property/method overrides. Added comprehensive test suite in test_registry.py covering singleton behavior, plugin discovery (by name and entry point), validation, error handling, and edge cases with monkeypatched entry points.
Valid Test Plugins
tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py, tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py
Created valid plugin modules with example_string (required) and ExampleSettings(SettingsBase) (optional) attributes to validate correct plugin structure.
Invalid Test Plugins
tests/plugins/invalid-class/__init__.py, tests/plugins/invalid-object/__init__.py, tests/plugins/missing-attr/__init__.py
Created fixture plugins to test error scenarios: invalid class without required base, invalid object type for example_string, and missing required attribute, respectively.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Registry as PluginRegistryBase
    participant ByName as Discovery:<br/>By Name
    participant ByEntryPoint as Discovery:<br/>By Entry Point
    participant Module as Module Loader
    participant Validator as Validator

    Client->>Registry: collect_plugins()
    activate Registry
    
    rect rgba(150, 180, 200, 0.2)
    Note over Registry,Module: Name-based Discovery
    Registry->>ByName: _collect_plugins_by_name()
    activate ByName
    ByName->>Module: import module<br/>(module_prefix + "*")
    Module->>ByName: module loaded
    ByName->>Validator: validate_plugin(module)
    Validator->>ByName: ✓ valid
    ByName->>Registry: plugins registered
    deactivate ByName
    end

    rect rgba(150, 200, 150, 0.2)
    Note over Registry,Validator: Entry Point Discovery
    Registry->>ByEntryPoint: _collect_plugins_by_entry_point()
    activate ByEntryPoint
    ByEntryPoint->>Module: entry_points(entry_point)
    Module->>ByEntryPoint: entry point objects
    ByEntryPoint->>Module: load entry point → module
    Module->>ByEntryPoint: module instance
    ByEntryPoint->>Validator: validate_plugin(module)
    Validator->>ByEntryPoint: ✓ valid
    ByEntryPoint->>Registry: plugins registered
    deactivate ByEntryPoint
    end

    deactivate Registry
    Client->>Registry: get_plugin(name)
    activate Registry
    Registry->>Client: return TPlugin instance
    deactivate Registry
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Singleton pattern and __new__ override: Verify thread-safety and correct instance management; ensure __init__ skips reinitialization when plugins are already loaded.
  • Discovery orchestration: Confirm both name-based and entry-point-based mechanisms integrate correctly without duplicate registrations or missing plugins.
  • Validation flow: Review validate_plugin logic against expected_attributes mapping, error message clarity, and exception handling (esp. InvalidPluginException).
  • Generic return type for get_plugin: Ensure type narrowing and return type consistency throughout.
  • Test coverage: Verify monkeypatched entry points, import error handling, and edge cases (missing attributes, invalid types, malformed entry points).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.76% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Register plugins through entry points' directly and accurately summarizes the main change—adding entry point-based plugin registration support.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/snakemake_interface_common/plugin_registry/__init__.py (2)

195-200: Consider including more context in the import error message.

The error message says "unable to import {ep.value}" but doesn't include the original exception message. Since the exception is chained (from exc), the full traceback will be available, but the InvalidPluginException message itself could be more informative.

             try:
                 module = ep.load()
             except ImportError as exc:
                 raise InvalidPluginException(
-                    ep.name, f"unable to import {ep.value}"
+                    ep.name, f"unable to import {ep.value}: {exc}"
                 ) from exc

191-193: Silent duplicate skip prioritizes name-based discovery.

Entry point plugins are silently skipped if a name-based plugin already exists. This is consistent with the documented behavior (name-based runs first), but users might not realize their entry point is being ignored. Consider logging a debug message when skipping duplicates for easier troubleshooting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3449f and 1147efc.

⛔ Files ignored due to path filters (1)
  • pyproject.toml is excluded by !pyproject.toml
📒 Files selected for processing (9)
  • src/snakemake_interface_common/plugin_registry/__init__.py (5 hunks)
  • tests/__init__.py (1 hunks)
  • tests/example_plugin.py (1 hunks)
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py (1 hunks)
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py (1 hunks)
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/__init__.py (1 hunks)
  • tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py (1 hunks)
  • tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py (1 hunks)
  • tests/test_registry.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

⚙️ CodeRabbit configuration file

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

Files:

  • tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py
  • tests/__init__.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/__init__.py
  • src/snakemake_interface_common/plugin_registry/__init__.py
  • tests/test_registry.py
  • tests/example_plugin.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py
🧬 Code graph analysis (5)
tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py (1)
tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py (1)
  • ExampleSettings (9-10)
src/snakemake_interface_common/plugin_registry/__init__.py (3)
src/snakemake_interface_common/plugin_registry/plugin.py (2)
  • name (85-85)
  • register_cli_args (134-198)
src/snakemake_interface_common/plugin_registry/attribute_types.py (1)
  • AttributeType (17-31)
src/snakemake_interface_common/exceptions.py (1)
  • InvalidPluginException (77-79)
tests/test_registry.py (4)
src/snakemake_interface_common/plugin_registry/plugin.py (1)
  • SettingsBase (41-50)
src/snakemake_interface_common/exceptions.py (1)
  • InvalidPluginException (77-79)
tests/example_plugin.py (5)
  • ExamplePlugin (18-34)
  • ExamplePluginRegistry (37-76)
  • new (39-43)
  • name (25-26)
  • settings_cls (33-34)
src/snakemake_interface_common/plugin_registry/__init__.py (3)
  • get_plugin_type (147-159)
  • get_plugin (117-132)
  • get_registered_plugins (109-111)
tests/example_plugin.py (3)
src/snakemake_interface_common/plugin_registry/__init__.py (5)
  • PluginRegistryBase (34-269)
  • module_prefix (87-88)
  • entry_point (91-97)
  • load_plugin (100-101)
  • expected_attributes (104-105)
src/snakemake_interface_common/plugin_registry/plugin.py (2)
  • PluginBase (82-360)
  • SettingsBase (41-50)
src/snakemake_interface_common/plugin_registry/attribute_types.py (3)
  • AttributeType (17-31)
  • AttributeMode (6-8)
  • AttributeKind (11-13)
tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py (2)
src/snakemake_interface_common/plugin_registry/plugin.py (1)
  • SettingsBase (41-50)
tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py (1)
  • ExampleSettings (6-7)
🔇 Additional comments (23)
tests/__init__.py (1)

1-1: LGTM!

Standard Python package initialization with a clear comment explaining its purpose.

tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py (1)

1-3: LGTM!

Valid test fixture that correctly provides the required example_string attribute without an optional ExampleSettings class, enabling testing of optional attribute handling.

tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/__init__.py (1)

1-1: LGTM!

Correct test fixture that intentionally omits the required example_string attribute to validate error handling for missing attributes.

tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py (1)

1-7: LGTM!

Correct test fixture where ExampleSettings intentionally does not inherit from SettingsBase to validate class inheritance checking.

tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py (1)

1-3: LGTM!

Correct test fixture where example_string is intentionally set to an integer instead of a string to validate type checking.

tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py (1)

1-10: LGTM!

Valid test fixture that correctly provides both the required example_string attribute and an optional ExampleSettings class that properly inherits from SettingsBase.

tests/test_registry.py (7)

19-33: LGTM!

Well-structured helper function that correctly monkeypatches the entry points system for testing. The implementation properly wraps the entry points and maintains the select interface.


36-47: LGTM!

Comprehensive test of basic registry functionality including singleton behavior, plugin type derivation, and error handling for missing plugins.


50-99: LGTM!

Thorough test of plugin discovery mechanisms covering both module-name-based and entry-point-based discovery. Correctly validates plugin attributes, settings class inheritance, and filtering of non-matching entry points.


101-111: LGTM!

Correct test for missing required attribute validation. Properly verifies that an InvalidPluginException is raised with an informative error message.


114-124: LGTM!

Correct test for object type validation. Properly verifies that an InvalidPluginException is raised when an attribute has an incorrect type.


127-138: LGTM!

Correct test for class inheritance validation. Properly verifies that an InvalidPluginException is raised when a settings class doesn't inherit from the required base class.


141-178: LGTM!

Comprehensive test of entry point validation covering both syntax errors (colon in module path) and import failures. Properly uses context managers for test isolation.

tests/example_plugin.py (5)

17-34: LGTM!

Well-structured test plugin class that correctly implements the PluginBase interface with appropriate property accessors for private fields and proper dataclass usage.


38-43: LGTM!

The new() method correctly bypasses the singleton pattern for test isolation by using object.__new__(cls) and explicitly calling __init__().


45-51: LGTM!

Properties correctly implement the abstract registry interface with appropriate prefix and entry point values for testing.


53-62: LGTM!

The load_plugin method correctly constructs the plugin instance. Direct attribute access on line 55 is safe because this method is called after validation, which ensures required attributes exist.


64-76: LGTM!

The expected_attributes method correctly defines validation constraints, specifying that ExampleSettings is an optional class attribute that must subclass SettingsBase, and example_string is a required string object.

src/snakemake_interface_common/plugin_registry/__init__.py (5)

69-81: Singleton pattern implementation looks correct.

The use of _instance at class level combined with the hasattr(self, "plugins") check in __init__ properly ensures single initialization. The idempotent __init__ is a good pattern for singletons.


85-106: Well-designed abstract interface.

The abstract properties and methods are clearly defined. Making entry_point optional with a None default maintains backward compatibility for registries that don't use entry point discovery.


117-132: Improved error messaging in get_plugin.

The error message now includes the pip-installable package name, making it easier for users to know what to install. This is a good UX improvement.


204-269: Registration and validation logic is well-implemented.

The register_plugin and validate_plugin methods properly handle:

  • Duplicate prevention
  • Optional vs required attributes
  • Class vs instance type checking with clear error messages

The docstrings are thorough with proper parameter and exception documentation.


228-229: I'm unable to complete the verification due to persistent repository access failures. Since I cannot execute shell scripts against the codebase, I cannot definitively verify whether is_valid_plugin_package_name is used elsewhere, overridden by subclasses, or if it's truly dead code.

Based on the original review comment context alone:

  • The method always returns True (stub implementation)
  • It's not called within the shown file
  • Its purpose (validating plugin package names) is not being performed

However, without accessing the full codebase, I cannot confirm:

  • Whether it's called by external code or subclasses
  • If it's part of a public API contract
  • Whether there are legitimate reasons for its current stub implementation

The original review comment cannot be fully verified due to technical constraints.

TypeVar,
Generic,
Optional,
Self,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Python version requirements in project configuration files
echo "=== Checking pyproject.toml ===" 
cat pyproject.toml 2>/dev/null | head -50

echo -e "\n=== Checking setup.py ==="
cat setup.py 2>/dev/null | head -50

echo -e "\n=== Checking setup.cfg ==="
cat setup.cfg 2>/dev/null | head -50

Repository: snakemake/snakemake-interface-common

Length of output: 216


🌐 Web query:

snakemake-interface-common Python version requirement pyproject.toml

💡 Result:

snakemake-interface-common requires Python >= 3.8. [1][2]

Sources:

  • Bioconda recipe for snakemake-interface-common (lists "depends python: >=3.8"). [1]
  • PyPI package page (wheels published for "py3"; source/binaries target Python 3). [2]

🌐 Web query:

snakemake-interface-common dependencies typing_extensions pypi

💡 Result:

Short answer: No — snakemake-interface-common on PyPI does not list typing_extensions as a dependency. See the PyPI project page and the Bioconda recipe (which lists its dependencies: argparse-dataclass, configargparse, packaging, python >=3.8). [1][2]

Sources:

  • PyPI: snakemake-interface-common project page. [1]
  • Bioconda recipe for snakemake-interface-common (dependency list). [2]

Self import breaks backward compatibility with Python 3.8–3.10.

The project requires Python >= 3.8, but Self (imported from typing on line 20) was only added in Python 3.11. Users on Python 3.8–3.10 will encounter an ImportError at runtime. Since typing_extensions is not listed as a project dependency, use either:

  • A conditional import: from typing_extensions import Self with a fallback for Python 3.11+, or
  • Add typing_extensions as a dependency and import from there for consistency across all Python versions.
🤖 Prompt for AI Agents
In src/snakemake_interface_common/plugin_registry/__init__.py around line 20,
importing Self from typing breaks compatibility with Python 3.8–3.10; replace it
with a compatibility import that uses typing_extensions when running on older
Pythons (or add typing_extensions as a dependency and import Self from there).
Update the top-level imports so the module imports Self from typing_extensions
for Python <3.11 (or falls back to typing for 3.11+), and add typing_extensions
to pyproject/requirements if you choose the dependency approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant