Repository navigation
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes Predbat’s plugin discovery so it won’t instantiate an imported base class (e.g., PredBatPlugin) just because it alphabetically sorts first in inspect.getmembers(), preventing silently “loaded” but non-functional plugins.
Changes:
- Tighten name-based plugin discovery to only consider classes defined in the plugin module (
obj.__module__ == plugin_module.__name__). - Add a regression test that creates a temp plugin whose class name sorts after
PredBatPluginand asserts the correct class is instantiated. - Register the new test in
unit_test.pyso it runs with the suite.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| apps/predbat/plugin_system.py | Restricts name-based class discovery to classes defined in the plugin file to avoid instantiating imported base classes. |
| apps/predbat/tests/test_plugin_startup.py | Adds a regression test for the discovery bug using a temporary plugin module. |
| apps/predbat/unit_test.py | Registers the new plugin discovery regression test in the main test list. |
4ebf9a4 to
0666c8c
Compare
|
@springfall2008 — gentle nudge on this one, it's been quiet since 26 August and I think it's ready. Status: CI green (pre-commit pass), no conflicts, and Copilot's review point is addressed in 3e1a1a7: strategy 2 (the One correction to the description above, which predates that commit: it says the two fallback strategies are unchanged. Strategy 2 is now guarded as well; strategy 3 ( Happy to rebase or split it if that would be easier to take. |
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
Addresses review feedback on the name-based fix.
PredBatPlugin sets PREDBAT_PLUGIN = True as its own discovery marker, so it
matches strategy 2 in every plugin that does `from plugin_system import
PredBatPlugin`. Guarding only strategy 1 left the identical bug reachable in the
two cases where strategy 1 declines: the plugin class does not end in "Plugin",
or it raised during __init__ and left plugin_instance None. Both end with the
abstract base constructed, the plugin logged as loaded, and no hooks registered.
Same __module__ check, same reason. Also replaces the redundant
hasattr/getattr pair with a single getattr default.
The new test needs three properties together, and getting any of them wrong
hides the bug: the file must end "_plugin.py" or discovery never reads it; the
class must not end in "Plugin" so strategy 1 declines; and the class must sort
AFTER "PredBatPlugin", because inspect.getmembers() is alphabetical and strategy
2 breaks on its first match. A first draft named the class AttributeOnlyWorker,
which sorts before the base and so passed with and without the fix. Verified
failing without the guard ("fallback instantiated PredBatPlugin, expected
ZzAttributeWorker") and passing with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVDP1s2fmBMFmbHNAirnUt
3e1a1a7 to
c2e75f0
Compare
|
@mgazza — you wrote the plugin loader in #2582, so you're the best person to sanity-check this. Could you take a look? @springfall2008 for merge if he's happy. The bug, in one line: any plugin whose class name sorts alphabetically after Why: Repro: a plugin file containing The fix: only consider classes defined in the plugin's own module ( Size: 116 lines added in total. The change to Rebased on |
The bug
load_plugin()'s preferred detection strategy takes the first class whose name ends inPlugin:inspect.getmembers()returns members alphabetically. Every plugin doesfrom 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 afterPredBatPlugintherefore gets the base instantiated instead of itself.When that happens the plugin looks completely healthy in the log:
…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;ColdWeatherPluginandCurtailmentPluginhappen to sort beforePredBatPlugin, 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:
This also covers the general case of a plugin importing any other helper class ending in
Plugin.The two fallback strategies (the
PREDBAT_PLUGINmarker and theinitialize_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_classto the existingtests/test_plugin_startup.py(no new files). It writes a temp plugin whose class name deliberately sorts afterPredBatPlugin, runs real discovery over it, and asserts the correct class was instantiated.Confirmed it fails without the fix:
and passes with it. Registered in
unit_test.pyasplugin_discovery. Fullunit_test.py --quickpasses,pre-commit runclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01WY52NvNyVdznsF4VcVG8Q8