Skip to content

fix(plugins): discovery must not instantiate an imported base class - #4319

Open
warksit wants to merge 1 commit into
springfall2008:mainfrom
warksit:fix/plugin-discovery-base-class
Open

fix(plugins): discovery must not instantiate an imported base class#4319
warksit wants to merge 1 commit into
springfall2008:mainfrom
warksit:fix/plugin-discovery-base-class

Conversation

@warksit

@warksit warksit commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The bug

load_plugin()'s preferred detection strategy takes the first class whose name ends in Plugin:

for name, obj in inspect.getmembers(plugin_module, inspect.isclass):
    if name.endswith("Plugin"):
        plugin_instance = obj(self.base)
        break

inspect.getmembers() returns members alphabetically. Every plugin does from plugin_system import PredBatPlugin, so the abstract base class is in the module namespace and is itself a match. Any plugin whose class name sorts after PredBatPlugin therefore gets the base instantiated instead of itself.

When that happens the plugin looks completely healthy in the log:

Initialising plugin class (name-based): PredBatPlugin
Successfully loaded plugin: soc_keep_publish_plugin

…but PredBatPlugin.register_hooks() is a no-op, so no hooks are registered and the plugin silently does nothing. There is no error, no warning, and the entity it was supposed to publish simply goes stale.

Whether a plugin works depends on the luck of its class name. I hit this with a plugin class called SocKeepPublishPlugin; ColdWeatherPlugin and CurtailmentPlugin happen to sort before PredBatPlugin, which is the only reason they were unaffected.

The fix

Require the candidate class to be defined in the plugin file, not merely imported into it:

if name.endswith("Plugin") and obj.__module__ == plugin_module.__name__:

This also covers the general case of a plugin importing any other helper class ending in Plugin.

The two fallback strategies (the PREDBAT_PLUGIN marker and the initialize_plugin() function) are unchanged.

Compatibility

No API change and no behaviour change for any plugin that was already working — a plugin class defined in its own file still matches exactly as before. The only plugins affected are ones that were silently broken.

Testing

Added test_plugin_discovery_skips_imported_base_class to the existing tests/test_plugin_startup.py (no new files). It writes a temp plugin whose class name deliberately sorts after PredBatPlugin, runs real discovery over it, and asserts the correct class was instantiated.

Confirmed it fails without the fix:

ERROR: discovery instantiated PredBatPlugin, expected ZzSortsLastPlugin
**** ERROR: Test plugin_discovery FAILED

and passes with it. Registered in unit_test.py as plugin_discovery. Full unit_test.py --quick passes, pre-commit run clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WY52NvNyVdznsF4VcVG8Q8

inspect.getmembers() returns members ALPHABETICALLY, and the name-based
detection strategy takes the first class ending in 'Plugin'. Every plugin
does 'from plugin_system import PredBatPlugin', so the abstract base sits
in the module namespace — and any plugin class sorting after
'PredBatPlugin' gets the base instantiated instead of itself.

The plugin then logs as loaded successfully and silently does nothing: no
hooks registered, no behaviour. Whether a plugin works depends on the
luck of its class name. Hit in the wild with a class named
SocKeepPublishPlugin; ColdWeatherPlugin and CurtailmentPlugin sort before
the base, so they were unaffected.

Fix: require obj.__module__ == plugin_module.__name__, so only classes
actually DEFINED in the plugin file are candidates. The two fallback
strategies (PREDBAT_PLUGIN marker, initialize_plugin function) are
unchanged.

Test discovers a temp plugin whose class name sorts after PredBatPlugin
and asserts the right class was instantiated. Verified it fails without
the fix:
  ERROR: discovery instantiated PredBatPlugin, expected ZzSortsLastPlugin
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