diff --git a/CHANGELOG.md b/CHANGELOG.md index 95c09d71..92ce050c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/plugins/concepts.rst b/docs/plugins/concepts.rst index a5405e81..368f51e4 100644 --- a/docs/plugins/concepts.rst +++ b/docs/plugins/concepts.rst @@ -20,7 +20,7 @@ Mixins Plugin mixins are the heart of OctoPrint's plugin system. They are :ref:`special base classes ` 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 `_, +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 `_. +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 + )) \ No newline at end of file diff --git a/docs/plugins/gettingstarted.rst b/docs/plugins/gettingstarted.rst index bac44528..66a22a98 100644 --- a/docs/plugins/gettingstarted.rst +++ b/docs/plugins/gettingstarted.rst @@ -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 ` -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: diff --git a/docs/plugins/infrastructure.rst b/docs/plugins/infrastructure.rst index 52f058e1..94a192c8 100644 --- a/docs/plugins/infrastructure.rst +++ b/docs/plugins/infrastructure.rst @@ -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 ` +``__plugin_implementation__`` + Instance of an implementation of one or more :ref:`plugin mixins ` ``__plugin_hooks__`` Handlers for one or more of the various :ref:`plugin hooks ` ``__plugin_check__`` diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index e4a646ff..068caa2e 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -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)) diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 0bf57c78..96cd50d5 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -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): diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 38f0551d..5b1f831f 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -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") diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 02d61973..fdaff494 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -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//``, 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. diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index 9209ada5..8b74a032 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -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()] \ No newline at end of file +__plugin_implementation__ = CuraPlugin() \ No newline at end of file diff --git a/src/octoprint/plugins/discovery/__init__.py b/src/octoprint/plugins/discovery/__init__.py index 8ded1122..90009281 100644 --- a/src/octoprint/plugins/discovery/__init__.py +++ b/src/octoprint/plugins/discovery/__init__.py @@ -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, diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 289741b5..5d248cda 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -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 diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index b04be2ff..2a625d5d 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -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//") 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 diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 29bf958a..431d8346 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -48,11 +48,14 @@ api.after_request(corsResponseHandler) @api.route("/plugin/", 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/", 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) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 0cf9b74b..ba63ddd1 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -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(): diff --git a/src/octoprint/server/apps/__init__.py b/src/octoprint/server/apps/__init__.py index 47a68cca..e3650d95 100644 --- a/src/octoprint/server/apps/__init__.py +++ b/src/octoprint/server/apps/__init__.py @@ -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() diff --git a/src/octoprint/server/util/sockjs.py b/src/octoprint/server/util/sockjs.py index 186e55b3..67137a99 100644 --- a/src/octoprint/server/util/sockjs.py +++ b/src/octoprint/server/util/sockjs.py @@ -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): diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index baa78816..4ba776a4 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -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": {}, diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index ab86c421..d5c74e2d 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -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 diff --git a/tests/plugin/_plugins/deprecated_plugin.py b/tests/plugin/_plugins/deprecated_plugin.py new file mode 100644 index 00000000..7137d0cb --- /dev/null +++ b/tests/plugin/_plugins/deprecated_plugin.py @@ -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()] \ No newline at end of file diff --git a/tests/plugin/_plugins/mixed_plugin/__init__.py b/tests/plugin/_plugins/mixed_plugin/__init__.py index c8c8996a..aed5a5ed 100644 --- a/tests/plugin/_plugins/mixed_plugin/__init__.py +++ b/tests/plugin/_plugins/mixed_plugin/__init__.py @@ -14,4 +14,4 @@ class TestMixedPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsP __plugin_name__ = "Mixed Plugin" __plugin_description__ = "Test mixed plugin" -__plugin_implementations__ = (TestMixedPlugin(),) \ No newline at end of file +__plugin_implementation__ = TestMixedPlugin() \ No newline at end of file diff --git a/tests/plugin/_plugins/settings_plugin.py b/tests/plugin/_plugins/settings_plugin.py index 01fb6524..f046a3ad 100644 --- a/tests/plugin/_plugins/settings_plugin.py +++ b/tests/plugin/_plugins/settings_plugin.py @@ -9,4 +9,4 @@ class TestSettingsPlugin(octoprint.plugin.SettingsPlugin): __plugin_name__ = "Settings Plugin" __plugin_description__ = "Test settings plugin" -__plugin_implementations__ = (TestSettingsPlugin(),) \ No newline at end of file +__plugin_implementation__ = TestSettingsPlugin() \ No newline at end of file diff --git a/tests/plugin/_plugins/startup_plugin.py b/tests/plugin/_plugins/startup_plugin.py index 7c3202b5..b75ca851 100644 --- a/tests/plugin/_plugins/startup_plugin.py +++ b/tests/plugin/_plugins/startup_plugin.py @@ -9,4 +9,4 @@ class TestStartupPlugin(octoprint.plugin.StartupPlugin): __plugin_name__ = "Startup Plugin" __plugin_description__ = "Test startup plugin" -__plugin_implementations__ = (TestStartupPlugin(),) \ No newline at end of file +__plugin_implementation__ = TestStartupPlugin() \ No newline at end of file diff --git a/tests/plugin/test_core.py b/tests/plugin/test_core.py index 22efe1a9..8a3f037d 100644 --- a/tests/plugin/test_core.py +++ b/tests/plugin/test_core.py @@ -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)) diff --git a/tests/slicing/test_slicingmanager.py b/tests/slicing/test_slicingmanager.py index 1b17a89d..c91b4dd5 100644 --- a/tests/slicing/test_slicingmanager.py +++ b/tests/slicing/test_slicingmanager.py @@ -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