Finalizing first version of plugin lifecycle management

This commit is contained in:
Gina Häußge 2015-04-14 17:55:46 +02:00
parent fc00ef032e
commit 2aa31024e6
4 changed files with 133 additions and 62 deletions

View file

@ -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 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 plugin can be instantiated later on, ``False`` if there are reasons why not, e.g. if dependencies
are missing. are missing.
``__plugin_init__`` ``__plugin_load__``
Method called upon initializing of the plugin by the plugin subsystem, can be used to instantiate Method called upon loading of the plugin by the plugin subsystem, can be used to instantiate
plugin implementations, connecting them to hooks etc. 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: .. _sec-plugin-concepts-mixins:
@ -323,3 +329,12 @@ An overview of these properties follows.
:ref:`Available Mixins <sec-plugins-mixins>` :ref:`Available Mixins <sec-plugins-mixins>`
Some mixin types trigger the injection of additional properties. 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.

View file

@ -92,19 +92,27 @@ class PluginInfo(object):
""" Module attribute which to call to determine if the plugin can be loaded. """ """ Module attribute which to call to determine if the plugin can be loaded. """
attr_init = '__plugin_init__' 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__' attr_load = '__plugin_load__'
""" Module attribute which to call when loading the plugin. """
attr_unload = '__plugin_unload__' attr_unload = '__plugin_unload__'
""" Module attribute which to call when unloading the plugin. """
attr_enable = '__plugin_enable__' attr_enable = '__plugin_enable__'
""" Module attribute which to call when enabling the plugin. """
attr_disable = '__plugin_disable__' attr_disable = '__plugin_disable__'
""" Module attribute which to call when disabling the plugin. """
attr_activate = '__plugin_activate__'
attr_deactivate = '__plugin_deactivate__'
def __init__(self, key, location, instance, name=None, version=None, description=None, author=None, url=None, license=None): def __init__(self, key, location, instance, name=None, version=None, description=None, author=None, url=None, license=None):
self.key = key self.key = key
@ -139,24 +147,65 @@ class PluginInfo(object):
# delete __plugin_implementations__ # delete __plugin_implementations__
delattr(self.instance, self.__class__.attr_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): def __str__(self):
if self.version: if self.version:
return "{name} ({version})".format(name=self.name, version=self.version) return "{name} ({version})".format(name=self.name, version=self.version)
else: else:
return self.name 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_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><str(self)><bundled><location>``.
``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: 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: else:
ret = "" ret = ""
ret += str(self) ret += str(self)
if show_bundled: 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: if show_location and self.location:
ret += location_str.format(location=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. in :attr:`attr_load` if available, otherwise a no-operation lambda will be returned.
Returns: 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) return self._get_instance_attribute(self.__class__.attr_load, default=lambda: True)
if load is None:
load = self._get_instance_attribute(self.__class__.attr_init, default=lambda:True)
return load
@property @property
def unload(self): 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) 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 @property
def enable(self): 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) return self._get_instance_attribute(self.__class__.attr_enable, default=lambda: True)
@property @property
def disable(self): 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) return self._get_instance_attribute(self.__class__.attr_disable, default=lambda: True)
def _get_instance_attribute(self, attr, default=None, defaults=None): 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.plugin_disabled_list = plugin_disabled_list
self.logging_prefix = logging_prefix self.logging_prefix = logging_prefix
self.plugins = dict() self.enabled_plugins = dict()
self.disabled_plugins = dict()
self.plugin_hooks = defaultdict(list) self.plugin_hooks = defaultdict(list)
self.plugin_implementations = dict() self.plugin_implementations = dict()
self.plugin_implementations_by_type = defaultdict(list) self.plugin_implementations_by_type = defaultdict(list)
@ -382,8 +440,6 @@ class PluginManager(object):
self.implementation_injects = dict() self.implementation_injects = dict()
self.implementation_inject_factories = [] self.implementation_inject_factories = []
self.disabled_plugins = dict()
self.on_plugin_loaded = lambda *args, **kwargs: None self.on_plugin_loaded = lambda *args, **kwargs: None
self.on_plugin_unloaded = lambda *args, **kwargs: None self.on_plugin_unloaded = lambda *args, **kwargs: None
self.on_plugin_enabled = 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) self.reload_plugins(startup=True, initialize_implementations=False)
@property @property
def all_plugins(self): def plugins(self):
plugins = dict(self.plugins) plugins = dict(self.enabled_plugins)
plugins.update(self.disabled_plugins) plugins.update(self.disabled_plugins)
return plugins return plugins
def find_plugins(self, existing=None): def find_plugins(self, existing=None):
if existing is None: if existing is None:
existing = self.all_plugins existing = self.plugins
result = dict() result = dict()
if self.plugin_folders: if self.plugin_folders:
@ -547,22 +603,22 @@ class PluginManager(object):
except (PluginLifecycleException, PluginNeedsRestart): except (PluginLifecycleException, PluginNeedsRestart):
pass pass
if len(self.plugins) <= 0: if len(self.enabled_plugins) <= 0:
self.logger.info("No plugins found") self.logger.info("No plugins found")
else: else:
self.logger.info("Found {count} plugin(s) providing {implementations} mixin implementations, {hooks} hook handlers".format( 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), implementations=len(self.plugin_implementations),
hooks=sum(map(lambda x: len(x), self.plugin_hooks.values())) hooks=sum(map(lambda x: len(x), self.plugin_hooks.values()))
)) ))
def load_plugin(self, name, plugin=None, startup=False, initialize_implementation=True): 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())) self.logger.warn("Trying to load an unknown plugin {name}".format(**locals()))
return return
if plugin is None: if plugin is None:
plugin = self.all_plugins[name] plugin = self.plugins[name]
try: try:
plugin.load() plugin.load()
@ -578,11 +634,11 @@ class PluginManager(object):
self.logger.exception("There was an error loading plugin %s" % name) self.logger.exception("There was an error loading plugin %s" % name)
def unload_plugin(self, 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())) self.logger.warn("Trying to unload unknown plugin {name}".format(**locals()))
return return
plugin = self.all_plugins[name] plugin = self.plugins[name]
try: try:
if plugin.enabled: if plugin.enabled:
@ -591,8 +647,8 @@ class PluginManager(object):
plugin.unload() plugin.unload()
self.on_plugin_unloaded(name, plugin) self.on_plugin_unloaded(name, plugin)
if name in self.plugins: if name in self.enabled_plugins:
del self.plugins[name] del self.enabled_plugins[name]
if name in self.disabled_plugins: if name in self.disabled_plugins:
del self.disabled_plugins[name] del self.disabled_plugins[name]
@ -606,8 +662,8 @@ class PluginManager(object):
self.logger.exception("There was an error unloading plugin {name}".format(**locals())) 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 # make sure the plugin is NOT in the list of enabled plugins but in the list of disabled plugins
if name in self.plugins: if name in self.enabled_plugins:
del self.plugins[name] del self.enabled_plugins[name]
if not name in self.disabled_plugins: if not name in self.disabled_plugins:
self.disabled_plugins[name] = plugin self.disabled_plugins[name] = plugin
@ -633,7 +689,7 @@ class PluginManager(object):
else: else:
if name in self.disabled_plugins: if name in self.disabled_plugins:
del self.disabled_plugins[name] del self.disabled_plugins[name]
self.plugins[name] = plugin self.enabled_plugins[name] = plugin
plugin.enabled = True plugin.enabled = True
if plugin.implementation: if plugin.implementation:
@ -648,12 +704,12 @@ class PluginManager(object):
return True return True
def disable_plugin(self, name, plugin=None): 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())) self.logger.warn("Tried to disable plugin {name}, however it is not enabled".format(**locals()))
return return
if plugin is None: if plugin is None:
plugin = self.plugins[name] plugin = self.enabled_plugins[name]
if plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin): if plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin):
raise PluginNeedsRestart(name) raise PluginNeedsRestart(name)
@ -667,8 +723,8 @@ class PluginManager(object):
self.logger.exception("There was an error while disabling plugin {name}".format(**locals())) self.logger.exception("There was an error while disabling plugin {name}".format(**locals()))
return False return False
else: else:
if name in self.plugins: if name in self.enabled_plugins:
del self.plugins[name] del self.enabled_plugins[name]
self.disabled_plugins[name] = plugin self.disabled_plugins[name] = plugin
plugin.enabled = False plugin.enabled = False
@ -716,7 +772,7 @@ class PluginManager(object):
pass pass
def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): 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, self.initialize_implementation_of_plugin(name, plugin,
additional_injects=additional_injects, additional_injects=additional_injects,
additional_inject_factories=additional_inject_factories) 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=(" ", "!")): 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: if len(all_plugins) <= 0:
self.logger.info("No plugins available") 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( self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join(
sorted( sorted(
map(lambda x: "| " + x.long_str(show_bundled=show_bundled, map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_str=bundled_str, bundled_strs=bundled_str,
show_location=show_location, show_location=show_location,
location_str=location_str, location_str=location_str,
show_enabled=show_enabled, show_enabled=show_enabled,
enabled_str=enabled_str), enabled_strs=enabled_str),
self.plugins.values()) self.enabled_plugins.values())
) )
))) )))
@ -839,8 +895,8 @@ class PluginManager(object):
~.PluginInfo: The requested :class:`PluginInfo` or None ~.PluginInfo: The requested :class:`PluginInfo` or None
""" """
if identifier in self.plugins: if identifier in self.enabled_plugins:
return self.plugins[identifier] return self.enabled_plugins[identifier]
elif not require_enabled and identifier in self.disabled_plugins: elif not require_enabled and identifier in self.disabled_plugins:
return self.disabled_plugins[identifier] return self.disabled_plugins[identifier]
@ -917,9 +973,9 @@ class PluginManager(object):
registered with the system. registered with the system.
""" """
if not name in self.plugins: if not name in self.enabled_plugins:
return None return None
plugin = self.plugins[name] plugin = self.enabled_plugins[name]
all_helpers = plugin.helpers all_helpers = plugin.helpers
if len(helpers): if len(helpers):

View file

@ -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_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf and uPnP"
__plugin_license__ = "AGPLv3" __plugin_license__ = "AGPLv3"
def __plugin_init__(): def __plugin_load__():
if not pybonjour: if not pybonjour:
# no pybonjour available, we can't use that # no pybonjour available, we can't use that
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available") logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available")

View file

@ -23,7 +23,7 @@ class PluginTestCase(unittest.TestCase):
self.plugin_manager.initialize_implementations() self.plugin_manager.initialize_implementations()
def test_plugin_loading(self): 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(1, len(self.plugin_manager.plugin_hooks))
self.assertEquals(4, len(self.plugin_manager.plugin_implementations)) self.assertEquals(4, len(self.plugin_manager.plugin_implementations))
self.assertEquals(3, len(self.plugin_manager.plugin_implementations_by_type)) 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 all_implementations = self.plugin_manager.plugin_implementations
self.assertEquals(4, len(all_implementations)) self.assertEquals(4, len(all_implementations))
for name, impl in all_implementations.items(): for name, impl in all_implementations.items():
self.assertTrue(name in self.plugin_manager.plugins) self.assertTrue(name in self.plugin_manager.enabled_plugins)
plugin = self.plugin_manager.plugins[name] plugin = self.plugin_manager.enabled_plugins[name]
# test that the standard fields were properly initialized # test that the standard fields were properly initialized
self.assertTrue(hasattr(impl, "_identifier")) self.assertTrue(hasattr(impl, "_identifier"))
@ -162,8 +162,8 @@ class PluginTestCase(unittest.TestCase):
client2.on_plugin_message.assert_called_once_with(plugin, data) client2.on_plugin_message.assert_called_once_with(plugin, data)
def test_validate_plugin(self): 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.assertTrue(hasattr(plugin.instance, plugin.__class__.attr_implementation))
self.assertFalse(hasattr(plugin.instance, plugin.__class__.attr_implementations)) self.assertFalse(hasattr(plugin.instance, plugin.__class__.attr_implementations))