Skip to content

fix: various plugin registry improvements - #86

Open
jlumpe wants to merge 13 commits into
snakemake:mainfrom
jlumpe:registry-improvements
Open

fix: various plugin registry improvements#86
jlumpe wants to merge 13 commits into
snakemake:mainfrom
jlumpe:registry-improvements

Conversation

@jlumpe

@jlumpe jlumpe commented Nov 29, 2025

Copy link
Copy Markdown
  • Changes to PluginRegsitryBase:
    • Improved docstrings.
    • Moved abstract methods to the top to make it clearer what needs to be implemented.
    • Removed type annotation of __new__() - previous annotation allowed for returning an instance of a different registry class as long as the type parameter was consistent. This may have been added in the first place due to the return cls._instance statement, which type checkers might not be able to resolve correctly. Fixed instead by adding # type: ignore to return statement, which should cause type checkers to infer standard behavior of returning an instance of cls.
    • Call collect_plugins() in __new__() rather than __init__(): previously _instance was assigned prior to collect_plugins() being called, so if there were any errors the singleton instance would be left in a broken or partially initialized state. This does not catch errors in __init__() if any subclasses define it, from a quick survey of the actual plugin packages I don't think any do.
    • register_plugin() method: Fixed issue where name conflicts were checked using the full module name with prefix instead of the registry name. Changed argument name to be a bit more clear.
    • get_plugin() method: Fix error message to properly display expected package name using get_plugin_package_name(). Also corrected return type annotation.
    • validate_plugin() method: fix TypeError when checking a type attribute where the value is not actually a type.
  • Added tests of registry using example registry class.
  • Add tests/ to Pixi format task
  • Fix pytest collection warnings on TestPlugin and TestSettings classes.

Summary by CodeRabbit

  • Bug Fixes

    • Clearer error messages for missing/invalid plugins and stricter validation of plugin attributes and types.
  • New Features

    • Registry now initializes discovery on first creation for consistent singleton behavior.
    • Plugin discovery normalizes registration names (underscores → dashes) for consistent identifiers.
    • Added an example plugin and registry to demonstrate loading, metadata, and optional settings handling.
  • Tests

    • Added comprehensive tests covering discovery, loading, validation, error messages, and registry behavior; prevented accidental test-class collection.

@coderabbitai

coderabbitai Bot commented Nov 29, 2025

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Moves singleton initialization into PluginRegistryBase.new, makes module_prefix/load_plugin/expected_attributes explicit abstract members, changes discovery to derive plugin registration names from module names, tightens attribute validation, and adds example plugin fixtures plus tests for discovery and error paths.

Changes

Cohort / File(s) Summary
Core Registry Refactor
src/snakemake_interface_common/plugin_registry/__init__.py
Implements singleton allocation in __new__; declares abstract module_prefix, load_plugin(name, module), and expected_attributes(); collect_plugins() derives registration names by stripping module_prefix and converting _- and calls register_plugin(name, module); register_plugin parameter renamed to module; get_plugin() returns generic TPlugin and composes package-aware error messages; tightened class-type attribute validation; removed unused Type import and updated docstrings.
Example plugin & registry (tests)
tests/example_plugin.py
Adds ExamplePlugin dataclass and ExamplePluginRegistry implementing module_prefix, load_plugin(name, module), and expected_attributes() to exercise registry behavior in tests.
Test fixtures — valid & invalid plugins
tests/plugins/valid/.../snakemake_example_plugin_valid_1/__init__.py, tests/plugins/valid/.../snakemake_example_plugin_valid_2/__init__.py, tests/plugins/invalid-class/.../snakemake_example_plugin_invalid_class/__init__.py, tests/plugins/invalid-object/.../snakemake_example_plugin_invalid_object/__init__.py, tests/plugins/missing-attr/.../snakemake_example_plugin_missing_attr/__init__.py
Adds fixture plugin packages: two valid plugins (one with a Settings subclass), one missing required attribute, one with incorrectly-typed attribute, and one with an invalid settings class.
Registry tests
tests/test_registry.py
New pytest module validating registry API, singleton semantics (resetting _instance in an autouse fixture), discovery loading, per-plugin metadata, and error paths asserting InvalidPluginException messages for missing/invalid attributes.
Test harness adjustments
tests/__init__.py, tests/tests.py
tests/__init__.py adds package comment; tests/tests.py reformats imports and sets __test__ = False on helper classes to avoid test collection.

Sequence Diagram

sequenceDiagram
    participant User as User Code
    participant Registry as PluginRegistry
    participant Discoverer as Module Discoverer
    participant Module as Plugin Module
    participant Validator as Validator
    participant Plugin as Plugin Instance

    User->>Registry: instantiate (triggers __new__)
    activate Registry
    Registry->>Discoverer: collect_plugins() / scan sys.path for module_prefix
    activate Discoverer
    Discoverer->>Module: import module
    Module-->>Discoverer: module
    Discoverer-->>Registry: module, derived name
    deactivate Discoverer

    loop each discovered module
        Registry->>Validator: validate_plugin(name, module)
        activate Validator
        Validator->>Registry: expected_attributes()
        Registry-->>Validator: attribute schema
        Validator->>Module: read attributes & types
        Module-->>Validator: attribute values
        alt valid
            Validator-->>Registry: valid
            Registry->>Registry: register_plugin(name, module)
            Registry->>Registry: load_plugin(name, module)
            Registry-->>Plugin: plugin instance
        else invalid
            Validator-->>Registry: raise InvalidPluginException
        end
        deactivate Validator
    end

    User->>Registry: get_plugin(plugin_name)
    Registry-->>User: TPlugin or InvalidPluginException
    deactivate Registry
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly identifies plugin registry improvements, which are the main focus of the changes. It is concise and related to the pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/example_plugin.py (1)

49-58: Consider defensive handling of module.__file__.

Line 56 assumes module.__file__ is not None, which could raise a TypeError if the module is a built-in or namespace package. While test fixtures are file-based, adding a defensive check or assertion would improve robustness.

Consider adding a guard:

     def load_plugin(self, name: str, module: ModuleType) -> ExamplePlugin:
         settings_cls = getattr(module, "ExampleSettings", None)
         string_attr = module.example_string
 
+        if module.__file__ is None:
+            raise ValueError(f"Plugin module {name} has no __file__ attribute")
+
         return ExamplePlugin(
             _name=name,
             _settings_cls=settings_cls,
             file=Path(module.__file__),
             string_attr=string_attr,
         )
📜 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 c205193.

⛔ 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/missing-attr/snakemake_example_plugin_missing_attr/__init__.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py
  • tests/__init__.py
  • tests/example_plugin.py
  • src/snakemake_interface_common/plugin_registry/__init__.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py
  • tests/test_registry.py
🧬 Code graph analysis (5)
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)
tests/example_plugin.py (3)
src/snakemake_interface_common/plugin_registry/__init__.py (1)
  • PluginRegistryBase (23-206)
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)
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/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)
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-72)
  • new (39-43)
  • name (25-26)
  • settings_cls (33-34)
src/snakemake_interface_common/plugin_registry/__init__.py (4)
  • get_plugin_type (113-125)
  • is_installed (79-81)
  • get_plugin (83-98)
  • get_registered_plugins (75-77)
🔇 Additional comments (23)
tests/__init__.py (1)

1-1: LGTM!

Clear comment explaining the purpose of this __init__.py file.

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

23-42: Excellent documentation.

The docstring clearly explains the singleton pattern, naming conventions, discovery mechanism, and includes a concrete example. The note about editable installs is a helpful caveat.


44-56: LGTM!

The singleton pattern using __new__ with an initialization guard in __init__ is well-implemented. Each subclass will maintain its own _instance class attribute due to Python's attribute assignment semantics.


58-72: LGTM!

Good organization moving abstract methods to the top. The contract for subclasses is clear and well-documented.


83-98: LGTM!

Good improvement to the error message using the pip-installable package name, making it actionable for users.


127-139: LGTM!

The plugin discovery logic correctly filters by prefix, transforms underscores to dashes in plugin names, and delegates to register_plugin for validation and registration.


141-206: LGTM!

The register_plugin and validate_plugin methods provide thorough validation with clear error messages. The handling of optional vs required attributes and class vs object types is correct.


11-11: Minimum Python version is already properly set to 3.11+.

Verification confirms that snakemake-interface-common requires Python >=3.11 in its package metadata (both PyPI and Bioconda). The Self import from typing on line 11 is therefore appropriately supported and no additional configuration changes are needed.

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

1-3: LGTM!

Good test fixture for validating plugin discovery with only required attributes (no optional settings class).

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

1-1: LGTM!

Appropriate negative test fixture for validating error handling when required plugin attributes are missing.

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

1-7: LGTM!

Appropriate negative test fixture for validating error handling when a plugin's class attribute doesn't inherit from the expected base class.

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

1-3: LGTM! Test fixture correctly implements invalid scenario.

This test fixture appropriately provides an integer value for example_string to enable validation of type-checking error handling in the registry.

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

1-6: LGTM! Valid test fixture structure.

The module correctly defines the required example_string attribute with the expected string type.


9-10: LGTM! Minimal settings class is appropriate for testing.

The empty ExampleSettings class correctly inherits from SettingsBase and is sufficient to test the optional settings class attribute validation. The TODO can remain as this fixture validates that minimal settings classes are properly recognized.

tests/test_registry.py (5)

1-14: LGTM! Clean test setup.

Imports and test directory setup are correct.


16-28: LGTM! Comprehensive basic functionality tests.

The test correctly validates registry type inference, singleton behavior, and plugin-not-found error handling.


30-59: LGTM! Thorough plugin discovery and validation tests.

The test comprehensively validates plugin discovery, instantiation, and attribute checking for both plugins with and without optional settings classes.


61-72: LGTM! Proper validation of missing attribute handling.

The test correctly verifies that the registry raises InvalidPluginException with an appropriate error message when a required attribute is missing.


74-98: LGTM! Comprehensive type validation tests.

Both test_invalid_object and test_invalid_class properly validate that the registry rejects plugins with incorrect attribute types and generates informative error messages.

tests/example_plugin.py (4)

1-16: LGTM! Clean import structure.

All necessary imports are present and correctly organized.


17-35: LGTM! ExamplePlugin correctly implements PluginBase.

The dataclass structure and property implementations appropriately fulfill the PluginBase contract for testing purposes.


38-48: LGTM! Test registry setup is correct.

The new() classmethod appropriately bypasses singleton behavior for isolated testing, and module_prefix correctly identifies test plugins.


60-72: LGTM! Attribute specifications are correct.

The expected_attributes() implementation properly defines the validation rules for test plugins, specifying ExampleSettings as an optional class attribute and example_string as a required object attribute.

@jlumpe
jlumpe force-pushed the registry-improvements branch from c205193 to 1ec5805 Compare March 26, 2026 21:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
src/snakemake_interface_common/plugin_registry/__init__.py (1)

135-137: Preserve the package-name filter before importing.

is_valid_plugin_package_name() is never consulted anywhere in this class, so any subclass override is currently dead and rejected packages still get imported. If that hook is meant to remain part of the extension surface, gate on it here before import_module().

Possible fix
             name = moduleinfo.name.removeprefix(self.module_prefix).replace("_", "-")
+            if not self.is_valid_plugin_package_name(name):
+                continue
             module = importlib.import_module(moduleinfo.name)
             self.register_plugin(name, module)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/snakemake_interface_common/plugin_registry/__init__.py` around lines 135
- 137, The code currently imports every discovered module before checking the
package-name filter, so override hooks like is_valid_plugin_package_name are
never used; modify the loop that handles moduleinfo (using moduleinfo.name and
self.module_prefix) to compute the plugin name (using
moduleinfo.name.removeprefix(self.module_prefix).replace("_", "-")) and call
self.is_valid_plugin_package_name(name) before calling
importlib.import_module(moduleinfo.name); only call importlib.import_module and
then self.register_plugin(name, module) if the name passes the filter, ensuring
subclasses can override is_valid_plugin_package_name to reject packages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/snakemake_interface_common/plugin_registry/__init__.py`:
- Around line 47-54: The singleton currently calls collect_plugins() from
__new__, which triggers plugin discovery before subclass __init__ runs and
causes __init__ to be re-entered on cached singletons; move the one-time
collection out of __new__ and into a guarded post-init path instead: remove
collect_plugins() from __new__, add a boolean flag like _plugins_collected on
the class/instance, and call collect_plugins() from __init__ only when not
already collected (or expose an explicit initialize_plugins() method and call it
once); ensure you keep the singleton behavior using cls._instance but prevent
re-running collect_plugins and avoid changing subclass init semantics (refer to
methods __new__, __init__, collect_plugins, and attribute
_instance/_plugins_collected when applying the change).

---

Nitpick comments:
In `@src/snakemake_interface_common/plugin_registry/__init__.py`:
- Around line 135-137: The code currently imports every discovered module before
checking the package-name filter, so override hooks like
is_valid_plugin_package_name are never used; modify the loop that handles
moduleinfo (using moduleinfo.name and self.module_prefix) to compute the plugin
name (using moduleinfo.name.removeprefix(self.module_prefix).replace("_", "-"))
and call self.is_valid_plugin_package_name(name) before calling
importlib.import_module(moduleinfo.name); only call importlib.import_module and
then self.register_plugin(name, module) if the name passes the filter, ensuring
subclasses can override is_valid_plugin_package_name to reject packages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2bc90299-57ba-40a7-a90d-758d8ea1c93d

📥 Commits

Reviewing files that changed from the base of the PR and between c205193 and 1ec5805.

⛔ Files ignored due to path filters (1)
  • pyproject.toml is excluded by !pyproject.toml
📒 Files selected for processing (10)
  • src/snakemake_interface_common/plugin_registry/__init__.py
  • tests/__init__.py
  • tests/example_plugin.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py
  • tests/test_registry.py
  • tests/tests.py
✅ Files skipped from review due to trivial changes (6)
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/init.py
  • tests/init.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/init.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/init.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/init.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_registry.py

Comment thread src/snakemake_interface_common/plugin_registry/__init__.py
@jlumpe
jlumpe force-pushed the registry-improvements branch from f5ee7ca to 9a6339f Compare March 26, 2026 22:56
@jlumpe
jlumpe force-pushed the registry-improvements branch from 9a6339f to 29baf79 Compare March 26, 2026 22:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/test_registry.py (2)

26-27: Assert the error message content to cover the get_plugin() message fix.

This currently validates only exception type. Add a message assertion so the PR’s package-name error-message correction is explicitly tested.

Proposed assertion update
-    with pytest.raises(InvalidPluginException):
+    with pytest.raises(InvalidPluginException) as exc_info:
         registry.get_plugin("foo")
+    assert ExamplePluginRegistry.get_plugin_package_name() in str(exc_info.value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_registry.py` around lines 26 - 27, The test currently only checks
that registry.get_plugin("foo") raises InvalidPluginException; update the
pytest.raises context to also assert the exception message contains the
corrected package-name text by capturing the exception (e.g., with "with
pytest.raises(InvalidPluginException) as excinfo: registry.get_plugin('foo')")
and then assert the message (e.g., assert "expected package-name error text" in
str(excinfo.value)). Reference the existing test call to registry.get_plugin and
the InvalidPluginException class when adding the message assertion.

16-22: Isolate singleton state between tests to prevent order-dependent behavior.

Line 18 initializes ExamplePluginRegistry’s singleton cache, which can leak across tests/modules. Consider resetting _instance in an autouse fixture to keep tests hermetic.

Proposed test-isolation patch
+@pytest.fixture(autouse=True)
+def _reset_example_registry_singleton():
+    ExamplePluginRegistry._instance = None
+    yield
+    ExamplePluginRegistry._instance = None
+
 def test_basic():
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_registry.py` around lines 16 - 22, The test leaks the
ExamplePluginRegistry singleton between tests; modify the test suite to reset
ExamplePluginRegistry._instance before each test using an autouse fixture so
tests are hermetic: add an autouse pytest fixture (e.g., in
tests/test_registry.py or conftest.py) that saves any existing
ExamplePluginRegistry._instance, sets ExamplePluginRegistry._instance = None (or
the desired fresh state) before each test run and restores it after, ensuring
test_basic and other tests always get a fresh ExamplePluginRegistry instance and
do not depend on test order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_registry.py`:
- Around line 26-27: The test currently only checks that
registry.get_plugin("foo") raises InvalidPluginException; update the
pytest.raises context to also assert the exception message contains the
corrected package-name text by capturing the exception (e.g., with "with
pytest.raises(InvalidPluginException) as excinfo: registry.get_plugin('foo')")
and then assert the message (e.g., assert "expected package-name error text" in
str(excinfo.value)). Reference the existing test call to registry.get_plugin and
the InvalidPluginException class when adding the message assertion.
- Around line 16-22: The test leaks the ExamplePluginRegistry singleton between
tests; modify the test suite to reset ExamplePluginRegistry._instance before
each test using an autouse fixture so tests are hermetic: add an autouse pytest
fixture (e.g., in tests/test_registry.py or conftest.py) that saves any existing
ExamplePluginRegistry._instance, sets ExamplePluginRegistry._instance = None (or
the desired fresh state) before each test run and restores it after, ensuring
test_basic and other tests always get a fresh ExamplePluginRegistry instance and
do not depend on test order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04de27b4-55be-4d1f-ba65-3805c7b4a5c2

📥 Commits

Reviewing files that changed from the base of the PR and between f5ee7ca and 9a6339f.

⛔ Files ignored due to path filters (1)
  • pyproject.toml is excluded by !pyproject.toml
📒 Files selected for processing (10)
  • src/snakemake_interface_common/plugin_registry/__init__.py
  • tests/__init__.py
  • tests/example_plugin.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py
  • tests/test_registry.py
  • tests/tests.py
✅ Files skipped from review due to trivial changes (7)
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/init.py
  • tests/init.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/init.py
  • tests/tests.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/init.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/init.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/init.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/example_plugin.py
  • src/snakemake_interface_common/plugin_registry/init.py

@jlumpe
jlumpe force-pushed the registry-improvements branch from 90c5aad to 82c33a8 Compare March 26, 2026 23:11
@jlumpe

jlumpe commented Mar 26, 2026

Copy link
Copy Markdown
Author

Followed CodeRabbit's suggestion about properly resetting the singleton of ExamplePluginRegistry between tests.

@cademirch
cademirch self-requested a review March 27, 2026 18:22
@cademirch cademirch changed the title Plugin registry improvements fix: Plugin registry improvements Mar 27, 2026
Comment thread src/snakemake_interface_common/plugin_registry/__init__.py
Comment thread src/snakemake_interface_common/plugin_registry/__init__.py
Co-authored-by: Cade Mirchandani <cmirchan@ucsc.edu>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/snakemake_interface_common/plugin_registry/__init__.py`:
- Around line 157-158: The line containing "name =
name.removeprefix(self.module_prefix).replace("_", "-")" is under-indented and
causes an IndentationError on import; adjust its indentation to match the
surrounding method body (same level as the following "if name in self.plugins:"
line) so it is inside the method scope, ensuring the assignment and the
subsequent check against self.plugins are at the same indentation level.

In `@tests/test_registry.py`:
- Line 48: The test declares plugins using the new-style annotation dict[str,
ExamplePlugin], which is incompatible with Python 3.8; change the type
annotation to use typing.Dict (e.g., Dict[str, ExamplePlugin]) or add from
__future__ import annotations at the top of the file to defer evaluation—update
the declaration of plugins in tests/test_registry.py (the plugins variable and
ExamplePlugin type reference) accordingly so it matches the project's Python 3.8
compatibility.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a52621e7-efbb-4197-b194-d6feb1eeea8c

📥 Commits

Reviewing files that changed from the base of the PR and between 9a6339f and 6074628.

⛔ Files ignored due to path filters (1)
  • pyproject.toml is excluded by !pyproject.toml
📒 Files selected for processing (10)
  • src/snakemake_interface_common/plugin_registry/__init__.py
  • tests/__init__.py
  • tests/example_plugin.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/__init__.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/__init__.py
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/__init__.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/__init__.py
  • tests/test_registry.py
  • tests/tests.py
✅ Files skipped from review due to trivial changes (7)
  • tests/plugins/missing-attr/snakemake_example_plugin_missing_attr/init.py
  • tests/init.py
  • tests/plugins/invalid-object/snakemake_example_plugin_invalid_object/init.py
  • tests/plugins/invalid-class/snakemake_example_plugin_invalid_class/init.py
  • tests/tests.py
  • tests/plugins/valid/snakemake_example_plugin_valid_1/init.py
  • tests/plugins/valid/snakemake_example_plugin_valid_2/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/example_plugin.py

Comment on lines 157 to 158
name = name.removeprefix(self.module_prefix).replace("_", "-")
if name in self.plugins:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical indentation error will cause IndentationError on import.

Line 157 has only a single space of indentation instead of matching the method body indentation (8 spaces). This is a syntax error that will prevent the module from loading.

🐛 Proposed fix for indentation
-        """
- name = name.removeprefix(self.module_prefix).replace("_", "-")
+        """
+        name = name.removeprefix(self.module_prefix).replace("_", "-")
         if name in self.plugins:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/snakemake_interface_common/plugin_registry/__init__.py` around lines 157
- 158, The line containing "name =
name.removeprefix(self.module_prefix).replace("_", "-")" is under-indented and
causes an IndentationError on import; adjust its indentation to match the
surrounding method body (same level as the following "if name in self.plugins:"
line) so it is inside the method scope, ensuring the assignment and the
subsequent check against self.plugins are at the same indentation level.

Comment thread tests/test_registry.py
registry = ExamplePluginRegistry()

expected_plugins = {"valid-1", "valid-2"}
plugins: dict[str, ExamplePlugin] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check minimum Python version requirement in project configuration
echo "=== Checking pyproject.toml for Python version ==="
cat pyproject.toml 2>/dev/null | grep -A5 -i "python" | head -20

echo ""
echo "=== Checking setup.py/setup.cfg for Python version ==="
cat setup.py 2>/dev/null | grep -i "python"
cat setup.cfg 2>/dev/null | grep -i "python"

echo ""
echo "=== Checking for other uses of lowercase generics in tests ==="
rg -n ":\s*(list|dict|set|tuple)\[" tests/ --type py

Repository: snakemake/snakemake-interface-common

Length of output: 895


🏁 Script executed:

#!/bin/bash
echo "=== Checking main module for Dict import from typing ==="
find snakemake_interface_common -name "*.py" -type f | head -10 | xargs grep -l "from typing import" | head -5 | xargs grep -n "Dict" | head -20

echo ""
echo "=== Checking test_registry.py imports ==="
head -30 tests/test_registry.py

echo ""
echo "=== Checking for Dict usage in main module ==="
rg -n "Dict\[" snakemake_interface_common/ --type py | head -20

echo ""
echo "=== Checking lowercase generic usage across main module ==="
rg -n ":\s*(list|dict|set|tuple)\[" snakemake_interface_common/ --type py | head -20

Repository: snakemake/snakemake-interface-common

Length of output: 1331


🏁 Script executed:

#!/bin/bash
echo "=== Repository structure ==="
ls -la

echo ""
echo "=== Find all Python source directories ==="
find . -maxdepth 2 -type d -name "*snakemake*" | head -10

echo ""
echo "=== Check for main source files ==="
find . -maxdepth 2 -name "*.py" -type f | head -20

echo ""
echo "=== Check for Dict usage anywhere in project ==="
rg -n "Dict\[" --type py | head -30

echo ""
echo "=== Check lowercase generic usage in entire project ==="
rg -n ":\s*(list|dict|set|tuple)\[" --type py | head -30

Repository: snakemake/snakemake-interface-common

Length of output: 2176


🏁 Script executed:

#!/bin/bash
echo "=== Full imports section of test_registry.py ==="
head -20 tests/test_registry.py

echo ""
echo "=== Check for __future__ import ==="
rg -n "from __future__" tests/test_registry.py

echo ""
echo "=== Check typing imports in test_registry.py ==="
rg -n "from typing import\|import typing" tests/test_registry.py

echo ""
echo "=== Verify Dict usage in main module ==="
rg -n "Dict\[" src/snakemake_interface_common/ --type py

Repository: snakemake/snakemake-interface-common

Length of output: 1783


Use Dict[str, ExamplePlugin] for Python 3.8 compatibility.

dict[str, ExamplePlugin] requires Python 3.9+, but the project declares requires-python = ">=3.8" in pyproject.toml and the main module consistently uses Dict from typing. Either use Dict[str, ExamplePlugin] or add from __future__ import annotations at the top of the file to maintain compatibility with Python 3.8.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_registry.py` at line 48, The test declares plugins using the
new-style annotation dict[str, ExamplePlugin], which is incompatible with Python
3.8; change the type annotation to use typing.Dict (e.g., Dict[str,
ExamplePlugin]) or add from __future__ import annotations at the top of the file
to defer evaluation—update the declaration of plugins in tests/test_registry.py
(the plugins variable and ExamplePlugin type reference) accordingly so it
matches the project's Python 3.8 compatibility.

@jlumpe

jlumpe commented Mar 31, 2026

Copy link
Copy Markdown
Author

@cademirch applied your suggestion.

One last potential change - I'm wondering if it might be better to use a metaclass to implement the singleton pattern rather than using __new__(). Generally I try to avoid those if possible, but here I think it gives a cleaner design where initialization is still done in an __init__() method that subclasses can override if they wish, but __init__() is not called if the singleton instance already exists, and you avoid the problem of being stuck with a broken instance if initialization fails.

I have a PR for it here.

Comment thread src/snakemake_interface_common/plugin_registry/__init__.py Outdated
@johanneskoester johanneskoester changed the title fix: Plugin registry improvements fix: various plugin registry improvements Sep 7, 2026
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.

3 participants