485 lines
20 KiB
Python
485 lines
20 KiB
Python
# coding=utf-8
|
|
"""
|
|
This module represents OctoPrint's plugin subsystem. This includes management and helper methods as well as the
|
|
registered plugin types.
|
|
|
|
.. autofunction:: plugin_manager
|
|
|
|
.. autofunction:: plugin_settings
|
|
|
|
.. autofunction:: call_plugin
|
|
|
|
.. autoclass:: PluginSettings
|
|
:members:
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
__author__ = "Gina Häußge <osd@foosel.net>"
|
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
|
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
|
|
|
import os
|
|
import logging
|
|
|
|
from octoprint.settings import settings
|
|
from octoprint.plugin.core import (PluginInfo, PluginManager, Plugin)
|
|
from octoprint.plugin.types import *
|
|
|
|
from octoprint.util import deprecated
|
|
|
|
# singleton
|
|
_instance = None
|
|
|
|
def _validate_plugin(phase, plugin_info):
|
|
if phase == "after_load":
|
|
if plugin_info.implementation is not None and isinstance(plugin_info.implementation, AppPlugin):
|
|
# transform app plugin into hook
|
|
import warnings
|
|
warnings.warn("{name} uses deprecated plugin mixin AppPlugin, use octoprint.accesscontrol.appkey hook instead".format(name=plugin_info.key), DeprecationWarning)
|
|
|
|
hooks = plugin_info.hooks
|
|
if not "octoprint.accesscontrol.appkey" in hooks:
|
|
hooks["octoprint.accesscontrol.appkey"] = plugin_info.implementation.get_additional_apps
|
|
setattr(plugin_info.instance, PluginInfo.attr_hooks, hooks)
|
|
|
|
def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None,
|
|
plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None):
|
|
"""
|
|
Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager`
|
|
singleton.
|
|
|
|
Arguments:
|
|
init (boolean): A flag indicating whether this is the initial call to construct the singleton (True) or not
|
|
(False, default). If this is set to True and the plugin manager has already been initialized, a :class:`ValueError`
|
|
will be raised. The same will happen if the plugin manager has not yet been initialized and this is set to
|
|
False.
|
|
plugin_folders (list): A list of folders (as strings containing the absolute path to them) in which to look for
|
|
potential plugin modules. If not provided this defaults to the configured ``plugins`` base folder and
|
|
``src/plugins`` within OctoPrint's code base.
|
|
plugin_types (list): A list of recognized plugin types for which to look for provided implementations. If not
|
|
provided this defaults to the plugin types found in :mod:`octoprint.plugin.types` without
|
|
:class:`~octoprint.plugin.OctoPrintPlugin`.
|
|
plugin_entry_points (list): A list of entry points pointing to modules which to load as plugins. If not provided
|
|
this defaults to the entry point ``octoprint.plugin``.
|
|
plugin_disabled_list (list): A list of plugin identifiers that are currently disabled. If not provided this
|
|
defaults to all plugins for which ``enabled`` is set to ``False`` in the settings.
|
|
plugin_restart_needing_hooks (list): A list of hook namespaces which cause a plugin to need a restart in order
|
|
be enabled/disabled. Does not have to contain full hook identifiers, will be matched with startswith similar
|
|
to logging handlers
|
|
plugin_obsolete_hooks (list): A list of hooks that have been declared obsolete. Plugins implementing them will
|
|
not be enabled since they might depend on functionality that is no longer available.
|
|
plugin_validators (list): A list of additional plugin validators through which to process each plugin.
|
|
|
|
Returns:
|
|
PluginManager: A fully initialized :class:`~octoprint.plugin.core.PluginManager` instance to be used for plugin
|
|
management tasks.
|
|
|
|
Raises:
|
|
ValueError: ``init`` was True although the plugin manager was already initialized, or it was False although
|
|
the plugin manager was not yet initialized.
|
|
"""
|
|
|
|
global _instance
|
|
if _instance is not None:
|
|
if init:
|
|
raise ValueError("Plugin Manager already initialized")
|
|
|
|
else:
|
|
if init:
|
|
if plugin_folders is None:
|
|
plugin_folders = (
|
|
settings().getBaseFolder("plugins"),
|
|
(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")), True)
|
|
)
|
|
if plugin_types is None:
|
|
plugin_types = [StartupPlugin,
|
|
ShutdownPlugin,
|
|
TemplatePlugin,
|
|
SettingsPlugin,
|
|
SimpleApiPlugin,
|
|
AssetPlugin,
|
|
BlueprintPlugin,
|
|
EventHandlerPlugin,
|
|
SlicerPlugin,
|
|
AppPlugin,
|
|
ProgressPlugin]
|
|
if plugin_entry_points is None:
|
|
plugin_entry_points = "octoprint.plugin"
|
|
if plugin_disabled_list is None:
|
|
plugin_disabled_list = settings().get(["plugins", "_disabled"])
|
|
if plugin_restart_needing_hooks is None:
|
|
plugin_restart_needing_hooks = [
|
|
"octoprint.server.http"
|
|
]
|
|
if plugin_obsolete_hooks is None:
|
|
plugin_obsolete_hooks = [
|
|
"octoprint.comm.protocol.gcode"
|
|
]
|
|
if plugin_validators is None:
|
|
plugin_validators = [
|
|
_validate_plugin
|
|
]
|
|
|
|
_instance = PluginManager(plugin_folders,
|
|
plugin_types,
|
|
plugin_entry_points,
|
|
logging_prefix="octoprint.plugins.",
|
|
plugin_disabled_list=plugin_disabled_list,
|
|
plugin_restart_needing_hooks=plugin_restart_needing_hooks,
|
|
plugin_obsolete_hooks=plugin_obsolete_hooks,
|
|
plugin_validators=plugin_validators)
|
|
else:
|
|
raise ValueError("Plugin Manager not initialized yet")
|
|
return _instance
|
|
|
|
|
|
def plugin_settings(plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=None):
|
|
"""
|
|
Factory method for creating a :class:`PluginSettings` instance.
|
|
|
|
Arguments:
|
|
plugin_key (string): The plugin identifier for which to create the settings instance.
|
|
defaults (dict): The default settings for the plugin.
|
|
get_preprocessors (dict): The getter preprocessors for the plugin.
|
|
set_preprocessors (dict): The setter preprocessors for the plugin.
|
|
|
|
Returns:
|
|
PluginSettings: A fully initialized :class:`PluginSettings` instance to be used to access the plugin's
|
|
settings
|
|
"""
|
|
return PluginSettings(settings(), plugin_key, defaults=defaults, get_preprocessors=get_preprocessors, set_preprocessors=set_preprocessors)
|
|
|
|
|
|
def call_plugin(types, method, args=None, kwargs=None, callback=None, error_callback=None):
|
|
"""
|
|
Helper method to invoke the indicated ``method`` on all registered plugin implementations implementing the
|
|
indicated ``types``. Allows providing method arguments and registering callbacks to call in case of success
|
|
and/or failure of each call which can be used to return individual results to the calling code.
|
|
|
|
Example:
|
|
|
|
.. sourcecode:: python
|
|
|
|
def my_success_callback(name, plugin, result):
|
|
print("{name} was called successfully and returned {result!r}".format(**locals()))
|
|
|
|
def my_error_callback(name, plugin, exc):
|
|
print("{name} raised an exception: {exc!s}".format(**locals()))
|
|
|
|
octoprint.plugin.call_plugin(
|
|
[octoprint.plugin.StartupPlugin],
|
|
"on_startup",
|
|
args=(my_host, my_port),
|
|
callback=my_success_callback,
|
|
error_callback=my_error_callback
|
|
)
|
|
|
|
Arguments:
|
|
types (list): A list of plugin implementation types to match against.
|
|
method (string): Name of the method to call on all matching implementations.
|
|
args (tuple): A tuple containing the arguments to supply to the called ``method``. Optional.
|
|
kwargs (dict): A dictionary containing the keyword arguments to supply to the called ``method``. Optional.
|
|
callback (function): A callback to invoke after an implementation has been called successfully. Will be called
|
|
with the three arguments ``name``, ``plugin`` and ``result``. ``name`` will be the plugin identifier,
|
|
``plugin`` the plugin implementation instance itself and ``result`` the result returned from the
|
|
``method`` invocation.
|
|
error_callback (function): A callback to invoke after the call of an implementation resulted in an exception.
|
|
Will be called with the three arguments ``name``, ``plugin`` and ``exc``. ``name`` will be the plugin
|
|
identifier, ``plugin`` the plugin implementation instance itself and ``exc`` the caught exception.
|
|
|
|
"""
|
|
|
|
if not isinstance(types, (list, tuple)):
|
|
types = [types]
|
|
if args is None:
|
|
args = []
|
|
if kwargs is None:
|
|
kwargs = dict()
|
|
|
|
plugins = plugin_manager().get_implementations(*types)
|
|
for plugin in plugins:
|
|
if hasattr(plugin, method):
|
|
try:
|
|
result = getattr(plugin, method)(*args, **kwargs)
|
|
if callback:
|
|
callback(plugin._identifier, plugin, result)
|
|
except Exception as exc:
|
|
logging.getLogger(__name__).exception("Error while calling plugin %s" % plugin._identifier)
|
|
if error_callback:
|
|
error_callback(plugin._identifier, plugin, exc)
|
|
|
|
|
|
class PluginSettings(object):
|
|
"""
|
|
The :class:`PluginSettings` class is the interface for plugins to their own or globally defined settings.
|
|
|
|
It provides a couple of convenience methods for directly accessing plugin settings via the regular
|
|
:class:`octoprint.settings.Settings` interfaces as well as means to access plugin specific folder locations.
|
|
|
|
All getter and setter methods will ensure that plugin settings are stored in their correct location within the
|
|
settings structure by modifying the supplied paths accordingly.
|
|
|
|
Arguments:
|
|
settings (Settings): The :class:`~octoprint.settings.Settings` instance on which to operate.
|
|
plugin_key (str): The plugin identifier of the plugin for which to create this instance.
|
|
defaults (dict): The plugin's defaults settings, will be used to determine valid paths within the plugin's
|
|
settings structure
|
|
|
|
.. method:: get(path, merged=False, asdict=False)
|
|
|
|
Retrieves a raw value from the settings for ``path``, optionally merging the raw value with the default settings
|
|
if ``merged`` is set to True.
|
|
|
|
:param path: The path for which to retrieve the value.
|
|
:type path: list, tuple
|
|
:param boolean merged: Whether to merge the returned result with the default settings (True) or not (False,
|
|
default).
|
|
:returns: The retrieved settings value.
|
|
:rtype: object
|
|
|
|
.. method:: get_int(path)
|
|
|
|
Like :func:`get` but tries to convert the retrieved value to ``int``.
|
|
|
|
.. method:: get_float(path)
|
|
|
|
Like :func:`get` but tries to convert the retrieved value to ``float``.
|
|
|
|
.. method:: get_boolean(path)
|
|
|
|
Like :func:`get` but tries to convert the retrieved value to ``boolean``.
|
|
|
|
.. method:: set(path, value, force=False)
|
|
|
|
Sets the raw value on the settings for ``path``.
|
|
|
|
:param path: The path for which to retrieve the value.
|
|
:type path: list, tuple
|
|
:param object value: The value to set.
|
|
:param boolean force: If set to True, the modified configuration will even be written back to disk if
|
|
the value didn't change.
|
|
|
|
.. method:: set_int(path, value, force=False)
|
|
|
|
Like :func:`set` but ensures the value is an ``int`` through attempted conversion before setting it.
|
|
|
|
.. method:: set_float(path, value, force=False)
|
|
|
|
Like :func:`set` but ensures the value is an ``float`` through attempted conversion before setting it.
|
|
|
|
.. method:: set_boolean(path, value, force=False)
|
|
|
|
Like :func:`set` but ensures the value is an ``boolean`` through attempted conversion before setting it.
|
|
"""
|
|
|
|
def __init__(self, settings, plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=None):
|
|
self.settings = settings
|
|
self.plugin_key = plugin_key
|
|
|
|
if defaults is None:
|
|
defaults = dict()
|
|
self.defaults = dict(plugins=dict())
|
|
self.defaults["plugins"][plugin_key] = defaults
|
|
|
|
if get_preprocessors is None:
|
|
get_preprocessors = dict()
|
|
self.get_preprocessors = dict(plugins=dict())
|
|
self.get_preprocessors["plugins"][plugin_key] = get_preprocessors
|
|
|
|
if set_preprocessors is None:
|
|
set_preprocessors = dict()
|
|
self.set_preprocessors = dict(plugins=dict())
|
|
self.set_preprocessors["plugins"][plugin_key] = set_preprocessors
|
|
|
|
def prefix_path(path):
|
|
return ['plugins', self.plugin_key] + path
|
|
|
|
def prefix_path_in_args(args, index=0):
|
|
result = []
|
|
if index == 0:
|
|
result.append(prefix_path(args[0]))
|
|
result.extend(args[1:])
|
|
else:
|
|
args_before = args[:index - 1]
|
|
args_after = args[index + 1:]
|
|
result.extend(args_before)
|
|
result.append(prefix_path(args[index]))
|
|
result.extend(args_after)
|
|
return result
|
|
|
|
def add_getter_kwargs(kwargs):
|
|
if not "defaults" in kwargs:
|
|
kwargs.update(defaults=self.defaults)
|
|
if not "preprocessors" in kwargs:
|
|
kwargs.update(preprocessors=self.get_preprocessors)
|
|
return kwargs
|
|
|
|
def add_setter_kwargs(kwargs):
|
|
if not "defaults" in kwargs:
|
|
kwargs.update(defaults=self.defaults)
|
|
if not "preprocessors" in kwargs:
|
|
kwargs.update(preprocessors=self.set_preprocessors)
|
|
return kwargs
|
|
|
|
self.access_methods = dict(
|
|
get =("get", prefix_path_in_args, add_getter_kwargs),
|
|
get_int =("getInt", prefix_path_in_args, add_getter_kwargs),
|
|
get_float =("getFloat", prefix_path_in_args, add_getter_kwargs),
|
|
get_boolean=("getBoolean", prefix_path_in_args, add_getter_kwargs),
|
|
set =("set", prefix_path_in_args, add_setter_kwargs),
|
|
set_int =("setInt", prefix_path_in_args, add_setter_kwargs),
|
|
set_float =("setFloat", prefix_path_in_args, add_setter_kwargs),
|
|
set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs)
|
|
)
|
|
self.deprecated_access_methods = dict(
|
|
getInt ="get_int",
|
|
getFloat ="get_float",
|
|
getBoolean="get_boolean",
|
|
setInt ="set_int",
|
|
setFloat ="set_float",
|
|
setBoolean="set_boolean"
|
|
)
|
|
|
|
def global_get(self, path, **kwargs):
|
|
"""
|
|
Getter for retrieving settings not managed by the plugin itself from the core settings structure. Use this
|
|
to access global settings outside of your plugin.
|
|
|
|
Directly forwards to :func:`octoprint.settings.Settings.get`.
|
|
"""
|
|
return self.settings.get(path, **kwargs)
|
|
|
|
def global_get_int(self, path, **kwargs):
|
|
"""
|
|
Like :func:`global_get` but directly forwards to :func:`octoprint.settings.Settings.getInt`.
|
|
"""
|
|
return self.settings.getInt(path, **kwargs)
|
|
|
|
def global_get_float(self, path, **kwargs):
|
|
"""
|
|
Like :func:`global_get` but directly forwards to :func:`octoprint.settings.Settings.getFloat`.
|
|
"""
|
|
return self.settings.getFloat(path, **kwargs)
|
|
|
|
def global_get_boolean(self, path, **kwargs):
|
|
"""
|
|
Like :func:`global_get` but directly orwards to :func:`octoprint.settings.Settings.getBoolean`.
|
|
"""
|
|
return self.settings.getBoolean(path, **kwargs)
|
|
|
|
def global_set(self, path, value, **kwargs):
|
|
"""
|
|
Setter for modifying settings not managed by the plugin itself on the core settings structure. Use this
|
|
to modify global settings outside of your plugin.
|
|
|
|
Directly forwards to :func:`octoprint.settings.Settings.set`.
|
|
"""
|
|
self.settings.set(path, value, **kwargs)
|
|
|
|
def global_set_int(self, path, value, **kwargs):
|
|
"""
|
|
Like :func:`global_set` but directly forwards to :func:`octoprint.settings.Settings.setInt`.
|
|
"""
|
|
self.settings.setInt(path, value, **kwargs)
|
|
|
|
def global_set_float(self, path, value, **kwargs):
|
|
"""
|
|
Like :func:`global_set` but directly forwards to :func:`octoprint.settings.Settings.setFloat`.
|
|
"""
|
|
self.settings.setFloat(path, value, **kwargs)
|
|
|
|
def global_set_boolean(self, path, value, **kwargs):
|
|
"""
|
|
Like :func:`global_set` but directly forwards to :func:`octoprint.settings.Settings.setBoolean`.
|
|
"""
|
|
self.settings.setBoolean(path, value, **kwargs)
|
|
|
|
def global_get_basefolder(self, folder_type, **kwargs):
|
|
"""
|
|
Retrieves a globally defined basefolder of the given ``folder_type``. Directly forwards to
|
|
:func:`octoprint.settings.Settings.getBaseFolder`.
|
|
"""
|
|
return self.settings.getBaseFolder(folder_type, **kwargs)
|
|
|
|
def get_plugin_logfile_path(self, postfix=None):
|
|
"""
|
|
Retrieves the path to a logfile specifically for the plugin. If ``postfix`` is not supplied, the logfile
|
|
will be named ``plugin_<plugin identifier>.log`` and located within the configured ``logs`` folder. If a
|
|
postfix is supplied, the name will be ``plugin_<plugin identifier>_<postfix>.log`` at the same location.
|
|
|
|
Plugins may use this for specific logging tasks. For example, a :class:`~octoprint.plugin.SlicingPlugin` might
|
|
want to create a log file for logging the output of the slicing engine itself if some debug flag is set.
|
|
|
|
Arguments:
|
|
postfix (str): Postfix of the logfile for which to create the path. If set, the file name of the log file
|
|
will be ``plugin_<plugin identifier>_<postfix>.log``, if not it will be
|
|
``plugin_<plugin identifier>.log``.
|
|
|
|
Returns:
|
|
str: Absolute path to the log file, directly usable by the plugin.
|
|
"""
|
|
filename = "plugin_" + self.plugin_key
|
|
if postfix is not None:
|
|
filename += "_" + postfix
|
|
filename += ".log"
|
|
return os.path.join(self.settings.getBaseFolder("logs"), filename)
|
|
|
|
def __getattr__(self, item):
|
|
all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys()
|
|
if item in all_access_methods:
|
|
decorator = None
|
|
if item in self.deprecated_access_methods:
|
|
new = self.deprecated_access_methods[item]
|
|
decorator = deprecated("{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2)
|
|
item = new
|
|
|
|
settings_name, args_mapper, kwargs_mapper = self.access_methods[item]
|
|
if hasattr(self.settings, settings_name) and callable(getattr(self.settings, settings_name)):
|
|
orig_func = getattr(self.settings, settings_name)
|
|
if decorator is not None:
|
|
orig_func = decorator(orig_func)
|
|
|
|
def _func(*args, **kwargs):
|
|
return orig_func(*args_mapper(args), **kwargs_mapper(kwargs))
|
|
_func.__name__ = item
|
|
_func.__doc__ = orig_func.__doc__ if "__doc__" in dir(orig_func) else None
|
|
|
|
return _func
|
|
|
|
return getattr(self.settings, item)
|
|
|
|
##~~ deprecated methods follow
|
|
|
|
# TODO: Remove with release of 1.3.0
|
|
|
|
globalGet = deprecated("globalGet has been renamed to global_get",
|
|
includedoc="Replaced by :func:`global_get`",
|
|
since="1.2.0-dev-546")(global_get)
|
|
globalGetInt = deprecated("globalGetInt has been renamed to global_get_int",
|
|
includedoc="Replaced by :func:`global_get_int`",
|
|
since="1.2.0-dev-546")(global_get_int)
|
|
globalGetFloat = deprecated("globalGetFloat has been renamed to global_get_float",
|
|
includedoc="Replaced by :func:`global_get_float`",
|
|
since="1.2.0-dev-546")(global_get_float)
|
|
globalGetBoolean = deprecated("globalGetBoolean has been renamed to global_get_boolean",
|
|
includedoc="Replaced by :func:`global_get_boolean`",
|
|
since="1.2.0-dev-546")(global_get_boolean)
|
|
globalSet = deprecated("globalSet has been renamed to global_set",
|
|
includedoc="Replaced by :func:`global_set`",
|
|
since="1.2.0-dev-546")(global_set)
|
|
globalSetInt = deprecated("globalSetInt has been renamed to global_set_int",
|
|
includedoc="Replaced by :func:`global_set_int`",
|
|
since="1.2.0-dev-546")(global_set_int)
|
|
globalSetFloat = deprecated("globalSetFloat has been renamed to global_set_float",
|
|
includedoc="Replaced by :func:`global_set_float`",
|
|
since="1.2.0-dev-546")(global_set_float)
|
|
globalSetBoolean = deprecated("globalSetBoolean has been renamed to global_set_boolean",
|
|
includedoc="Replaced by :func:`global_set_boolean`",
|
|
since="1.2.0-dev-546")(global_set_boolean)
|
|
globalGetBaseFolder = deprecated("globalGetBaseFolder has been renamed to global_get_basefolder",
|
|
includedoc="Replaced by :func:`global_get_basefolder`",
|
|
since="1.2.0-dev-546")(global_get_basefolder)
|
|
getPluginLogfilePath = deprecated("getPluginLogfilePath has been renamed to get_plugin_logfile_path",
|
|
includedoc="Replaced by :func:`get_plugin_logfile_path`",
|
|
since="1.2.0-dev-546")(get_plugin_logfile_path)
|