Finalizing first version of plugin lifecycle management
This commit is contained in:
parent
fc00ef032e
commit
2aa31024e6
4 changed files with 133 additions and 62 deletions
|
|
@ -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 <sec-plugins-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.
|
||||
|
|
@ -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><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:
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue