diff --git a/docs/plugins/concepts.rst b/docs/plugins/concepts.rst index b57535a7..87386049 100644 --- a/docs/plugins/concepts.rst +++ b/docs/plugins/concepts.rst @@ -60,9 +60,15 @@ The following properties are recognized: Method called upon discovery of the plugin by the plugin subsystem, should return ``True`` if the plugin can be instantiated later on, ``False`` if there are reasons why not, e.g. if dependencies are missing. -``__plugin_init__`` - Method called upon initializing of the plugin by the plugin subsystem, can be used to instantiate +``__plugin_load__`` + Method called upon loading of the plugin by the plugin subsystem, can be used to instantiate plugin implementations, connecting them to hooks etc. +``__plugin_unload__`` + Method called upon unloading of the plugin by the plugin subsystem, can be used to do any final clean ups. +``__plugin_enable__`` + Method called upon enabling of the plugin by the plugin subsystem. Also see :func:`~octoprint.plugin.core.Plugin.on_plugin_enabled``. +``__plugin_disable__`` + Method called upon disabling of the plugin by the plugin subsystem. Also see :func:`~octoprint.plugin.core.Plugin.on_plugin_disabled``. .. _sec-plugin-concepts-mixins: @@ -323,3 +329,12 @@ An overview of these properties follows. :ref:`Available Mixins ` Some mixin types trigger the injection of additional properties. + +.. _sec-plugins-concept-lifecycle: + +Lifecycle +--------- + +.. image:: ../images/plugins_lifecycle.png + :align: center + :alt: The lifecycle of OctoPrint plugins. \ No newline at end of file diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 0d8880c9..dbbb8923 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -92,19 +92,27 @@ class PluginInfo(object): """ Module attribute which to call to determine if the plugin can be loaded. """ attr_init = '__plugin_init__' - """ Module attribute which to call when loading the plugin. """ + """ + Module attribute which to call when loading the plugin. + + This deprecated attribute will only be used if a plugin does not yet offer :attr:`attr_load`. + + .. deprecated:: 1.2.0-dev-720 + + Use :attr:`attr_load` instead. + """ attr_load = '__plugin_load__' + """ Module attribute which to call when loading the plugin. """ attr_unload = '__plugin_unload__' + """ Module attribute which to call when unloading the plugin. """ attr_enable = '__plugin_enable__' + """ Module attribute which to call when enabling the plugin. """ attr_disable = '__plugin_disable__' - - attr_activate = '__plugin_activate__' - - attr_deactivate = '__plugin_deactivate__' + """ Module attribute which to call when disabling the plugin. """ def __init__(self, key, location, instance, name=None, version=None, description=None, author=None, url=None, license=None): self.key = key @@ -139,24 +147,65 @@ class PluginInfo(object): # delete __plugin_implementations__ delattr(self.instance, self.__class__.attr_implementations) + # if the plugin still uses __plugin_init__, log a deprecation warning and move it to __plugin_load__ + if hasattr(self.instance, self.__class__.attr_init): + if not hasattr(self.instance, self.__class__.attr_load): + # deprecation warning + import warnings + warnings.warn("{name} uses deprecated control property __plugin_init__, use __plugin_load__ instead".format(name=self.key), DeprecationWarning) + + # move it + init = getattr(self.instance, self.__class__.attr_init) + setattr(self.instance, self.__class__.attr_load, init) + + # delete __plugin_init__ + delattr(self.instance, self.__class__.attr_init) + def __str__(self): if self.version: return "{name} ({version})".format(name=self.name, version=self.version) else: return self.name - def long_str(self, show_bundled=False, bundled_str=(" [B]", ""), + def long_str(self, show_bundled=False, bundled_strs=(" [B]", ""), show_location=False, location_str=" - {location}", - show_enabled=False, enabled_str=("* ", " ")): + show_enabled=False, enabled_strs=("* ", " ")): + """ + Long string representation of the plugin's information. Will return a string of the format ````. + + ``enabled``, ``bundled`` and ``location`` will only be displayed if the corresponding flags are set to ``True``. + The will be filled from ``enabled_str``, ``bundled_str`` and ``location_str`` as follows: + + ``enabled_str`` + a 2-tuple, the first entry being the string to insert when the plugin is enabled, the second + entry the string to insert when it is not. + ``bundled_str`` + a 2-tuple, the first entry being the string to insert when the plugin is bundled, the second + entry the string to insert when it is not. + ``location_str`` + a format string (to be parsed with ``str.format``), the ``{location}`` placeholder will be + replaced with the plugin's installation folder on disk. + + Arguments: + show_enabled (boolean): whether to show the ``enabled`` part + enabled_strs (tuple): the 2-tuple containing the two possible strings to use for displaying the enabled state + show_bundled (boolean): whether to show the ``bundled`` part + bundled_strs(tuple): the 2-tuple containing the two possible strings to use for displaying the bundled state + show_location (boolean): whether to show the ``location`` part + location_str (str): the format string to use for displaying the plugin's installation location + + Returns: + str: The long string representation of the plugin as described above + """ if show_enabled: - ret = enabled_str[0] if self.enabled else enabled_str[1] + ret = enabled_strs[0] if self.enabled else enabled_strs[1] else: ret = "" ret += str(self) if show_bundled: - ret += bundled_str[0] if self.bundled else bundled_str[1] + ret += bundled_strs[0] if self.bundled else bundled_strs[1] if show_location and self.location: ret += location_str.format(location=self.location) @@ -313,33 +362,41 @@ class PluginInfo(object): in :attr:`attr_load` if available, otherwise a no-operation lambda will be returned. Returns: - callable: Init method for the plugin module. + callable: Load method for the plugin module. """ - load = self._get_instance_attribute(self.__class__.attr_load, default=None) - if load is None: - load = self._get_instance_attribute(self.__class__.attr_init, default=lambda:True) - return load + return self._get_instance_attribute(self.__class__.attr_load, default=lambda: True) @property def unload(self): + """ + Method for unloading the plugin module. Will be taken from the unload attribute of the plugin module as defined + in :attr:`attr_unload` if available, otherwise a no-operation lambda will be returned. + + Returns: + callable: Unload method for the plugin module. + """ return self._get_instance_attribute(self.__class__.attr_unload, default=lambda: True) - @property - def activate(self): - return self._get_instance_attribute(self.__class__.attr_activate, default=lambda: True) - - @property - def deactivate(self): - return self._get_instance_attribute(self.__class__.attr_deactivate, default=lambda: True) - @property def enable(self): - self.enabled = True + """ + Method for enabling the plugin module. Will be taken from the enable attribute of the plugin module as defined + in :attr:`attr_enable` if available, otherwise a no-operation lambda will be returned. + + Returns: + callable: Enable method for the plugin module. + """ return self._get_instance_attribute(self.__class__.attr_enable, default=lambda: True) @property def disable(self): - self.enabled = False + """ + Method for disabling the plugin module. Will be taken from the disable attribute of the plugin module as defined + in :attr:`attr_disable` if available, otherwise a no-operation lambda will be returned. + + Returns: + callable: Disable method for the plugin module. + """ return self._get_instance_attribute(self.__class__.attr_disable, default=lambda: True) def _get_instance_attribute(self, attr, default=None, defaults=None): @@ -374,7 +431,8 @@ class PluginManager(object): self.plugin_disabled_list = plugin_disabled_list self.logging_prefix = logging_prefix - self.plugins = dict() + self.enabled_plugins = dict() + self.disabled_plugins = dict() self.plugin_hooks = defaultdict(list) self.plugin_implementations = dict() self.plugin_implementations_by_type = defaultdict(list) @@ -382,8 +440,6 @@ class PluginManager(object): self.implementation_injects = dict() self.implementation_inject_factories = [] - self.disabled_plugins = dict() - self.on_plugin_loaded = lambda *args, **kwargs: None self.on_plugin_unloaded = lambda *args, **kwargs: None self.on_plugin_enabled = lambda *args, **kwargs: None @@ -395,14 +451,14 @@ class PluginManager(object): self.reload_plugins(startup=True, initialize_implementations=False) @property - def all_plugins(self): - plugins = dict(self.plugins) + def plugins(self): + plugins = dict(self.enabled_plugins) plugins.update(self.disabled_plugins) return plugins def find_plugins(self, existing=None): if existing is None: - existing = self.all_plugins + existing = self.plugins result = dict() if self.plugin_folders: @@ -547,22 +603,22 @@ class PluginManager(object): except (PluginLifecycleException, PluginNeedsRestart): pass - if len(self.plugins) <= 0: + if len(self.enabled_plugins) <= 0: self.logger.info("No plugins found") else: self.logger.info("Found {count} plugin(s) providing {implementations} mixin implementations, {hooks} hook handlers".format( - count=len(self.plugins) + len(self.disabled_plugins), + count=len(self.enabled_plugins) + len(self.disabled_plugins), implementations=len(self.plugin_implementations), hooks=sum(map(lambda x: len(x), self.plugin_hooks.values())) )) def load_plugin(self, name, plugin=None, startup=False, initialize_implementation=True): - if not name in self.all_plugins: + if not name in self.plugins: self.logger.warn("Trying to load an unknown plugin {name}".format(**locals())) return if plugin is None: - plugin = self.all_plugins[name] + plugin = self.plugins[name] try: plugin.load() @@ -578,11 +634,11 @@ class PluginManager(object): self.logger.exception("There was an error loading plugin %s" % name) def unload_plugin(self, name): - if not name in self.all_plugins: + if not name in self.plugins: self.logger.warn("Trying to unload unknown plugin {name}".format(**locals())) return - plugin = self.all_plugins[name] + plugin = self.plugins[name] try: if plugin.enabled: @@ -591,8 +647,8 @@ class PluginManager(object): plugin.unload() self.on_plugin_unloaded(name, plugin) - if name in self.plugins: - del self.plugins[name] + if name in self.enabled_plugins: + del self.enabled_plugins[name] if name in self.disabled_plugins: del self.disabled_plugins[name] @@ -606,8 +662,8 @@ class PluginManager(object): self.logger.exception("There was an error unloading plugin {name}".format(**locals())) # make sure the plugin is NOT in the list of enabled plugins but in the list of disabled plugins - if name in self.plugins: - del self.plugins[name] + if name in self.enabled_plugins: + del self.enabled_plugins[name] if not name in self.disabled_plugins: self.disabled_plugins[name] = plugin @@ -633,7 +689,7 @@ class PluginManager(object): else: if name in self.disabled_plugins: del self.disabled_plugins[name] - self.plugins[name] = plugin + self.enabled_plugins[name] = plugin plugin.enabled = True if plugin.implementation: @@ -648,12 +704,12 @@ class PluginManager(object): return True def disable_plugin(self, name, plugin=None): - if not name in self.plugins: + if not name in self.enabled_plugins: self.logger.warn("Tried to disable plugin {name}, however it is not enabled".format(**locals())) return if plugin is None: - plugin = self.plugins[name] + plugin = self.enabled_plugins[name] if plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin): raise PluginNeedsRestart(name) @@ -667,8 +723,8 @@ class PluginManager(object): self.logger.exception("There was an error while disabling plugin {name}".format(**locals())) return False else: - if name in self.plugins: - del self.plugins[name] + if name in self.enabled_plugins: + del self.enabled_plugins[name] self.disabled_plugins[name] = plugin plugin.enabled = False @@ -716,7 +772,7 @@ class PluginManager(object): pass def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): - for name, plugin in self.plugins.items(): + for name, plugin in self.enabled_plugins.items(): self.initialize_implementation_of_plugin(name, plugin, additional_injects=additional_injects, additional_inject_factories=additional_inject_factories) @@ -789,7 +845,7 @@ class PluginManager(object): def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), show_location=True, location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!")): - all_plugins = self.plugins.values() + self.disabled_plugins.values() + all_plugins = self.enabled_plugins.values() + self.disabled_plugins.values() if len(all_plugins) <= 0: self.logger.info("No plugins available") @@ -797,12 +853,12 @@ class PluginManager(object): self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join( sorted( map(lambda x: "| " + x.long_str(show_bundled=show_bundled, - bundled_str=bundled_str, + bundled_strs=bundled_str, show_location=show_location, location_str=location_str, show_enabled=show_enabled, - enabled_str=enabled_str), - self.plugins.values()) + enabled_strs=enabled_str), + self.enabled_plugins.values()) ) ))) @@ -839,8 +895,8 @@ class PluginManager(object): ~.PluginInfo: The requested :class:`PluginInfo` or None """ - if identifier in self.plugins: - return self.plugins[identifier] + if identifier in self.enabled_plugins: + return self.enabled_plugins[identifier] elif not require_enabled and identifier in self.disabled_plugins: return self.disabled_plugins[identifier] @@ -917,9 +973,9 @@ class PluginManager(object): registered with the system. """ - if not name in self.plugins: + if not name in self.enabled_plugins: return None - plugin = self.plugins[name] + plugin = self.enabled_plugins[name] all_helpers = plugin.helpers if len(helpers): diff --git a/src/octoprint/plugins/discovery/__init__.py b/src/octoprint/plugins/discovery/__init__.py index 90009281..4732ce19 100644 --- a/src/octoprint/plugins/discovery/__init__.py +++ b/src/octoprint/plugins/discovery/__init__.py @@ -28,7 +28,7 @@ __plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Discovery" __plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf and uPnP" __plugin_license__ = "AGPLv3" -def __plugin_init__(): +def __plugin_load__(): if not pybonjour: # no pybonjour available, we can't use that logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available") diff --git a/tests/plugin/test_core.py b/tests/plugin/test_core.py index 8a3f037d..0667785d 100644 --- a/tests/plugin/test_core.py +++ b/tests/plugin/test_core.py @@ -23,7 +23,7 @@ class PluginTestCase(unittest.TestCase): self.plugin_manager.initialize_implementations() def test_plugin_loading(self): - self.assertEquals(5, len(self.plugin_manager.plugins)) + self.assertEquals(5, len(self.plugin_manager.enabled_plugins)) self.assertEquals(1, len(self.plugin_manager.plugin_hooks)) self.assertEquals(4, len(self.plugin_manager.plugin_implementations)) self.assertEquals(3, len(self.plugin_manager.plugin_implementations_by_type)) @@ -65,8 +65,8 @@ class PluginTestCase(unittest.TestCase): all_implementations = self.plugin_manager.plugin_implementations self.assertEquals(4, len(all_implementations)) for name, impl in all_implementations.items(): - self.assertTrue(name in self.plugin_manager.plugins) - plugin = self.plugin_manager.plugins[name] + self.assertTrue(name in self.plugin_manager.enabled_plugins) + plugin = self.plugin_manager.enabled_plugins[name] # test that the standard fields were properly initialized self.assertTrue(hasattr(impl, "_identifier")) @@ -162,8 +162,8 @@ class PluginTestCase(unittest.TestCase): client2.on_plugin_message.assert_called_once_with(plugin, data) def test_validate_plugin(self): - self.assertTrue("deprecated_plugin" in self.plugin_manager.plugins) + self.assertTrue("deprecated_plugin" in self.plugin_manager.enabled_plugins) - plugin = self.plugin_manager.plugins["deprecated_plugin"] + plugin = self.plugin_manager.enabled_plugins["deprecated_plugin"] self.assertTrue(hasattr(plugin.instance, plugin.__class__.attr_implementation)) self.assertFalse(hasattr(plugin.instance, plugin.__class__.attr_implementations))