From 945cb46f3249ba64d64f2fa450b71a580172fa9d Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 6 Jul 2025 10:15:33 +1000 Subject: [PATCH] [bug] Logic fix for plugins (#9934) * Logic fix for plugins - Prevent tasks being run for disabled plugins * Adjust default value for "get_plugin" method * Fix return type * Update typing * Tweak unit test * Update unit tests * More test updates --- src/backend/InvenTree/InvenTree/exchange.py | 2 +- src/backend/InvenTree/data_exporter/tasks.py | 2 +- .../InvenTree/plugin/base/event/events.py | 4 ++-- src/backend/InvenTree/plugin/base/label/label.py | 4 ++-- .../plugin/base/label/test_label_mixin.py | 2 +- .../InvenTree/plugin/base/locate/test_locate.py | 4 +--- src/backend/InvenTree/plugin/registry.py | 16 +++++++++------- .../plugin/samples/event/test_event_sample.py | 4 +--- .../samples/event/test_filtered_event_sample.py | 8 ++------ .../plugin/samples/icons/test_icon_sample.py | 4 +--- .../plugin/samples/integration/test_sample.py | 9 ++++++++- .../samples/integration/test_scheduled_task.py | 10 +++++++++- .../plugin/samples/locate/test_locate_sample.py | 4 ++-- .../plugin/samples/mail/test_mail_sample.py | 2 +- src/backend/InvenTree/plugin/test_plugin.py | 3 ++- src/backend/InvenTree/report/tasks.py | 2 +- src/backend/InvenTree/report/tests.py | 5 +---- 17 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/exchange.py b/src/backend/InvenTree/InvenTree/exchange.py index 29fec64f29..639d60ff7a 100644 --- a/src/backend/InvenTree/InvenTree/exchange.py +++ b/src/backend/InvenTree/InvenTree/exchange.py @@ -30,7 +30,7 @@ class InvenTreeExchange(SimpleExchangeBackend): # Find the selected exchange rate plugin slug = get_global_setting('CURRENCY_UPDATE_PLUGIN', create=False) - plugin = registry.get_plugin(slug) if slug else None + plugin = registry.get_plugin(slug, active=True) if slug else None if not plugin: # Find the first active currency exchange plugin diff --git a/src/backend/InvenTree/data_exporter/tasks.py b/src/backend/InvenTree/data_exporter/tasks.py index 6895895c19..4a6bde9c87 100644 --- a/src/backend/InvenTree/data_exporter/tasks.py +++ b/src/backend/InvenTree/data_exporter/tasks.py @@ -35,7 +35,7 @@ def export_data( """ from plugin import registry - if (plugin := registry.get_plugin(plugin_key)) is None: + if (plugin := registry.get_plugin(plugin_key, active=True)) is None: logger.warning("export_data: Plugin '%s' not found", plugin_key) return diff --git a/src/backend/InvenTree/plugin/base/event/events.py b/src/backend/InvenTree/plugin/base/event/events.py index 3596b3711c..f59330b754 100644 --- a/src/backend/InvenTree/plugin/base/event/events.py +++ b/src/backend/InvenTree/plugin/base/event/events.py @@ -105,10 +105,10 @@ def process_event(plugin_slug, event, *args, **kwargs): This function is run by the background worker process. This function may queue multiple functions to be handled by the background worker. """ - plugin = registry.get_plugin(plugin_slug) + plugin = registry.get_plugin(plugin_slug, active=True) if plugin is None: # pragma: no cover - logger.error("Could not find matching plugin for '%s'", plugin_slug) + logger.error("Could not find matching active plugin for '%s'", plugin_slug) return logger.debug("Plugin '%s' is processing triggered event '%s'", plugin_slug, event) diff --git a/src/backend/InvenTree/plugin/base/label/label.py b/src/backend/InvenTree/plugin/base/label/label.py index 028c3c8e09..ea6e229bdc 100644 --- a/src/backend/InvenTree/plugin/base/label/label.py +++ b/src/backend/InvenTree/plugin/base/label/label.py @@ -26,10 +26,10 @@ def print_label(plugin_slug: str, **kwargs): """ logger.info("Plugin '%s' is printing a label", plugin_slug) - plugin = registry.get_plugin(plugin_slug) + plugin = registry.get_plugin(plugin_slug, active=True) if plugin is None: # pragma: no cover - logger.error("Could not find matching plugin for '%s'", plugin_slug) + logger.error("Could not find matching active plugin for '%s'", plugin_slug) return try: diff --git a/src/backend/InvenTree/plugin/base/label/test_label_mixin.py b/src/backend/InvenTree/plugin/base/label/test_label_mixin.py index d77364e812..0fcd470853 100644 --- a/src/backend/InvenTree/plugin/base/label/test_label_mixin.py +++ b/src/backend/InvenTree/plugin/base/label/test_label_mixin.py @@ -124,7 +124,7 @@ class LabelMixinTests(PrintTestMixins, InvenTreeAPITestCase): plugins = registry.with_mixin(PluginMixinEnum.LABELS) self.assertGreater(len(plugins), 0) - plugin = registry.get_plugin('samplelabelprinter') + plugin = registry.get_plugin('samplelabelprinter', active=None) self.assertIsNotNone(plugin) config = plugin.plugin_config() diff --git a/src/backend/InvenTree/plugin/base/locate/test_locate.py b/src/backend/InvenTree/plugin/base/locate/test_locate.py index 6170edfb8b..23277d845a 100644 --- a/src/backend/InvenTree/plugin/base/locate/test_locate.py +++ b/src/backend/InvenTree/plugin/base/locate/test_locate.py @@ -16,9 +16,7 @@ class LocatePluginTests(InvenTreeAPITestCase): super().setUp() # Activate plugin - config = registry.get_plugin('samplelocate').plugin_config() - config.active = True - config.save() + registry.set_plugin_state('samplelocate', True) fixtures = ['category', 'part', 'location', 'stock'] diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 9ed312f1dc..d664903ce8 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -101,16 +101,18 @@ class PluginsRegistry: self.installed_apps = [] # Holds all added plugin_paths @property - def is_loading(self): + def is_loading(self) -> bool: """Return True if the plugin registry is currently loading.""" return self.loading_lock.locked() - def get_plugin(self, slug, active=None, with_mixin=None): + def get_plugin( + self, slug: str, active: bool = True, with_mixin: Optional[str] = None + ) -> InvenTreePlugin: """Lookup plugin by slug (unique key). Args: slug (str): The slug of the plugin to look up. - active (bool, optional): Filter by 'active' status of the plugin. If None, no filtering is applied. Defaults to None. + active (bool, optional): Filter by 'active' status of the plugin. If None, no filtering is applied. Defaults to True. with_mixin (str, optional): Filter by mixin name. If None, no filtering is applied. Defaults to None. Returns: @@ -136,7 +138,7 @@ class PluginsRegistry: def get_plugin_config(self, slug: str, name: Union[str, None] = None): """Return the matching PluginConfig instance for a given plugin. - Args: + Arguments: slug: The plugin slug name: The plugin name (optional) """ @@ -167,10 +169,10 @@ class PluginsRegistry: return cfg - def set_plugin_state(self, slug, state): + def set_plugin_state(self, slug: str, state: bool): """Set the state(active/inactive) of a plugin. - Args: + Arguments: slug (str): Plugin slug state (bool): Plugin state - true = active, false = inactive """ @@ -374,7 +376,7 @@ class PluginsRegistry: # Ensure the lock is released always self.loading_lock.release() - def plugin_dirs(self): + def plugin_dirs(self) -> list[str]: """Construct a list of directories from where plugins can be loaded.""" # Builtin plugins are *always* loaded dirs = ['plugin.builtin'] diff --git a/src/backend/InvenTree/plugin/samples/event/test_event_sample.py b/src/backend/InvenTree/plugin/samples/event/test_event_sample.py index c65ff2dfd4..035c67f2bc 100644 --- a/src/backend/InvenTree/plugin/samples/event/test_event_sample.py +++ b/src/backend/InvenTree/plugin/samples/event/test_event_sample.py @@ -15,9 +15,7 @@ class EventPluginSampleTests(TestCase): def test_run_event(self): """Check if the event is issued.""" # Activate plugin - config = registry.get_plugin('sampleevent').plugin_config() - config.active = True - config.save() + registry.set_plugin_state('sampleevent', True) InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None) diff --git a/src/backend/InvenTree/plugin/samples/event/test_filtered_event_sample.py b/src/backend/InvenTree/plugin/samples/event/test_filtered_event_sample.py index 0e7e9fc065..3a8e7474b1 100644 --- a/src/backend/InvenTree/plugin/samples/event/test_filtered_event_sample.py +++ b/src/backend/InvenTree/plugin/samples/event/test_filtered_event_sample.py @@ -13,9 +13,7 @@ class FilteredEventPluginSampleTests(TestCase): def test_run_event(self): """Check if the event is issued.""" # Activate plugin - config = registry.get_plugin('filteredsampleevent').plugin_config() - config.active = True - config.save() + registry.set_plugin_state('filteredsampleevent', True) InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None) @@ -29,9 +27,7 @@ class FilteredEventPluginSampleTests(TestCase): def test_ignore_event(self): """Check if the event is issued.""" # Activate plugin - config = registry.get_plugin('filteredsampleevent').plugin_config() - config.active = True - config.save() + registry.set_plugin_state('filteredsampleevent', True) InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None) diff --git a/src/backend/InvenTree/plugin/samples/icons/test_icon_sample.py b/src/backend/InvenTree/plugin/samples/icons/test_icon_sample.py index fa1eef5af8..f1767b5278 100644 --- a/src/backend/InvenTree/plugin/samples/icons/test_icon_sample.py +++ b/src/backend/InvenTree/plugin/samples/icons/test_icon_sample.py @@ -14,9 +14,7 @@ class SampleIconPackPluginTests(InvenTreeAPITestCase): def test_get_icons_api(self): """Check get icons api.""" # Activate plugin - config = registry.get_plugin('sampleicons').plugin_config() - config.active = True - config.save() + registry.set_plugin_state('sampleicons', True) response = self.get(reverse('api-icon-list'), expected_code=200) self.assertEqual(len(response.data), 2) diff --git a/src/backend/InvenTree/plugin/samples/integration/test_sample.py b/src/backend/InvenTree/plugin/samples/integration/test_sample.py index 1da0bb9800..ea86e3f312 100644 --- a/src/backend/InvenTree/plugin/samples/integration/test_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/test_sample.py @@ -45,6 +45,7 @@ class SampleIntegrationPluginTests(InvenTreeTestCase): def test_settings(self): """Check the SettingsMixin.check_settings function.""" + registry.set_plugin_state('sample', True) plugin = registry.get_plugin('sample') self.assertIsNotNone(plugin) @@ -57,13 +58,19 @@ class SampleIntegrationPluginTests(InvenTreeTestCase): def test_settings_validator(self): """Test settings validator for plugins.""" + registry.set_plugin_state('sample', False) + self.assertIsNone(registry.get_plugin('sample')) + + registry.set_plugin_state('sample', True) plugin = registry.get_plugin('sample') + self.assertIsNotNone(plugin) + valid_json = '{"ts": 13}' not_valid_json = '{"ts""13"}' # no error, should pass validator plugin.set_setting('VALIDATOR_SETTING', valid_json) - # should throw an error + # This should throw an error with self.assertRaises(ValidationError): plugin.set_setting('VALIDATOR_SETTING', not_valid_json) diff --git a/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py b/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py index 2d5bc9c12c..edf428f0f1 100644 --- a/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -76,7 +76,15 @@ class ExampleScheduledTaskPluginTests(TestCase): def test_calling(self): """Test calling of plugin functions by name.""" - # Check with right parameters + # First, plugin is *not* enabled + registry.set_plugin_state('schedule', False) + + with self.assertRaises(AttributeError): + self.assertEqual(call_plugin_function('schedule', 'member_func'), False) + + registry.set_plugin_state('schedule', True) + + # Should work now self.assertEqual(call_plugin_function('schedule', 'member_func'), False) # Check with wrong key diff --git a/src/backend/InvenTree/plugin/samples/locate/test_locate_sample.py b/src/backend/InvenTree/plugin/samples/locate/test_locate_sample.py index e34d0de0ae..023e203539 100644 --- a/src/backend/InvenTree/plugin/samples/locate/test_locate_sample.py +++ b/src/backend/InvenTree/plugin/samples/locate/test_locate_sample.py @@ -8,7 +8,7 @@ from plugin.helpers import MixinNotImplementedError from plugin.mixins import LocateMixin -class SampleLocatePlugintests(InvenTreeAPITestCase): +class SampleLocatePluginTests(InvenTreeAPITestCase): """Tests for SampleLocatePlugin.""" fixtures = ['location', 'category', 'part', 'stock'] @@ -16,7 +16,7 @@ class SampleLocatePlugintests(InvenTreeAPITestCase): def test_run_locator(self): """Check if the event is issued.""" # Activate plugin - config = registry.get_plugin('samplelocate').plugin_config() + config = registry.get_plugin('samplelocate', active=None).plugin_config() config.active = True config.save() diff --git a/src/backend/InvenTree/plugin/samples/mail/test_mail_sample.py b/src/backend/InvenTree/plugin/samples/mail/test_mail_sample.py index 9eaad0aa00..25eb0b5a60 100644 --- a/src/backend/InvenTree/plugin/samples/mail/test_mail_sample.py +++ b/src/backend/InvenTree/plugin/samples/mail/test_mail_sample.py @@ -15,7 +15,7 @@ class MailPluginSampleTests(TestCase): def activate_plugin(self): """Activate the sample mail plugin.""" - config = registry.get_plugin('samplemail').plugin_config() + config = registry.get_plugin('samplemail', active=None).plugin_config() config.active = True config.save() diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index 2053590b20..b8ae982e45 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -217,6 +217,7 @@ class RegistryTests(TestCase): with mock.patch.dict(os.environ, envs): # Reload to rediscover plugins registry.reload_plugins(full_reload=True, collect=True) + registry.set_plugin_state('simple', True) # Depends on the meta set in InvenTree/plugin/mock/simple:SimplePlugin plg = registry.get_plugin('simple') @@ -264,7 +265,7 @@ class RegistryTests(TestCase): registry.reload_plugins(full_reload=True, collect=True) # Test that plugin was installed - plg = registry.get_plugin('zapier') + plg = registry.get_plugin('zapier', active=None) self.assertEqual(plg.slug, 'zapier') self.assertEqual(plg.name, 'inventree_zapier') diff --git a/src/backend/InvenTree/report/tasks.py b/src/backend/InvenTree/report/tasks.py index f39762e952..db8dec1d6a 100644 --- a/src/backend/InvenTree/report/tasks.py +++ b/src/backend/InvenTree/report/tasks.py @@ -68,7 +68,7 @@ def print_labels( model = template.get_model() items = model.objects.filter(pk__in=item_ids) - plugin = registry.get_plugin(plugin_slug) + plugin = registry.get_plugin(plugin_slug, active=True) if not plugin: logger.warning("Label printing plugin '%s' not found", plugin_slug) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index e351084e11..9e83b2d01f 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -357,12 +357,9 @@ class PrintTestMixins: def do_activate_plugin(self): """Activate the 'samplelabel' plugin.""" + registry.set_plugin_state(self.plugin_ref, True) plugin = registry.get_plugin(self.plugin_ref) self.assertIsNotNone(plugin) - config = plugin.plugin_config() - self.assertIsNotNone(config) - config.active = True - config.save() def run_print_test(self, qs, model_type, label: bool = True): """Run tests on single and multiple page printing.