Fix & Docs: Plugins may only have one mixin implementation

Multiple mixins are allowed of course. Allowing multiple implementations lead to too many problems due to plugin names for referring to the APIs of SimpleApiPlugins or the assets of AssetPlugins.

 Hence __plugin_implementations__ has been deprecated in favor of __plugin_implementation__. The plugin subsystem will automatically copy the first implementation from __plugin_implementations__ to __plugin_implementation__ and log a deprecation warning.

 Adjusted documentation accordingly. Also added docs for helpers.
This commit is contained in:
Gina Häußge 2015-03-30 16:50:06 +02:00
parent 79336ca108
commit 8ff0096eb6
24 changed files with 404 additions and 146 deletions

View file

@ -37,7 +37,8 @@
* Controls for adjusting feed and flow rate factor added to Controls ([#362](https://github.com/foosel/OctoPrint/issues/362))
* Custom controls now also support slider controls
* Custom controls now support a row layout
* Users can now define custom GCODE scripts to run upon starting/pausing/resuming/success/failure of a print
* Users can now define custom GCODE scripts to run upon starting/pausing/resuming/success/failure of a print or for
custom controls ([#457](https://github.com/foosel/OctoPrint/issues/457), [#347](https://github.com/foosel/OctoPrint/issues/347))
### Improvements

View file

@ -20,7 +20,7 @@ Mixins
Plugin mixins are the heart of OctoPrint's plugin system. They are :ref:`special base classes <sec-plugins-mixins>`
which are to be subclassed and extended to add functionality to OctoPrint. Plugins declare their instances that
implement one or multiple mixins using the ``__plugin_implementations__`` control property. OctoPrint's plugin core
implement one or multiple mixins using the ``__plugin_implementation__`` control property. OctoPrint's plugin core
collects those from the plugins and offers methods to access them based on the mixin type, which get used at multiple
locations within OctoPrint.
@ -135,7 +135,7 @@ If you want your hook handler to be an instance method of a mixin implementation
need access to instance variables handed to your implementation via mixin invocations), you can get this work
by using a small trick. Instead of defining it directly via ``__plugin_hooks__`` utilize the ``__plugin_init__``
property instead, manually instantiate your implementation instance and then add its hook handler method to the
``__plugin_hooks__`` property and itself to the ``__plugin_implementations__`` property. See the following example.
``__plugin_hooks__`` property and itself to the ``__plugin_implementation__`` property. See the following example.
.. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/custom_action_command.py
:linenos:
@ -153,3 +153,74 @@ property instead, manually instantiate your implementation instance and then add
Helpers
-------
Helpers are methods that plugin can exposed to other plugins in order to make common functionality available on the
system. They are registered with the OctoPrint plugin system through the use of the control property ``__plugin_helpers__``.
An example for providing a couple of helper functions to the system can be found in the
`Discovery Plugin <https://github.com/foosel/OctoPrint/wiki/Plugin:-Discovery>`_,
which provides it's SSDP browsing and Zeroconf browsing and publishing functions as helper methods.
.. code-block:: python
:linenos:
:emphasize-lines: 11-20
:caption: Excerpt from the Discovery Plugin showing the declaration of its exported helpers.
:name: sec-plugin-concepts-helpers-example-export
def __plugin_init__():
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")
plugin = DiscoveryPlugin()
global __plugin_implementation__
__plugin_implementation__ = plugin
global __plugin_helpers__
__plugin_helpers__ = dict(
ssdp_browse=plugin.ssdp_browse
)
if pybonjour:
__plugin_helpers__.update(dict(
zeroconf_browse=plugin.zeroconf_browse,
zeroconf_register=plugin.zeroconf_register,
zeroconf_unregister=plugin.zeroconf_unregister
))
An example of how to use helpers can be found in the `Growl Plugin <https://github.com/OctoPrint/OctoPrint-Growl>`_.
Using :meth:`~octoprint.plugin.code.PluginManager.get_helpers` plugins can retrieve exported helper methods and call
them as (hopefully) documented.
.. code-block:: python
:linenos:
:emphasize-lines: 6-8,20
:caption: Excerpt from the Growl Plugin showing utilization of the helpers published by the Discovery Plugin.
:name: sec-plugin-concepts-helpers-example-usage
def on_after_startup(self):
host = self._settings.get(["hostname"])
port = self._settings.getInt(["port"])
password = self._settings.get(["password"])
helpers = self._plugin_manager.get_helpers("discovery", "zeroconf_browse")
if helpers and "zeroconf_browse" in helpers:
self.zeroconf_browse = helpers["zeroconf_browse"]
self.growl, _ = self._register_growl(host, port, password=password)
# ...
def on_api_get(self, request):
if not self.zeroconf_browse:
return flask.jsonify(dict(
browsing_enabled=False
))
browse_results = self.zeroconf_browse("_gntp._tcp", block=True)
growl_instances = [dict(name=v["name"], host=v["host"], port=v["port"]) for v in browse_results]
return flask.jsonify(dict(
browsing_enabled=True,
growl_instances=growl_instances
))

View file

@ -53,14 +53,14 @@ Apart from being discovered by OctoPrint, our plugin does nothing yet. We want t
__plugin_name__ = "Hello World"
__plugin_version__ = "1.0"
__plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
and restart OctoPrint. You now get this output in the log::
2015-01-27 11:17:10,792 - octoprint.plugins.helloworld - INFO - Hello World!
Neat, isn't it? We added a custom class that subclasses one of OctoPrint's :ref:`plugin mixins <sec-plugins-mixins>`
with :class:`~octoprint.plugin.StartupPlugin` and another control property, ``__plugin_implementations__``, that instantiates
with :class:`~octoprint.plugin.StartupPlugin` and another control property, ``__plugin_implementation__``, that instantiates
our plugin class and tells OctoPrint about it. Taking a look at the documentation of :class:`~octoprint.plugin.StartupPlugin` we see that
this mixin offers two methods that get called by OctoPrint during startup of the server, :func:`~octoprint.plugin.StartupPlugin.on_startup` and
:func:`~octoprint.plugin.StartupPlugin.on_after_startup`. We decided to add our logging output by overriding :func:`~octoprint.plugin.StartupPlugin.on_after_startup`, but we could also have
@ -169,7 +169,7 @@ and ``__plugin_description__``:
def on_after_startup(self):
self._logger.info("Hello World!")
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
and restart OctoPrint::
@ -192,7 +192,7 @@ Our "Hello World" Plugin still gets detected fine, but it's now listed under the
self._logger.info("Hello World!")
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Restart OctoPrint again::
@ -238,7 +238,7 @@ add the :class:`TemplatePlugin` to our ``HelloWorldPlugin`` class:
self._logger.info("Hello World!")
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Next, we'll create a sub folder ``templates`` underneath our ``octoprint_helloworld`` folder, and within that a file
``helloworld_navbar.jinja2`` like so:
@ -304,7 +304,7 @@ Let's take a look at how all that would look in our plugin's ``__init__.py``:
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Restart OctoPrint. You should see something like this::
@ -341,7 +341,7 @@ Adjust your plugin's ``__init__.py`` like this:
return dict(url=self._settings.get(["url"]))
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Also adjust your plugin's ``templates/helloworld_navbar.jinja2`` like this:
@ -451,7 +451,7 @@ again since we don't use that anymore:
]
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Restart OctoPrint and shift-reload your browser. Your link in the navigation bar should still point to the URL we
defined in ``config.yaml`` earlier. Open the "Settings" and click on the new "Hello World" entry that shows up under
@ -550,7 +550,7 @@ like so:
)
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Note how we did not add another entry to the return value of :func:`~octoprint.plugin.TemplatePlugin.get_template_configs`.
Remember how we only added those since we wanted OctoPrint to use existing bindings on our navigation bar and settings
@ -731,7 +731,7 @@ a reference to our CSS file:
)
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
Restart OctoPrint, shift-reload your browser and take a look. Everything should still look like before, but now
OctoPrint linked to our stylesheet and the style information for the ``iframe`` is taken from that instead of
@ -801,7 +801,7 @@ Then adjust our returned assets to include our LESS file as well:
)
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
and enable LESS mode by adjusting one of OctoPrint's ``devel`` flags via the ``config.yaml`` file:

View file

@ -21,8 +21,8 @@ Control Properties
provided.
``__plugin_license__``
License of your plugin, optional, overrides the license specified in ``setup.py`` if provided.
``__plugin_implementations__``
Instances of one or more of the various :ref:`plugin mixins <sec-plugins-mixins>`
``__plugin_implementation__``
Instance of an implementation of one or more :ref:`plugin mixins <sec-plugins-mixins>`
``__plugin_hooks__``
Handlers for one or more of the various :ref:`plugin hooks <sec-plugins-hooks>`
``__plugin_check__``

View file

@ -257,11 +257,11 @@ class FileManager(object):
if progress_int:
def call_plugins(slicer, source_location, source_path, dest_location, dest_path, progress):
for name, plugin in self._progress_plugins.items():
for plugin in self._progress_plugins:
try:
plugin.on_slicing_progress(slicer, source_location, source_path, dest_location, dest_path, progress)
except:
self._logger.exception("Exception while sending slicing progress to plugin %s" % name)
self._logger.exception("Exception while sending slicing progress to plugin %s" % plugin._identifier)
import threading
thread = threading.Thread(target=call_plugins, args=(slicer, source_location, source_path, dest_location, dest_path, progress_int))

View file

@ -164,16 +164,16 @@ def call_plugin(types, method, args=None, kwargs=None, callback=None, error_call
kwargs = dict()
plugins = plugin_manager().get_implementations(*types)
for name, plugin in plugins.items():
for plugin in plugins:
if hasattr(plugin, method):
try:
result = getattr(plugin, method)(*args, **kwargs)
if callback:
callback(name, plugin, result)
callback(plugin._identifier, plugin, result)
except Exception as exc:
logging.getLogger(__name__).exception("Error while calling plugin %s" % name)
logging.getLogger(__name__).exception("Error while calling plugin %s" % plugin._identifier)
if error_callback:
error_callback(name, plugin, exc)
error_callback(plugin._identifier, plugin, exc)
class PluginSettings(object):

View file

@ -70,8 +70,20 @@ class PluginInfo(object):
attr_hooks = '__plugin_hooks__'
""" Module attribute from which to retrieve the plugin's provided hooks. """
attr_implementation = '__plugin_implementation__'
""" Module attribute from which to retrieve the plugin's provided mixin implementation. """
attr_implementations = '__plugin_implementations__'
""" Module attribute from which to retrieve the plugin's provided implementations. """
"""
Module attribute from which to retrieve the plugin's provided implementations.
This deprecated attribute will only be used if a plugin does not yet offer :attr:`attr_implementation`. Only the
first entry will be evaluated.
.. deprecated:: 1.2.0-dev-694
Use :attr:`attr_implementation` instead.
"""
attr_helpers = '__plugin_helpers__'
""" Module attribute from which to retrieve the plugin's provided helpers. """
@ -97,6 +109,25 @@ class PluginInfo(object):
self._url = url
self._license = license
self._validate()
def _validate(self):
# if the plugin still uses __plugin_implementations__, log a deprecation warning and put the first
# item into __plugin_implementation__
if hasattr(self.instance, self.__class__.attr_implementations):
if not hasattr(self.instance, self.__class__.attr_implementation):
# deprecation warning
import warnings
warnings.warn("{name} uses deprecated control property __plugin_implementations__, use __plugin_implementation__ instead - only the first implementation of {name} will be recognized".format(name=self.key), DeprecationWarning)
# put first item into __plugin_implementation__
implementations = getattr(self.instance, self.__class__.attr_implementations)
if len(implementations) > 0:
setattr(self.instance, self.__class__.attr_implementation, implementations[0])
# delete __plugin_implementations__
delattr(self.instance, self.__class__.attr_implementations)
def __str__(self):
if self.version:
return "{name} ({version})".format(name=self.name, version=self.version)
@ -134,24 +165,23 @@ class PluginInfo(object):
return None
return self.hooks[hook]
def get_implementations(self, *types):
def get_implementation(self, *types):
"""
Arguments:
types (list): List of :class:`Plugin` sub classes all returned implementations need to implement.
Returns:
~__builtin__.set: The plugin's implementations matching all of the requested ``types``. Might be empty.
object: The plugin's implementation if it matches all of the requested ``types``, None otherwise.
"""
result = set()
for implementation in self.implementations:
matches_all = True
for type in types:
if not isinstance(implementation, type):
matches_all = False
if matches_all:
result.add(implementation)
return result
if not self.implementation:
return None
for t in types:
if not isinstance(self.implementation, t):
return None
return self.implementation
@property
def name(self):
@ -232,7 +262,7 @@ class PluginInfo(object):
return self._get_instance_attribute(self.__class__.attr_hooks, default={})
@property
def implementations(self):
def implementation(self):
"""
Implementations provided by the plugin. Will be taken from the implementations attribute of the plugin module
as defined in :attr:`attr_implementations` if available, otherwise an empty list is returned.
@ -240,7 +270,7 @@ class PluginInfo(object):
Returns:
list: Implementations provided by the plugin.
"""
return self._get_instance_attribute(self.__class__.attr_implementations, default=[])
return self._get_instance_attribute(self.__class__.attr_implementation, default=None)
@property
def helpers(self):
@ -310,7 +340,7 @@ class PluginManager(object):
self.plugins = dict()
self.plugin_hooks = defaultdict(list)
self.plugin_implementations = defaultdict(set)
self.plugin_implementations = dict()
self.plugin_implementations_by_type = defaultdict(list)
self.disabled_plugins = dict()
@ -460,12 +490,13 @@ class PluginManager(object):
# evaluate registered implementations
for plugin_type in self.plugin_types:
implementations = plugin.get_implementations(plugin_type)
self.plugin_implementations_by_type[plugin_type] += ( (name, implementation) for implementation in implementations )
implementation = plugin.get_implementation(plugin_type)
if implementation is not None:
self.plugin_implementations_by_type[plugin_type].append((name, implementation))
plugin_implementations = plugin.get_implementations()
if len(plugin_implementations):
self.plugin_implementations[name].update(plugin_implementations)
plugin_implementation = plugin.get_implementation()
if plugin_implementation is not None:
self.plugin_implementations[name] = plugin_implementation
except:
self.logger.exception("There was an error loading plugin %s" % name)
@ -474,7 +505,7 @@ class PluginManager(object):
else:
self.logger.info("Found {count} plugin(s) providing {implementations} mixin implementations, {hooks} hook handlers".format(
count=len(self.plugins) + len(self.disabled_plugins),
implementations=sum(map(lambda x: len(x), self.plugin_implementations.values())),
implementations=len(self.plugin_implementations),
hooks=sum(map(lambda x: len(x), self.plugin_hooks.values()))
))
@ -484,40 +515,40 @@ class PluginManager(object):
if additional_inject_factories is None:
additional_inject_factories = []
for name, implementations in self.plugin_implementations.items():
for name, implementation in self.plugin_implementations.items():
plugin = self.plugins[name]
for implementation in implementations:
try:
kwargs = dict(additional_injects)
try:
kwargs = dict(additional_injects)
kwargs.update(dict(
identifier=name,
plugin_name=plugin.name,
plugin_version=plugin.version,
basefolder=os.path.realpath(plugin.location),
logger=logging.getLogger(self.logging_prefix + name),
))
kwargs.update(dict(
identifier=name,
plugin_name=plugin.name,
plugin_version=plugin.version,
basefolder=os.path.realpath(plugin.location),
logger=logging.getLogger(self.logging_prefix + name),
))
# inject the additional_injects
for arg, value in kwargs.items():
setattr(implementation, "_" + arg, value)
# inject the additional_injects
for arg, value in kwargs.items():
setattr(implementation, "_" + arg, value)
# inject any injects produced in the additional_inject_factories
for factory in additional_inject_factories:
try:
return_value = factory(name, implementation)
except:
self.logger.exception("Exception while executing injection factory %r" % factory)
else:
if return_value is not None:
if isinstance(return_value, dict):
for arg, value in return_value.items():
setattr(implementation, "_" + arg, value)
# inject any injects produced in the additional_inject_factories
for factory in additional_inject_factories:
try:
return_value = factory(name, implementation)
except:
self.logger.exception("Exception while executing injection factory %r" % factory)
else:
if return_value is not None:
if isinstance(return_value, dict):
for arg, value in return_value.items():
setattr(implementation, "_" + arg, value)
implementation.initialize()
except:
self.logger.exception("Exception while initializing plugin")
# TODO disable plugin!
# allow implementations to be disabled here if False is returned
implementation.initialize()
except:
self.logger.exception("Exception while initializing plugin")
# TODO disable plugin!
self.logger.debug("Initialized {count} plugin mixin implementation(s)".format(count=len(self.plugin_implementations)))
@ -539,17 +570,72 @@ class PluginManager(object):
)
)))
def get_plugin(self, name):
if not name in self.plugins:
return None
return self.plugins[name].instance
def get_plugin(self, identifier, require_enabled=True):
"""
Retrieves the module of the plugin identified by ``identifier``. If the plugin is not registered or disabled and
``required_enabled`` is True (the default) None will be returned.
Arguments:
identifier (str): The identifier of the plugin to retrieve.
require_enabled (boolean): Whether to only return the plugin if is enabled (True, default) or also if it's
disabled.
Returns:
module: The requested plugin module or None
"""
plugin_info = self.get_plugin_info(identifier, require_enabled=require_enabled)
if plugin_info is not None:
return plugin_info.instance
return None
def get_plugin_info(self, identifier, require_enabled=True):
"""
Retrieves the :class:`PluginInfo` instance identified by ``identifier``. If the plugin is not registered or
disabled and ``required_enabled`` is True (the default) None will be returned.
Arguments:
identifier (str): The identifier of the plugin to retrieve.
require_enabled (boolean): Whether to only return the plugin if is enabled (True, default) or also if it's
disabled.
Returns:
~.PluginInfo: The requested :class:`PluginInfo` or None
"""
if identifier in self.plugins:
return self.plugins[identifier]
elif not require_enabled and identifier in self.disabled_plugins:
return self.disabled_plugins[identifier]
return None
def get_hooks(self, hook):
"""
Retrieves all registered handlers for the specified hook.
Arguments:
hook (str): The hook for which to retrieve the handlers.
Returns:
dict: A dict containing all registered handlers mapped by their plugin's identifier.
"""
if not hook in self.plugin_hooks:
return dict()
return {hook[0]: hook[1] for hook in self.plugin_hooks[hook]}
def get_implementations(self, *types):
"""
Get all mixin implementations that implement *all* of the provided ``types``.
Arguments:
types (one or more type): The types a mixin implementation needs to implement in order to be returned.
Returns:
list: A list of all found implementations
"""
result = None
for t in types:
@ -561,9 +647,40 @@ class PluginManager(object):
if result is None:
return dict()
return {impl[0]: impl[1] for impl in result}
return [impl[1] for impl in result]
def get_filtered_implementations(self, f, *types):
"""
Get all mixin implementation that implementat *all* of the provided ``types`` and match the provided filter `f`.
Arguments:
f (callable): A filter function returning True for implementations to return and False for those to exclude.
types (one or more type): The types a mixin implementation needs to implement in order to be returned.
Returns:
list: A list of all found and matching implementations.
"""
assert callable(f)
implementations = self.get_implementations(*types)
return filter(f, implementations)
def get_helpers(self, name, *helpers):
"""
Retrieves the named ``helpers`` for the plugin with identifier ``name``.
If the plugin is not available, returns None. Otherwise returns a :class:`dict` with the requested plugin
helper names mapped to the method - if a helper could not be resolved, it will be missing from the dict.
Arguments:
name (str): Identifier of the plugin for which to look up the ``helpers``.
helpers (one or more str): Identifiers of the helpers of plugin ``name`` to return.
Returns:
dict: A dictionary of all resolved helpers, mapped by their identifiers, or None if the plugin was not
registered with the system.
"""
if not name in self.plugins:
return None
plugin = self.plugins[name]
@ -574,17 +691,35 @@ class PluginManager(object):
else:
return all_helpers
def register_client(self, client):
def register_message_receiver(self, client):
"""
Registers a ``client`` for receiving plugin messages. The ``client`` needs to be a callable accepting two
input arguments, ``plugin`` (the sending plugin's identifier) and ``data`` (the message itself).
"""
if client is None:
return
self.registered_clients.append(client)
def unregister_client(self, client):
def unregister_message_receiver(self, client):
"""
Unregisters a ``client`` for receiving plugin messages.
"""
self.registered_clients.remove(client)
def send_plugin_message(self, plugin, data):
"""
Sends ``data`` in the name of ``plugin`` to all currently registered message receivers by invoking them
with the two arguments.
Arguments:
plugin (str): The sending plugin's identifier.
data (object): The message.
"""
for client in self.registered_clients:
try: client.sendPluginMessage(plugin, data)
try: client(plugin, data)
except: self.logger.exception("Exception while sending plugin data to client")

View file

@ -522,7 +522,7 @@ class SimpleApiPlugin(OctoPrintPlugin):
def on_api_get(self, request):
return flask.jsonify(foo="bar")
__plugin_implementations__ = [MySimpleApiPlugin()]
__plugin_implementation__ = MySimpleApiPlugin()
Our plugin defines two commands, ``command1`` with no mandatory parameters and ``command2`` with one
@ -635,7 +635,7 @@ class BlueprintPlugin(OctoPrintPlugin):
return flask.make_response("Expected a text to echo back.", 400)
return flask.request.values["text"]
__plugin_implementations__ = [MyBlueprintPlugin()]
__plugin_implementation__ = MyBlueprintPlugin()
Your blueprint will be published by OctoPrint under the base URL ``/plugin/<plugin identifier>/``, so the above
example of a plugin with the identifier "myblueprintplugin" would be reachable under
@ -761,7 +761,7 @@ class SettingsPlugin(OctoPrintPlugin):
some_flag = self._settings.get_boolean(["sub", "some_flag"])
self._logger.info("some_setting = {some_setting}, some_value = {some_value}, sub.some_flag = {some_flag}".format(**locals())
__plugin_implementations__ = [MySettingsPlugin()]
__plugin_implementation__ = MySettingsPlugin()
Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the
default implementations do not fit your requirements.

View file

@ -420,4 +420,4 @@ __plugin_author__ = "Gina Häußge"
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
__plugin_license__ = "AGPLv3"
__plugin_implementations__ = [CuraPlugin()]
__plugin_implementation__ = CuraPlugin()

View file

@ -33,20 +33,20 @@ def __plugin_init__():
# no pybonjour available, we can't use that
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available")
discovery_plugin = DiscoveryPlugin()
plugin = DiscoveryPlugin()
global __plugin_implementations__
__plugin_implementations__ = [discovery_plugin]
global __plugin_implementation__
__plugin_implementation__ = plugin
global __plugin_helpers__
__plugin_helpers__ = dict(
ssdp_browse=discovery_plugin.ssdp_browse
ssdp_browse=plugin.ssdp_browse
)
if pybonjour:
__plugin_helpers__.update(dict(
zeroconf_browse=discovery_plugin.zeroconf_browse,
zeroconf_register=discovery_plugin.zeroconf_register,
zeroconf_unregister=discovery_plugin.zeroconf_unregister
zeroconf_browse=plugin.zeroconf_browse,
zeroconf_register=plugin.zeroconf_register,
zeroconf_unregister=plugin.zeroconf_unregister
))
class DiscoveryPlugin(octoprint.plugin.StartupPlugin,

View file

@ -172,11 +172,11 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
filename = self._selectedFile["filename"]
def call_plugins(storage, filename, progress):
for name, plugin in self._progressPlugins.items():
for plugin in self._progressPlugins:
try:
plugin.on_print_progress(storage, filename, progress)
except:
self._logger.exception("Exception while sending print progress to plugin %s" % name)
self._logger.exception("Exception while sending print progress to plugin %s" % plugin._identifier)
thread = threading.Thread(target=call_plugins, args=(storage, filename, progress))
thread.daemon = False

View file

@ -175,7 +175,8 @@ def index():
assets["stylesheets"].append(("css", url_for('static', filename='css/octoprint.css')))
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
for name, implementation in asset_plugins.items():
for implementation in asset_plugins:
name = implementation._identifier
all_assets = implementation.get_assets()
if "js" in all_assets:
@ -285,8 +286,11 @@ def index():
)
plugin_vars = dict()
plugin_names = template_plugins.keys()
for name, implementation in template_plugins.items():
plugin_names = set()
for implementation in template_plugins:
name = implementation._identifier
plugin_names.add(name)
vars = implementation.get_template_vars()
if not isinstance(vars, dict):
vars = dict()
@ -458,11 +462,15 @@ def robotsTxt():
@app.route("/plugin_assets/<string:name>/<path:filename>")
def plugin_assets(name, filename):
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
asset_plugins = pluginManager.get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.AssetPlugin)
if not name in asset_plugins:
if not asset_plugins:
return make_response("Asset not found", 404)
asset_plugin = asset_plugins[name]
if len(asset_plugins) > 1:
return make_response("More than one asset provider for {name}, can't proceed".format(name=name), 500)
asset_plugin = asset_plugins[0]
asset_folder = asset_plugin.get_asset_folder()
if asset_folder is None:
return make_response("Asset not found", 404)
@ -598,7 +606,7 @@ class Server():
# configure additional template folders for jinja2
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
additional_template_folders = []
for plugin in template_plugins.values():
for plugin in template_plugins:
folder = plugin.get_template_folder()
if folder is not None:
additional_template_folders.append(plugin.get_template_folder())
@ -671,7 +679,8 @@ class Server():
# also register any blueprints defined in BlueprintPlugins
blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin)
for name, plugin in blueprint_plugins.items():
for plugin in blueprint_plugins:
name = plugin._identifier
blueprint = plugin.get_blueprint()
if blueprint is None:
continue

View file

@ -48,11 +48,14 @@ api.after_request(corsResponseHandler)
@api.route("/plugin/<string:name>", methods=["GET"])
def pluginData(name):
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
if not name in api_plugins:
api_plugins = octoprint.plugin.plugin_manager().get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.SimpleApiPlugin)
if not api_plugins:
return make_response("Not found", 404)
api_plugin = api_plugins[name]
if len(api_plugins) > 1:
return make_response("More than one api provider registered for {name}, can't proceed".format(name=name), 500)
api_plugin = api_plugins[0]
response = api_plugin.on_api_get(request)
if response is not None:
@ -64,11 +67,15 @@ def pluginData(name):
@api.route("/plugin/<string:name>", methods=["POST"])
@restricted_access
def pluginCommand(name):
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
if not name in api_plugins:
api_plugins = octoprint.plugin.plugin_manager().get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.SimpleApiPlugin)
if not api_plugins:
return make_response("Not found", 404)
api_plugin = api_plugins[name]
if len(api_plugins) > 1:
return make_response("More than one api provider registered for {name}, can't proceed".format(name=name), 500)
api_plugin = api_plugins[0]
valid_commands = api_plugin.get_api_commands()
if valid_commands is None:
return make_response("Method not allowed", 405)

View file

@ -224,9 +224,10 @@ def setSettings():
s.saveScript("gcode", name, script.replace("\r\n", "\n").replace("\r", "\n"))
if "plugins" in data:
for name, plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin).items():
if name in data["plugins"]:
plugin.on_settings_save(data["plugins"][name])
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin).items():
plugin_id = plugin._identifer
if plugin_id in data["plugins"]:
plugin.on_settings_save(data["plugins"][plugin_id])
if s.save():

View file

@ -87,7 +87,7 @@ def _get_registered_apps():
apps[app]["enabled"] = True
app_plugins = octoprint.server.pluginManager.get_implementations(octoprint.plugin.AppPlugin)
for name, plugin in app_plugins.items():
for plugin in app_plugins:
additional_apps = plugin.get_additional_apps()
any_version_enabled = dict()

View file

@ -54,7 +54,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
self._printer.register_callback(self)
self._fileManager.register_slicingprogress_callback(self)
octoprint.timelapse.registerCallback(self)
self._pluginManager.register_client(self)
self._pluginManager.register_message_receiver(self.on_plugin_message)
self._eventManager.fire(Events.CLIENT_OPENED, {"remoteAddress": self._remoteAddress})
for event in octoprint.events.all_events():
@ -67,7 +67,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
self._printer.unregister_callback(self)
self._fileManager.unregister_slicingprogress_callback(self)
octoprint.timelapse.unregisterCallback(self)
self._pluginManager.unregister_client(self)
self._pluginManager.unregister_message_receiver(self.on_plugin_message)
self._eventManager.fire(Events.CLIENT_CLOSED, {"remoteAddress": self._remoteAddress})
for event in octoprint.events.all_events():
@ -119,7 +119,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
dict(slicer=slicer, source_location=source_location, source_path=source_path, dest_location=dest_location, dest_path=dest_path, progress=progress)
)
def sendPluginMessage(self, plugin, data):
def on_plugin_message(self, plugin, data):
self._emit("plugin", dict(plugin=plugin, data=data))
def on_printer_add_log(self, data):

View file

@ -221,7 +221,7 @@ default_settings = {
"apps": {}
},
"terminalFilters": [
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok T\d*:)" },
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok (B|T\d*):)" },
{ "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" }
],
"plugins": {},

View file

@ -121,7 +121,7 @@ class SlicingManager(object):
available slicers.
"""
plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
for name, plugin in plugins.items():
for plugin in plugins:
self._slicers[plugin.get_slicer_properties()["type"]] = plugin
@property

View file

@ -0,0 +1,16 @@
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class TestDeprecatedAssetPlugin(octoprint.plugin.AssetPlugin):
pass
class TestSecondaryDeprecatedAssetPlugin(octoprint.plugin.AssetPlugin):
pass
__plugin_name__ = "Deprecated Plugin"
__plugin_description__ = "Test deprecated plugin"
__plugin_implementations__ = [TestDeprecatedAssetPlugin(), TestSecondaryDeprecatedAssetPlugin()]

View file

@ -14,4 +14,4 @@ class TestMixedPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsP
__plugin_name__ = "Mixed Plugin"
__plugin_description__ = "Test mixed plugin"
__plugin_implementations__ = (TestMixedPlugin(),)
__plugin_implementation__ = TestMixedPlugin()

View file

@ -9,4 +9,4 @@ class TestSettingsPlugin(octoprint.plugin.SettingsPlugin):
__plugin_name__ = "Settings Plugin"
__plugin_description__ = "Test settings plugin"
__plugin_implementations__ = (TestSettingsPlugin(),)
__plugin_implementation__ = TestSettingsPlugin()

View file

@ -9,4 +9,4 @@ class TestStartupPlugin(octoprint.plugin.StartupPlugin):
__plugin_name__ = "Startup Plugin"
__plugin_description__ = "Test startup plugin"
__plugin_implementations__ = (TestStartupPlugin(),)
__plugin_implementation__ = TestStartupPlugin()

View file

@ -17,25 +17,33 @@ class PluginTestCase(unittest.TestCase):
self.plugin_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "_plugins")
plugin_folders = [self.plugin_folder]
plugin_types = [octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin]
plugin_types = [octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.AssetPlugin]
plugin_entry_points = None
self.plugin_manager = octoprint.plugin.core.PluginManager(plugin_folders, plugin_types, plugin_entry_points, plugin_disabled_list=[], logging_prefix="logging_prefix.")
self.plugin_manager.initialize_implementations()
def test_plugin_loading(self):
self.assertEquals(4, len(self.plugin_manager.plugins))
self.assertEquals(5, len(self.plugin_manager.plugins))
self.assertEquals(1, len(self.plugin_manager.plugin_hooks))
self.assertEquals(3, len(self.plugin_manager.plugin_implementations))
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type))
self.assertEquals(4, len(self.plugin_manager.plugin_implementations))
self.assertEquals(3, len(self.plugin_manager.plugin_implementations_by_type))
# hook_plugin
self.assertTrue("octoprint.core.startup" in self.plugin_manager.plugin_hooks)
self.assertEquals(1, len(self.plugin_manager.plugin_hooks["octoprint.core.startup"]))
# TestStartupPlugin & TestMixedPlugin
self.assertTrue(octoprint.plugin.StartupPlugin in self.plugin_manager.plugin_implementations_by_type)
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.StartupPlugin]))
# TestSettingsPlugin & TestMixedPlugin
self.assertTrue(octoprint.plugin.SettingsPlugin in self.plugin_manager.plugin_implementations_by_type)
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.SettingsPlugin]))
# TestDeprecatedAssetPlugin, NOT TestSecondaryDeprecatedAssetPlugin
self.assertTrue(octoprint.plugin.AssetPlugin in self.plugin_manager.plugin_implementations_by_type)
self.assertEquals(1, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.AssetPlugin]))
def test_plugin_initializing(self):
def test_factory(name, implementation):
@ -55,11 +63,8 @@ class PluginTestCase(unittest.TestCase):
)
all_implementations = self.plugin_manager.plugin_implementations
self.assertEquals(3, len(all_implementations))
for name, implementations in all_implementations.items():
self.assertEquals(1, len(implementations))
impl = implementations.pop()
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]
@ -97,6 +102,14 @@ class PluginTestCase(unittest.TestCase):
plugin = self.plugin_manager.get_plugin("unknown_plugin")
self.assertIsNone(plugin)
def test_get_plugin_info(self):
plugin_info = self.plugin_manager.get_plugin_info("hook_plugin")
self.assertIsNotNone(plugin_info)
self.assertEquals("Hook Plugin", plugin_info.name)
plugin_info = self.plugin_manager.get_plugin_info("unknown_plugin")
self.assertIsNone(plugin_info)
def test_get_hooks(self):
hooks = self.plugin_manager.get_hooks("octoprint.core.startup")
self.assertEquals(1, len(hooks))
@ -108,44 +121,49 @@ class PluginTestCase(unittest.TestCase):
def test_get_implementation(self):
implementations = self.plugin_manager.get_implementations(octoprint.plugin.StartupPlugin)
self.assertEquals(2, len(implementations))
self.assertTrue('startup_plugin' in implementations)
self.assertTrue('mixed_plugin' in implementations)
self.assertEquals(2, len(implementations)) # startup_plugin, mixed_plugin
implementations = self.plugin_manager.get_implementations(octoprint.plugin.SettingsPlugin)
self.assertEquals(2, len(implementations))
self.assertTrue('settings_plugin' in implementations)
self.assertTrue('mixed_plugin' in implementations)
self.assertEquals(2, len(implementations)) # settings_plugin, mixed_plugin
implementations = self.plugin_manager.get_implementations(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin)
self.assertEquals(1, len(implementations))
self.assertTrue('mixed_plugin' in implementations)
self.assertEquals(1, len(implementations)) # mixed_plugin
implementations = self.plugin_manager.get_implementations(octoprint.plugin.AssetPlugin)
self.assertEquals(1, len(implementations)) # deprecated_plugin, but only first implementation!
def test_client_registration(self):
client = mock.Mock()
def test_client(*args, **kwargs):
pass
self.assertEquals(0, len(self.plugin_manager.registered_clients))
self.plugin_manager.register_client(client)
self.plugin_manager.register_message_receiver(test_client)
self.assertEquals(1, len(self.plugin_manager.registered_clients))
self.assertIn(client, self.plugin_manager.registered_clients)
self.assertIn(test_client, self.plugin_manager.registered_clients)
self.plugin_manager.unregister_client(client)
self.plugin_manager.unregister_message_receiver(test_client)
self.assertEquals(0, len(self.plugin_manager.registered_clients))
self.assertNotIn(client, self.plugin_manager.registered_clients)
self.assertNotIn(test_client, self.plugin_manager.registered_clients)
def test_send_plugin_message(self):
client1 = mock.Mock()
client2 = mock.Mock()
self.plugin_manager.register_client(client1)
self.plugin_manager.register_client(client2)
self.plugin_manager.register_message_receiver(client1.on_plugin_message)
self.plugin_manager.register_message_receiver(client2.on_plugin_message)
plugin = "some plugin"
data = "some data"
self.plugin_manager.send_plugin_message(plugin, data)
client1.sendPluginMessage.assert_called_once_with(plugin, data)
client2.sendPluginMessage.assert_called_once_with(plugin, data)
client1.on_plugin_message.assert_called_once_with(plugin, data)
client2.on_plugin_message.assert_called_once_with(plugin, data)
def test_validate_plugin(self):
self.assertTrue("deprecated_plugin" in self.plugin_manager.plugins)
plugin = self.plugin_manager.plugins["deprecated_plugin"]
self.assertTrue(hasattr(plugin.instance, plugin.__class__.attr_implementation))
self.assertFalse(hasattr(plugin.instance, plugin.__class__.attr_implementations))

View file

@ -50,7 +50,7 @@ class TestSlicingManager(unittest.TestCase):
def get_implementations(*types):
import octoprint.plugin
if octoprint.plugin.SlicerPlugin in types:
return dict(("slicer_" + plugin.get_slicer_properties()["type"], plugin) for plugin in plugins)
return plugins
return dict()
self.plugin_manager.return_value.get_implementations.side_effect = get_implementations