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:
parent
79336ca108
commit
8ff0096eb6
24 changed files with 404 additions and 146 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
))
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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__``
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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": {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
16
tests/plugin/_plugins/deprecated_plugin.py
Normal file
16
tests/plugin/_plugins/deprecated_plugin.py
Normal 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()]
|
||||
|
|
@ -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()
|
||||
|
|
@ -9,4 +9,4 @@ class TestSettingsPlugin(octoprint.plugin.SettingsPlugin):
|
|||
|
||||
__plugin_name__ = "Settings Plugin"
|
||||
__plugin_description__ = "Test settings plugin"
|
||||
__plugin_implementations__ = (TestSettingsPlugin(),)
|
||||
__plugin_implementation__ = TestSettingsPlugin()
|
||||
|
|
@ -9,4 +9,4 @@ class TestStartupPlugin(octoprint.plugin.StartupPlugin):
|
|||
|
||||
__plugin_name__ = "Startup Plugin"
|
||||
__plugin_description__ = "Test startup plugin"
|
||||
__plugin_implementations__ = (TestStartupPlugin(),)
|
||||
__plugin_implementation__ = TestStartupPlugin()
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue