From 79336ca10810696fb3fbff2d896223ffb51bd17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 30 Mar 2015 13:15:52 +0200 Subject: [PATCH 1/8] Fix: Improved handling of data coming in from the printer Hopefully closes #829 --- src/octoprint/util/__init__.py | 6 +++++- src/octoprint/util/comm.py | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index c16dbeef..ea4012a3 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -389,7 +389,11 @@ def silent_remove(file): def sanitize_ascii(line): - return unicode(line, 'ascii', 'replace').encode('ascii', 'replace').rstrip() + if not isinstance(line, basestring): + raise ValueError("Expected either str or unicode but got {} instead".format(line.__class__.__name__ if line is not None else None)) + if isinstance(line, str): + line = unicode(line, 'ascii', 'replace') + return line.encode('ascii', 'replace').rstrip() def filter_non_ascii(line): diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index ab8cbea4..4062e743 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -1250,7 +1250,13 @@ class MachineCom(object): if ret == '': #self._log("Recv: TIMEOUT") return '' - self._log("Recv: %s" % sanitize_ascii(ret)) + + try: + self._log("Recv: %s" % sanitize_ascii(ret)) + except ValueError as e: + self._log("WARN: While reading last line: %s" % e) + self._log("Recv: %r" % ret) + return ret def _getNext(self): From 8ff0096eb61960fd23285a97b7d34178592ea033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 30 Mar 2015 16:50:06 +0200 Subject: [PATCH 2/8] 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. --- CHANGELOG.md | 3 +- docs/plugins/concepts.rst | 75 +++++- docs/plugins/gettingstarted.rst | 22 +- docs/plugins/infrastructure.rst | 4 +- src/octoprint/filemanager/__init__.py | 4 +- src/octoprint/plugin/__init__.py | 8 +- src/octoprint/plugin/core.py | 251 ++++++++++++++---- src/octoprint/plugin/types.py | 6 +- src/octoprint/plugins/cura/__init__.py | 2 +- src/octoprint/plugins/discovery/__init__.py | 14 +- src/octoprint/printer/standard.py | 4 +- src/octoprint/server/__init__.py | 25 +- src/octoprint/server/api/__init__.py | 19 +- src/octoprint/server/api/settings.py | 7 +- src/octoprint/server/apps/__init__.py | 2 +- src/octoprint/server/util/sockjs.py | 6 +- src/octoprint/settings.py | 2 +- src/octoprint/slicing/__init__.py | 2 +- tests/plugin/_plugins/deprecated_plugin.py | 16 ++ .../plugin/_plugins/mixed_plugin/__init__.py | 2 +- tests/plugin/_plugins/settings_plugin.py | 2 +- tests/plugin/_plugins/startup_plugin.py | 2 +- tests/plugin/test_core.py | 70 +++-- tests/slicing/test_slicingmanager.py | 2 +- 24 files changed, 404 insertions(+), 146 deletions(-) create mode 100644 tests/plugin/_plugins/deprecated_plugin.py 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 From 86cd16278401ee4dc540dcdb8c1269f4e9d2bc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 30 Mar 2015 22:03:36 +0200 Subject: [PATCH 3/8] Fix: get_implementations now returns a list instead of a dict --- src/octoprint/server/api/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index ba63ddd1..f290d61a 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -224,7 +224,7 @@ def setSettings(): s.saveScript("gcode", name, script.replace("\r\n", "\n").replace("\r", "\n")) if "plugins" in data: - for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin).items(): + for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin): plugin_id = plugin._identifer if plugin_id in data["plugins"]: plugin.on_settings_save(data["plugins"][plugin_id]) From 2d54ab5fcfaf303f5591920bef5302b7a066939e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 30 Mar 2015 22:08:10 +0200 Subject: [PATCH 4/8] Fix: identifer => identifier (is it really that hard to type foosel?) --- src/octoprint/plugin/__init__.py | 2 +- src/octoprint/server/api/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 96cd50d5..5b2bdea0 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -188,7 +188,7 @@ class PluginSettings(object): Arguments: settings (Settings): The :class:`~octoprint.settings.Settings` instance on which to operate. - plugin_key (str): The plugin identifer of the plugin for which to create this instance. + plugin_key (str): The plugin identifier of the plugin for which to create this instance. defaults (dict): The plugin's defaults settings, will be used to determine valid paths within the plugin's settings structure diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index f290d61a..ce838257 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -225,7 +225,7 @@ def setSettings(): if "plugins" in data: for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin): - plugin_id = plugin._identifer + plugin_id = plugin._identifier if plugin_id in data["plugins"]: plugin.on_settings_save(data["plugins"][plugin_id]) From c9f54765883836386c008584f65c9250644be208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 31 Mar 2015 18:20:44 +0200 Subject: [PATCH 5/8] Settings now allow providing a custom "config" dict to work on Can be used to always retrieve the defaults (by providing an empty config dict) or to utilize the get method with other settings than the system settings. --- src/octoprint/settings.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 4ba776a4..5d3d5793 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -757,13 +757,14 @@ class Settings(object): #~~ getter - def get(self, path, asdict=False, defaults=None, preprocessors=None, merged=False): + def get(self, path, asdict=False, config=None, defaults=None, preprocessors=None, merged=False): import octoprint.util as util if len(path) == 0: return None - config = self._config + if config is None: + config = self._config if defaults is None: defaults = default_settings if preprocessors is None: @@ -820,7 +821,7 @@ class Settings(object): else: return results - def getInt(self, path, defaults=None, preprocessors=None): + def getInt(self, path, config=None, defaults=None, preprocessors=None): value = self.get(path, defaults=defaults, preprocessors=preprocessors) if value is None: return None @@ -831,8 +832,8 @@ class Settings(object): self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path)) return None - def getFloat(self, path, defaults=None, preprocessors=None): - value = self.get(path, defaults=defaults, preprocessors=preprocessors) + def getFloat(self, path, config=None, defaults=None, preprocessors=None): + value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors) if value is None: return None @@ -842,8 +843,8 @@ class Settings(object): self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path)) return None - def getBoolean(self, path, defaults=None, preprocessors=None): - value = self.get(path, defaults=defaults, preprocessors=preprocessors) + def getBoolean(self, path, config=None, defaults=None, preprocessors=None): + value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors) if value is None: return None if isinstance(value, bool): From 8a3993ca592406ca82b4345c38b61c054e8c2d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 31 Mar 2015 18:22:38 +0200 Subject: [PATCH 6/8] To determine order of template components, first use user order, then default order, then type ordering Also moved suffix calculation for template keys into _process_template_config --- src/octoprint/server/__init__.py | 33 ++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 2a625d5d..0f430acf 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -311,8 +311,7 @@ def index(): else: data = include[1] - suffix = data["suffix"] if "suffix" in data else "" - key = "plugin_" + name + suffix + key = data["_key"] if "replaces" in data: key = data["replaces"] templates[t]["entries"][key] = include @@ -324,25 +323,38 @@ def index(): # 2) we have all entries located somewhere within the order for t in template_types: + default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) configured_order = settings().get(["appearance", "components", "order", t], merged=True) configured_disabled = settings().get(["appearance", "components", "disabled", t]) + + # first create the ordered list of all component ids according to the configured order templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled] + # now append the entries from the default order that are not already in there + templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled] + all_ordered = set(templates[t]["order"]) all_disabled = set(configured_disabled) + # check if anything is missing, if not we are done here missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled) if len(missing_in_order) == 0: continue + # finally add anything that's not included in our order yet sorted_missing = list(missing_in_order) if not t == "navbar" and not t == "generic": + # anything but navbar and generic components get sorted by their name sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0]) + if t == "navbar": + # additional navbar components are prepended templates[t]["order"] = sorted_missing + templates[t]["order"] elif t == "sidebar" or t == "tab" or t == "generic" or t == "usersettings": + # additional sidebar, generic or usersettings components are appended templates[t]["order"] += sorted_missing elif t == "settings": + # additional settings items are added to the plugin section templates[t]["entries"]["section_plugins"] = (gettext("Plugins"), None) templates[t]["order"] += ["section_plugins"] + sorted_missing @@ -432,27 +444,32 @@ def _process_template_config(name, implementation, rule, config=None, counter=1) config = dict() data = dict(config) + if not "suffix" in data and counter > 1: + data["suffix"] = "_%d" % counter + if "div" in data: data["_div"] = data["div"] elif "div" in rule: data["_div"] = rule["div"](name) if "suffix" in data: - data["_div"] += "_" + data["suffix"] - elif counter > 1: - data["_div"] += "_%d" % counter - data["suffix"] = "_%d" % counter - else: - data["suffix"] = "" + data["_div"] = data["_div"] + data["suffix"] + if not "template" in data: data["template"] = rule["template"](name) + if not "name" in data: data["name"] = implementation._plugin_name + if not "custom_bindings" in data or data["custom_bindings"]: data_bind = "allowBindings: true" if "data_bind" in data: data_bind = data_bind + ", " + data["data_bind"] data["data_bind"] = data_bind + data["_key"] = "plugin_" + name + if "suffix" in data: + data["_key"] += data["suffix"] + return data @app.route("/robots.txt") From 5c228e6071cb6a188bbc65357fe29124025e1b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 31 Mar 2015 18:23:18 +0200 Subject: [PATCH 7/8] [Doc] Big overhaul of TemplatePlugin docs & component ordering --- docs/configuration/config_yaml.rst | 145 ++++++++++++++- docs/plugins/gettingstarted.rst | 2 +- docs/plugins/templates.rst | 98 +++++++++- src/octoprint/plugin/types.py | 282 +++++++++++------------------ 4 files changed, 344 insertions(+), 183 deletions(-) diff --git a/docs/configuration/config_yaml.rst b/docs/configuration/config_yaml.rst index 14116796..9efa5f47 100644 --- a/docs/configuration/config_yaml.rst +++ b/docs/configuration/config_yaml.rst @@ -11,6 +11,10 @@ settings. Note that many of these settings are available from the "Settings" menu in OctoPrint itself. +.. contents:: + +.. _sec-configuration-config_yaml-serial: + Serial ------ @@ -52,6 +56,8 @@ Use the following settings to configure the serial connection to the printer: additionalPorts: - /dev/myPrinterSymlink +.. _sec-configuration-config_yaml-server: + Server ------ @@ -95,6 +101,8 @@ Use the following settings to configure the server: `into OctoPrint's wiki `_ for a couple of examples on how to configure this. +.. _sec-configuration-config_yaml-webcam: + Webcam ------ @@ -133,6 +141,8 @@ Use the following settings to configure webcam support: options: interval: 2 +.. _sec-configuration-config_yaml-feature: + Feature ------- @@ -162,6 +172,8 @@ Use the following settings to enable or disable OctoPrint features: # Specifies whether support for SD printing and file management should be enabled sdSupport: true +.. _sec-configuration-config_yaml-folder: + Folder ------ @@ -194,6 +206,8 @@ Use the following settings to set custom paths for folders used by OctoPrint: # and/or sliced objects to print in the future. watched: /path/to/watched/folder +.. _sec-configuration-config_yaml-temperature: + Temperature ----------- @@ -210,21 +224,128 @@ Use the following settings to configure temperature profiles which will be displ extruder: 180 bed: 60 +.. _sec-configuration-config_yaml-appearance: + Appearance ---------- Use the following settings to tweak OctoPrint's appearance a bit to better distinguish multiple instances/printers -appearance: +appearance or to modify the order and presence of the various UI components: .. code-block:: yaml appearance: - # Use this to give your printer a name. It will be displayed in the title bar (as " [OctoPrint]") and in the - # navigation bar (as "OctoPrint: ") - name: My Printer Model + # Use this to give your printer a name. It will be displayed in the title bar + # (as " [OctoPrint]") and in the navigation bar (as "OctoPrint: ") + name: My Printer - # Use this to color the navigation bar. Supported colors are red, orange, yellow, green, blue, violet and default. - color: blue + # Use this to color the navigation bar. Supported colors are red, orange, + # yellow, green, blue, violet and default. + color: default + + # Makes the color of the navigation bar "transparent". In case your printer uses + # acrylic for its frame ;) + colorTransparent: false + + # Configures the order and availability of the UI components + components: + + # Defines the order of the components within their respective containers. + # + # If overridden by the user the resulting order for display will be calculated as + # follows: + # + # - first all components as defined by the user + # - then all enabled core components as define in the default order (see below) + # + # Components not contained within the default order (e.g. from plugins) will be either + # prepended or appended to that result, depending on component type. + # + # Note that a component is not included in the order as defined by the user will still + # be put into the container, according to the default order. To fully disable a + # component, you'll need to add it to the container's disabled list further below. + order: + + # order of navbar items + navbar: + - settings + - systemmenu + - login + + # order of sidebar items + sidebar: + - connection + - state + - files + + # order of tab items + tab: + - temperature + - control + - gcodeviewer + - terminal + - timelapse + + # order of settings, if settings plugins are registered gets extended internally by + # section_plugins and all settings plugins + settings + - section_printer + - serial + - printerprofiles + - temperatures + - terminalfilters + - gcodescripts + - section_features + - features + - webcam + - accesscontrol + - api + - section_octoprint + - folders + - appearance + - logs + + # order of user settings + usersettings: + - access + - interface + + # order of generic templates + generic: [] + + # Disabled components per container. If a component is included here it will not + # be included in OctoPrint's UI at all. Note that this might mean that critical + # functionality will not be available if no replacement is registered. + disabled: + navbar: [], + sidebar: [], + tab: [], + settings: [], + usersettings: [], + generic: [] + +.. note:: + + By modifying the ``components`` > ``order`` lists you may reorder OctoPrint's UI components as you like. You can also + inject Plugins at another than their default location in their respective container by adding the entry + ``plugin_`` where you want them to appear. + + Example: If you want the tab of the :ref:`Hello World Plugin ` to appear as the first tab + in OctoPrint, you'd need to redefine ``components`` > ``order`` > ``tab`` by including something like this in your + ``config.yaml``: + + .. code-block:: yaml + + appearance: + components: + order: + tab: + - plugin_helloworld + + OctoPrint will then turn this into the order ``plugin_helloworld``, ``temperature``, ``control``, ``gcodeviewer``, + ``terminal``, ``timelapse`` plus any other plugins. + +.. _sec-configuration-config_yaml-controls: Controls -------- @@ -249,6 +370,8 @@ OctoPrint. type: command command: M107 +.. _sec-configuration-config_yaml-system: + System ------ @@ -269,6 +392,8 @@ OctoPrint is running is allowed to do this without password entry: command: sudo shutdown -h now confirm: You are about to shutdown the system. +.. _sec-configuration-config_yaml-accesscontrol: + Access Control -------------- @@ -305,6 +430,8 @@ Use the following settings to enable access control: - 127.0.0.0/8 - 192.168.1.0/24 +.. _sec-configuration-config_yaml-events: + Events ------ @@ -326,6 +453,8 @@ Use the following settings to add shell/gcode commands to be executed on certain type: gcode enabled: False +.. _sec-configuration-config_yaml-terminalfilters: + Terminal Filters ---------------- @@ -343,6 +472,8 @@ Use `Javascript regular expressions `_. + +.. _sec-plugins-templates-navbar: + +Navbar +------ + +The right part of the navigation bar located at the top of the UI can be enriched with additional links. Note that +with the current implementation, plugins will always be located *to the left* of the existing links. + +The included template must be called ``_navbar.jinja2`` (e.g. ``myplugin_navbar.jinja2``) unless +overridden by the configuration supplied through :func:`get_template_configs`. + +The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The +wrapper structure will have all additional classes and styles applied as specified via the configuration supplied +through :func:`get_template_configs`. + +.. _sec-plugins-templates-sidebar: + +Sidebar +------- + +The left side bar containing Connection, State and Files sections can be enriched with additional sections. Note +that with the current implementations, plugins will always be located *beneath* the existing sections. + +The included template must be called ``_sidebar.jinja2`` (e.g. ``myplugin_sidebar.jinja2``) unless +overridden by the configuration supplied through :func:`get_template_configs`. + +The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The +wrapper divs for both the whole box as well as the content pane will have all additional classes and styles applied +as specified via the configuration supplied through :func:`get_template_configs`. + +.. _sec-plugins-templates-tabs: + +Tabs +---- + +The available tabs of the main part of the interface may be extended with additional tabs originating from within +plugins. Note that with the current implementation, plugins will always be located *to the right* of the existing +tabs. + +The included template must be called ``_tab.jinja2`` (e.g. ``myplugin_tab.jinja2``) unless +overridden by the configuration supplied through :func:`get_template_configs`. + +The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The +wrapper div and the link in the navigation will have the additional classes and styles applied as specified via the +configuration supplied through :func:`get_template_configs`. + +.. _sec-plugins-templates-settings: + +Settings +-------- + +Plugins may inject a dialog into the existing settings view. Note that with the current implementations, plugins +will always be listed beneath the "Plugins" header in the settings link list, ordered alphabetically after +their displayed name. + +The included template must be called ``_settings.jinja2`` (e.g. ``myplugin_settings.jinja2``) unless +overridden by the configuration supplied through :func:`get_template_configs`. + +The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The +wrapper div and the link in the navigation will have the additional classes and styles applied as defined via the +supplied configuration supplied through :func:`get_template_configs`. + +.. _sec-plugins-templates-generic: + +Generic +------- + +Plugins may also inject arbitrary templates into the page of the web interface itself, e.g. in order to +add overlays or dialogs to be called from within the plugin's javascript code. + +.. _sec-plugins-templates-replacement: + +Replacing existing components +----------------------------- diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index fdaff494..2f38879d 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -232,218 +232,134 @@ class TemplatePlugin(OctoPrintPlugin): Template injection types in the settings - You can find an example for a simple plugin which injects navbar, sidebar, tab and settings content into the interface in - `the "helloworld" plugin in OctoPrint's collection of plugin examples `_. + You can find an example for a simple plugin which injects navbar, tab and settings content into the interface in + the "helloworld" plugin in OctoPrint's :ref:`Plugin Tutorial `. + + Plugins may replace existing components, see the ``replaces`` keyword in the template configurations returned by + :meth:`.get_template_configs` below. Note that if a plugin replaces a core component, it is the plugin's + responsibility to ensure that all core functionality is still maintained. """ def get_template_configs(self): """ Allows configuration of injected navbar, sidebar, tab and settings templates. Should be a list containing one - configuration object per template to inject. Each configuration object is represented by a dictionary with a mandatory key - ``type`` encoding the template type the configuration is targeting. Possible values here are ``navbar``, ``sidebar``, - ``tab``, ``settings`` and ``generic``. + configuration object per template to inject. Each configuration object is represented by a dictionary which + may contain the following keys: + + .. list-table:: + :widths: 5 95 + + * - type + - The template type the configuration is targeting. Possible values here are ``navbar``, ``sidebar``, + ``tab``, ``settings`` and ``generic``. Mandatory. + * - name + - The name of the component, if not set the name of the plugin will be used. The name will be visible at + a location depending on the ``type``: + + * ``navbar``: unused + * ``sidebar``: sidebar heading + * ``tab``: tab heading + * ``settings``: settings link + * ``generic``: unused + + * - template + - Name of the template to inject, default value depends on the ``type``: + + * ``navbar``: ``_navbar.jinja2`` + * ``sidebar``: ``_sidebar.jinja2`` + * ``tab``: ``_tab.jinja2`` + * ``settings``: ``_settings.jinja2`` + * ``generic``: ``.jinja2`` + + * - suffix + - Suffix to attach to the component identifier and the div identifier of the injected template. Will be + ``_`` if not provided and not the first template of the type, with ``index`` counting from 1 and + increasing for each template of the same type. + + Example: If your plugin with identifier ``myplugin`` defines two tab components like this: + + .. code-block:: python + + return [ + dict(type="tab", template="myplugin_first_tab.jinja2"), + dict(type="tab", template="myplugin_second_tab.jinja2") + ] + + then the first tab will have the component identifier ``plugin_myplugin`` and the second one will have + the component identifier ``plugin_myplugin_2`` (the generated divs will be ``tab_plugin_myplugin`` and + ``tab_plugin_myplugin_2`` accordingly). Notice that the first tab is *not* called ``plugin_myplugin_1`` -- + as stated above while the ``index`` used as default suffix starts counting at 1, it will not be applied + for the first component of a given type. + + If on the other hand your plugin's definition looks like this: + + .. code-block:: python + + return [ + dict(type="tab", template="myplugin_first_tab_jinja2", suffix="_1st"), + dict(type="tab", template="myplugin_second_tab_jinja2", suffix="_2nd") + ] + + then the generated component identifier will be ``plugin_myplugin_1st`` and ``plugin_myplugin_2nd`` + (and the divs will be ``tab_plugin_myplugin_1st`` and ``tab_plugin_myplugin_2nd``). + + * - div + - Id for the div containing the component. If not provided, defaults to ``_plugin_`` plus + the ``suffix`` if provided or required. + * - replaces + - Id of the component this one replaces, might be either one of the core components or a component + provided by another plugin. A list of the core component identifiers can be found + :ref:`in the configuration documentation `. The identifiers of + other plugin components always follow the format described above. + * - custom_bindings + - A boolean value indicating whether the default view model should be bound to the component (``false``) + or if a custom binding will be used by the plugin (``true``, default). + * - data_bind + - Additional knockout data bindings to apply to the component, can be used to add further behaviour to + the container based on internal state if necessary. + * - classes + - Additional classes to apply to the component, as a list of individual classes + (e.g. ``classes=["myclass", "myotherclass"]``) which will be joined into the correct format by the template engine. + * - styles + - Additional CSS styles to apply to the component, as a list of individual declarations + (e.g. ``styles=["color: red", "display: block"]``) which will be joined into the correct format by the template + engine. Further keys to be included in the dictionary depend on the type: - ``navbar`` type - .. figure:: ../images/template-plugin-type-navbar.png - :align: center - :alt: Structure of navbar plugins - - Structure of navbar plugins - - Configures a navbar component to inject. The following keys are supported: - - .. list-table:: - :widths: 5 95 - - * - template - - Name of the template to inject, defaults to ``_navbar.jinja2``. - * - suffix - - Suffix to attach to the element ID of the injected template, will be ``_`` if not provided and not - the first template of the type, with ``index`` counting from 1 and increasing for each template of the same - type. - * - div - - Id for the div containing the component. If not provided, defaults to ``plugin_`` plus - the suffix if provided or required. - * - replaces - - Id of navbar component this one replaces, might be either one of the core components or a component - provided by another plugin. See :ref:`this section ` for more on replacing template components. - * - custom_bindings - - A boolean value indicating whether the default view model should be bound to the navbar entry (``false``) - or if a custom binding will be used by the plugin (``true``, default). - * - data_bind - - Additional knockout data bindings to apply to the navbar entry, can be used to add further behaviour to - the container based on internal state if necessary. - * - classes - - Additional classes to apply to the navbar entry, as a list of individual classes - (e.g. ``classes=["myclass", "myotherclass"]``) which will be joined into the correct format by the template engine. - * - styles - - Additional CSS styles to apply to the navbar entry, as a list of individual declarations - (e.g. ``styles=["color: red", "display: block"]``) which will be joined into the correct format by the template - engine. - ``sidebar`` type - .. figure:: ../images/template-plugin-type-sidebar.png - :align: center - :alt: Structure of sidebar plugins - - Structure of sidebar plugins - - Configures a sidebar component to inject. The following keys are supported: .. list-table:: :widths: 5 95 - * - name - - The name of the sidebar entry, if not set the name of the plugin will be used. * - icon - Icon to use for the sidebar header, should be the name of a Font Awesome icon without the leading ``icon-`` part. - * - template - - Name of the template to inject, defaults to ``_sidebar.jinja2``. * - template_header - Additional template to include in the head section of the sidebar item. For an example of this, see the additional options included in the "Files" section. - * - suffix - - Suffix to attach to the element ID of the injected template, will be ``_`` if not provided and not - the first template of the type, with ``index`` counting from 1 and increasing for each template of the same - type. - * - div - - Id for the div containing the component. If not provided, defaults to ``plugin_`` plus - the suffix if provided or required. - * - replaces - - Id of sidebar component this one replaces, might be either one of the core components or a component - provided by another plugin. See :ref:`this section ` for more on replacing template components. - * - custom_bindings - - A boolean value indicating whether the default view model should be bound to the sidebar container (``false``) - or if a custom binding will be used by the plugin (``true``, default). - * - data_bind - - Additional knockout data bindings to apply to the template container, can be used to add further behaviour to - the container based on internal state if necessary. - * - classes - - Additional classes to apply to both the wrapper around the sidebar box as well as the content pane itself, as a - list of individual classes (e.g. ``classes=["myclass", "myotherclass"]``) which will be joined into the correct - format by the template engine. * - classes_wrapper - Like ``classes`` but only applied to the whole wrapper around the sidebar box. * - classes_content - Like ``classes`` but only applied to the content pane itself. - * - styles - - Additional CSS styles to apply to both the wrapper around the sidebar box as well as the content pane itself, - as a list of individual declarations (e.g. ``styles=["color: red", "display: block"]``) which will be joined - into the correct format by the template engine. * - styles_wrapper - Like ``styles`` but only applied to the whole wrapper around the sidebar box. * - styles_content - Like ``styles`` but only applied to the content pane itself - ``tab`` type - .. figure:: ../images/template-plugin-type-tab.png - :align: center - :alt: Structure of tab plugins - - Structure of tab plugins - - Configures a tab component to inject. The value must be a dictionary, supported values are the following: + ``tab`` type and ``settings`` type .. list-table:: :widths: 5 95 - * - name - - The name under which to include the tab, if not set the name of the plugin will be used. - * - template - - Name of the template to inject, defaults to ``_tab.jinja2``. - * - suffix - - Suffix to attach to the element ID of the injected template, will be ``_`` if not provided and not - the first template of the type, with ``index`` counting from 1 and increasing for each template of the same - type. - * - div - - Id for the div containing the component. If not provided, defaults to ``plugin_`` plus - the suffix if provided or required. - * - replaces - - Id of tab component this one replaces, might be either one of the core components or a component - provided by another plugin. See :ref:`this section ` for more on replacing template components. - * - custom_bindings - - A boolean value indicating whether the default view model should be bound to the tab pane and link - in the navigation (``false``) or if a custom binding will be used by the plugin (``true``, default). - * - data_bind - - Additional knockout data bindings to apply to the template container, can be used to add further behaviour to - the container based on internal state if necessary. - * - classes - - Additional classes to apply to both the wrapper around the sidebar box as well as the content pane itself, as a - list of individual classes (e.g. ``classes=["myclass", "myotherclass"]``) which will be joined into the correct - format by the template engine. * - classes_link - Like ``classes`` but only applied to the link in the navigation. * - classes_content - Like ``classes`` but only applied to the content pane itself. - * - styles - - Additional CSS styles to apply to both the wrapper around the sidebar box as well as the content pane itself, - as a list of individual declarations (e.g. ``styles=["color: red", "display: block"]``) which will be joined - into the correct format by the template engine. * - styles_link - Like ``styles`` but only applied to the link in the navigation. * - styles_content - Like ``styles`` but only applied to the content pane itself. - ``settings`` type - .. figure:: ../images/template-plugin-type-settings.png - :align: center - :alt: Structure of settings plugins - - Structure of settings plugins - - Configures a settings component to inject. The value must be a dictionary, supported values are the following: - - .. list-table:: - :widths: 5 95 - - * - name - - The name under which to include the settings pane, if not set the name of the plugin will be used. - * - template - - Name of the template to inject, defaults to ``_settings.jinja2``. - * - suffix - - Suffix to attach to the element ID of the injected template, will be ``_`` if not provided and not - the first template of the type, with ``index`` counting from 1 and increasing for each template of the same - type. - * - div - - Id for the div containing the component. If not provided, defaults to ``plugin_`` plus - the suffix if provided or required. - * - replaces - - Id of settings component this one replaces, might be either one of the core components or a component - provided by another plugin. See :ref:`this section ` for more on replacing template components. - * - custom_bindings - - A boolean value indicating whether the default settings view model should be bound to the settings pane and link - in the navigation (``false``) or if a custom binding will be used by the plugin (``true``, default). - * - data_bind - - Additional knockout data bindings to apply to the template container, can be used to add further behaviour to - the container based on internal state if necessary. - * - classes - - Additional classes to apply to both the wrapper around the navigation link as well as the content pane itself, as a - list of individual classes (e.g. ``classes=["myclass", "myotherclass"]``) which will be joined into the correct - format by the template engine. - * - classes_link - - Like ``classes`` but only applied to the link in the navigation. - * - classes_content - - Like ``classes`` but only applied to the content pane itself. - * - styles - - Additional CSS styles to apply to both the wrapper around the navigation link as well as the content pane itself, - as a list of individual declarations (e.g. ``styles=["color: red", "display: block"]``) which will be joined - into the correct format by the template engine. - * - styles_link - - Like ``styles`` but only applied to the link in the navigation. - * - styles_content - - Like ``styles`` but only applied to the content pane itself - - ``generic`` type - Configures a generic template to inject. The following keys are supported: - - .. list-table:: - :widths: 5 95 - - * - template - - Name of the template to inject, defaults to ``.jinja2``. - .. note:: As already outlined above, each template type has a default template name (i.e. the default navbar template @@ -452,6 +368,28 @@ class TemplatePlugin(OctoPrintPlugin): those, since the implicit default template will only be included automatically if no other templates of that type are defined. + Example: If you have a plugin that injects two tab components, one defined in the template file + ``myplugin_tab.jinja2`` (the default template) and one in the template ``myplugin_othertab.jinja2``, you + might be tempted to just return the following configuration since one your templates is named by the default + template name: + + .. code-block:: python + + return [ + dict(type="tab", template="myplugin_othertab.jinja2") + ] + + This will only include the tab defined in ``myplugin_othertab.jinja2`` though, ``myplugin_tab.jinja2`` will + not be included automatically since the presence of a defintion for the ``tab`` type overrides the automatic + injection of the default template. You'll have to include it explicitely: + + .. code-block:: python + + return [ + dict(type="tab", template="myplugin_tab.jinja2"), + dict(type="tab", template="myplugin_othertab.jinja2") + ] + :return list: a list containing the configuration options for the plugin's injected templates """ return [] From 8a41cef00b95ddfded8683d1723306f986da151a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 31 Mar 2015 20:08:32 +0200 Subject: [PATCH 8/8] [Doc] Restructured the plugins section so it makes more sense --- docs/features/index.rst | 1 + docs/features/plugins.rst | 64 ++++++++++++++++++++ docs/plugins/concepts.rst | 103 +++++++++++++++++++++++++++++++- docs/plugins/distributing.rst | 45 ++++++++++---- docs/plugins/gettingstarted.rst | 38 ++++++++---- docs/plugins/index.rst | 18 ++---- docs/plugins/infrastructure.rst | 70 ---------------------- docs/plugins/templates.rst | 98 ------------------------------ docs/plugins/using.rst | 44 -------------- src/octoprint/plugin/core.py | 2 +- 10 files changed, 231 insertions(+), 252 deletions(-) create mode 100644 docs/features/plugins.rst delete mode 100644 docs/plugins/infrastructure.rst delete mode 100644 docs/plugins/templates.rst delete mode 100644 docs/plugins/using.rst diff --git a/docs/features/index.rst b/docs/features/index.rst index 29015bf6..5ebc8620 100644 --- a/docs/features/index.rst +++ b/docs/features/index.rst @@ -10,3 +10,4 @@ Features custom_controls.rst gcode_scripts.rst action_commands.rst + plugins.rst diff --git a/docs/features/plugins.rst b/docs/features/plugins.rst new file mode 100644 index 00000000..7d574824 --- /dev/null +++ b/docs/features/plugins.rst @@ -0,0 +1,64 @@ +.. _sec-features-plugins: + +******* +Plugins +******* + +Starting with OctoPrint 1.2.0, there's now a plugin system in place which allows to individually +extend OctoPrint's functionality. + +Right now plugins can be used to extend OctoPrint's web interface, to execute specific tasks on server startup and +shutdown, to provide custom (API) endpoints or whole user interfaces with special functionality, to react to system +events or progress reports or to add support for additional slicers. More plugin types are planned for the future. + +.. _sec-features-plugins-available: + +Finding Plugins +=============== + +Currently there's no such thing as a centralized plugin repository for available plugins. + +Plugins may be found in the lists provided in `the OctoPrint wiki `_ +and on the `OctoPrint organization Github page `_. + +.. _sec-features-plugins-installing: + +Installing Plugins +================== + +Plugins can be installed either by unpacking them into one of the configured plugin folders (regularly those are +``/plugins`` and ``/plugins`` [#f1]_ or by installing them as regular python +modules via ``pip`` [#f2]_. + +Please refer to the documentation of the plugin for installations instructions. + +For a plugin available on the Python Package Index (PyPi), the process is as simple as issuing a + +.. code-block:: bash + + pip install + +For plugins not available on PyPi, you'll have to give ``pip`` an URL from which to install the package (e.g. the URL to +a ZIP file of the current master branch of a Github repository hosting a plugin, or even a ``git+https`` URL), example: + +.. code-block:: bash + + pip install https://github.com/OctoPrint/OctoPrint-Growl/archive/master.zip + +See `the pip install documentation `_ for what URL +types are possible. + +.. _sec-features-plugins-developing: + +Developing Plugins +================== + +See :ref:`Developing Plugins `. + +.. rubric:: Footnotes + +.. [#f1] For Linux that will be ``~/.octoprint/plugins``, for Windows it will be ``%APPDATA%/OctoPrint/plugins`` and for + Mac ``~/Library/Application Support/OctoPrint`` +.. [#f2] Make sure to use the exact same Python installation for installing the plugin that you also used for + installing & running OctoPrint. For OctoPi this means using ``~/oprint/bin/pip`` for installing plugins + instead of just ``pip``. diff --git a/docs/plugins/concepts.rst b/docs/plugins/concepts.rst index 368f51e4..b57535a7 100644 --- a/docs/plugins/concepts.rst +++ b/docs/plugins/concepts.rst @@ -4,7 +4,7 @@ Concepts ======== OctoPrint's plugins are `Python Packages `_ which in their -top-level module define a bunch of :ref:`control properties ` defining +top-level module define a bunch of :ref:`control properties ` defining metadata (like name, version etc of the plugin) as well as information on how to initialize the plugin and into what parts of the system the plugin will actually plug in to perform its job. @@ -13,6 +13,57 @@ There are three types of ways a plugin might attach itself to the system, throug :ref:`hook ` or by offering :ref:`helper ` functionality to be used by other plugins. +Plugin mixin implementations will get a bunch of :ref:`properties injected ` +by OctoPrint plugin system to help them work. + +.. _sec-plugin-concepts-controlproperties: + +Control Properties +------------------ + +As already mentioned above, plugins are Python packages which provide certain pieces of metadata to tell OctoPrint's +plugin subsystem about themselves. These are simple package attributes defined in the top most package file, e.g.: + +.. code-block:: python + + import octoprint.plugin + + # ... + + __plugin_name__ = "My Plugin" + def __plugin_init__(): + # whatever you need to do to init your plugin, if anything at all + pass + +The following properties are recognized: + +``__plugin_name__`` + Name of your plugin, optional, overrides the name specified in ``setup.py`` if provided. If neither this property nor + a name from ``setup.py`` is available to the plugin subsystem, the plugin's identifier (= package name) will be + used instead. +``__plugin_version__`` + Version of your plugin, optional, overrides the version specified in ``setup.py`` if provided. +``__plugin_description__`` + Description of your plugin, optional, overrides the description specified in ``setup.py`` if provided. +``__plugin_author__`` + Author of your plugin, optional, overrides the author specified in ``setup.py`` if provided. +``__plugin_url__`` + URL of the webpage of your plugin, e.g. the Github repository, optional, overrides the URL specified in ``setup.py`` if + provided. +``__plugin_license__`` + License of your plugin, optional, overrides the license specified in ``setup.py`` if provided. +``__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__`` + Method called upon discovery of the plugin by the plugin subsystem, should return ``True`` if the + plugin can be instantiated later on, ``False`` if there are reasons why not, e.g. if dependencies + are missing. +``__plugin_init__`` + Method called upon initializing of the plugin by the plugin subsystem, can be used to instantiate + plugin implementations, connecting them to hooks etc. + .. _sec-plugin-concepts-mixins: Mixins @@ -223,4 +274,52 @@ them as (hopefully) documented. return flask.jsonify(dict( browsing_enabled=True, growl_instances=growl_instances - )) \ No newline at end of file + )) + +.. _sec-plugins-concepts-injectedproperties: + +Injected Properties +------------------- + +OctoPrint's plugin subsystem will inject a bunch of properties into each :ref:`mixin implementation `. +An overview of these properties follows. + +``self._identifier`` + The plugin's identifier. +``self._plugin_name`` + The plugin's name, as taken from either the ``__plugin_name__`` control property or the package info. +``self._plugin_version`` + The plugin's version, as taken from either the ``__plugin_version__`` control property or the package info. +``self._basefolder`` + The plugin's base folder where it's installed. Can be used to refer to files relative to the plugin's installation + location, e.g. included scripts, templates or assets. +``self._logger`` + A `python logger instance `_ logging to the log target + ``octoprint.plugin.``. +``self._settings`` + The plugin's personalized settings manager, injected only into plugins that include the :class:`~octoprint.plugin.SettingsPlugin` mixin. + An instance of :class:`octoprint.plugin.PluginSettings`. +``self._plugin_manager`` + OctoPrint's plugin manager object, an instance of :class:`octoprint.plugin.core.PluginManager`. +``self._printer_profile_manager`` + OctoPrint's printer profile manager, an instance of :class:`octoprint.printer.profile.PrinterProfileManager`. +``self._event_bus`` + OctoPrint's event bus, an instance of :class:`octoprint.events.EventManager`. +``self._analysis_queue`` + OctoPrint's analysis queue for analyzing GCODEs or other files, an instance of :class:`octoprint.filemanager.analysis.AnalysisQueue`. +``self._slicing_manager`` + OctoPrint's slicing manager, an instance of :class:`octoprint.slicing.SlicingManager`. +``self._file_manager`` + OctoPrint's file manager, an instance of :class:`octoprint.filemanager.FileManager`. +``self._printer`` + OctoPrint's printer management object, an instance of :class:`octoprint.printer.PrinterInterface`. +``self._app_session_manager`` + OctoPrint's application session manager, an instance of :class:`octoprint.server.util.flask.AppSessionManager`. + +.. seealso:: + + :class:`~octoprint.plugin.core.Plugin` and :class:`~octoprint.plugin.types.OctoPrintPlugin` + Class documentation also containing the properties shared among all mixing implementations. + + :ref:`Available Mixins ` + Some mixin types trigger the injection of additional properties. diff --git a/docs/plugins/distributing.rst b/docs/plugins/distributing.rst index 7a8d6fc0..9cdeba2e 100644 --- a/docs/plugins/distributing.rst +++ b/docs/plugins/distributing.rst @@ -3,25 +3,44 @@ Distributing your plugin ======================== -You can distribute a plugin with OctoPrint via two ways: +You can distribute a plugin with OctoPrint via two ways. - - You can have your users copy it to OctoPrint's plugin folder (normally located at ``~/.octoprint/plugins`` under Linux, - ``%APPDATA%\OctoPrint\plugins`` on Windows and ... on Mac). In this case your plugin will be distributed directly - as a Python module (a single ``.py`` file containing all of your plugin's code directly and named - like your plugin) or a package (a folder named like your plugin + ``__init.py__`` contained within). - - You can have your users install it via ``pip`` and register it for the `entry point `_ ``octoprint.plugin`` via - your plugin's ``setup.py``, this way it will be found automatically by OctoPrint upon initialization of the - plugin subsystem [#f1]_. +.. _sec-plugins-distribution-manual: - For an example of how the directory structure and related files would look like in this case, please take a - look at the `helloworld example from OctoPrint's example plugins `_. +Manual file distribution +------------------------ - This variant is highly recommended for pretty much any plugin besides the most basic ones since it also allows - requirements management and pretty much any thing else that Python's setuptools provide to the developer. +You can have your users copy it to OctoPrint's plugin folder (normally located at ``~/.octoprint/plugins`` under Linux, +``%APPDATA%\OctoPrint\plugins`` on Windows and ... on Mac). In this case your plugin will be distributed directly +as a Python module (a single ``.py`` file containing all of your plugin's code directly and named +like your plugin) or a package (a folder named like your plugin + ``__init.py__`` contained within). + +.. _sec-plugins-distribution-pip: + +Proper packages installable via pip +----------------------------------- + +You can have your users install it via ``pip`` and register it for the `entry point `_ ``octoprint.plugin`` via +your plugin's ``setup.py``, this way it will be found automatically by OctoPrint upon initialization of the +plugin subsystem [#f1]_. + +For an example of how the directory structure and related files would look like in this case, please take a +look at the `helloworld example from OctoPrint's example plugins `_. + +This variant is highly recommended for pretty much any plugin besides the most basic ones since it also allows +requirements management and pretty much any thing else that Python's setuptools provide to the developer. + +.. seealso:: + + `OctoPrint Plugin Skeleton `_ + A basic plugin skeleton providing you with all you need to get started with distributing your plugin as a proper + package. See the :ref:`Getting Started Guide ` for an + :ref:`example ` on how to use this. .. rubric:: Footnotes .. [#f1] The automatic registration will only work within the same Python installation (this also includes virtual environments), so make sure to instruct your users to use the exact same Python installation for installing - the plugin that they also used for installing & running OctoPrint. + the plugin that they also used for installing & running OctoPrint. For OctoPi this means using + ``~/oprint/bin/pip`` for installing plugins instead of just ``pip``. diff --git a/docs/plugins/gettingstarted.rst b/docs/plugins/gettingstarted.rst index ebbd73bf..2b310e4e 100644 --- a/docs/plugins/gettingstarted.rst +++ b/docs/plugins/gettingstarted.rst @@ -22,8 +22,13 @@ We'll start at the most basic form a plugin can take - just a couple of simple l Saving this as ``helloworld.py`` in ``~/.octoprint/plugins`` yields you something resembling these log entries upon server startup:: 2015-01-27 11:14:35,124 - octoprint.server - INFO - Starting OctoPrint 1.2.0-dev-448-gd96e56e (devel branch) + [...] 2015-01-27 11:14:35,124 - octoprint.plugin.core - INFO - Loading plugins from /home/pi/.octoprint/plugins, /home/pi/OctoPrint/src/octoprint/plugins and installed plugin packages... - 2015-01-27 11:14:36,135 - octoprint.plugin.core - INFO - Found 3 plugin(s): Hello World (1.0), CuraEngine (0.1), Discovery (0.1) + [...] + 2015-01-27 11:14:36,135 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system: + | CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura + | Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery + | Hello World (1.0) = /home/pi/.octoprint/plugins/helloworld.py OctoPrint found that plugin in the folder and took a look into it. The name and the version it displays in that log entry it got from the ``__plugin_name__`` and ``__plugin_version__`` lines. It also read the description from @@ -68,7 +73,7 @@ used :func:`~octoprint.plugin.StartupPlugin.on_startup` instead, in which case o up and ready to serve requests. You'll also note that we are using ``self._logger`` for logging. Where did that one come from? OctoPrint's plugin system -injects :ref:`a couple of useful objects ` into our plugin implementation classes, +injects :ref:`a couple of useful objects ` into our plugin implementation classes, one of those being a fully instantiated `python logger `_ ready to be used by your plugin. As you can see in the log output above, that logger uses the namespace ``octoprint.plugins.helloworld`` for our little plugin here, or more generally ``octoprint.plugins.``. @@ -85,8 +90,7 @@ You basically have two options to distribute your plugin. One would be about the as a simple python file following the naming convention ``.py`` that your users add to their ``~/.octoprint/plugins`` folder. You already know how that works. But let's say you have more than just a simple plugin that can be done in one file. Distributing multiple files and getting your users to install them in the right way -so that OctoPrint will be able to actually find and load them is certainly not impossible (see :ref:`the plugin distribution -documentation ` if you want to take a closer look at that option), but we want to do it in the +so that OctoPrint will be able to actually find and load them is certainly not impossible, but we want to do it in the best way possible, meaning we want to make our plugin a fully installable python module that your users will be able to install directly via Python's standard package manager ``pip`` or alternatively via `OctoPrint's own plugin manager `_. @@ -130,8 +134,13 @@ discoverable by OctoPrint, however we don't have to reinstall it after any chang Restart OctoPrint. Your plugin should still be properly discovered and the log line should be printed:: 2015-01-27 13:43:34,134 - octoprint.server - INFO - Starting OctoPrint 1.2.0-dev-448-gd96e56e (devel branch) + [...] 2015-01-27 13:43:34,134 - octoprint.plugin.core - INFO - Loading plugins from /home/pi/.octoprint/plugins, /home/pi/OctoPrint/src/octoprint/plugins and installed plugin packages... - 2015-01-27 13:43:34,818 - octoprint.plugin.core - INFO - Found 3 plugin(s): Hello World (1.0), CuraEngine (0.1), Discovery (0.1) + [...] + 2015-01-27 13:43:34,818 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system: + | CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura + | Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery + | Hello World (1.0) = /home/pi/OctoPrint-HelloWorld/octoprint_helloworld [...] 2015-01-27 13:43:38,997 - octoprint.plugins.helloworld - INFO - Hello World! @@ -142,13 +151,16 @@ of information now defined twice: .. code-block:: python :linenos: + :caption: __init__.py - # __init__.py: __plugin_name__ = "Hello World" __plugin_version__ = "1.0" __plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint" - # setup.py +.. code-block:: python + :linenos: + :caption: setup.py + plugin_name = "OctoPrint-HelloWorld" plugin_version = "1.0" plugin_description = "A quick \"Hello World\" example plugin for OctoPrint" @@ -173,7 +185,10 @@ and ``__plugin_description__`` from ``__init__.py``: and restart OctoPrint:: - 2015-01-27 13:46:33,786 - octoprint.plugin.core - INFO - Found 3 plugin(s): OctoPrint-HelloWorld (1.0), CuraEngine (0.1), Discovery (0.1) + 2015-01-27 13:46:33,786 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system: + | CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura + | Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery + | OctoPrint-HelloWorld (1.0) = /home/pi/OctoPrint-HelloWorld/octoprint_helloworld Our "Hello World" Plugin still gets detected fine, but it's now listed under the same name it's installed under, "OctoPrint-HelloWorld". That's a bit redundant and squashed, so we'll override that bit via ``__plugin_name__`` again: @@ -197,10 +212,13 @@ Our "Hello World" Plugin still gets detected fine, but it's now listed under the Restart OctoPrint again:: - 2015-01-27 13:48:54,122 - octoprint.plugin.core - INFO - Found 3 plugin(s): Hello World (1.0), CuraEngine (0.1), Discovery (0.1) + 2015-01-27 13:48:54,122 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system: + | CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura + | Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery + | Hello World (1.0) = /home/pi/OctoPrint-HelloWorld/octoprint_helloworld Much better! You can override pretty much all of the metadata defined within ``setup.py`` from within your Plugin itself -- -take a look at :ref:`the available control properties ` for all available +take a look at :ref:`the available control properties ` for all available overrides. Following the README of the `Plugin Skeleton `_ you could now diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 74d23be2..f70097ef 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -1,24 +1,14 @@ .. _sec-plugins: -####### -Plugins -####### - -Starting with OctoPrint 1.2.0, there's now a plugin system in place which allows to individually -extend OctoPrint's functionality. - -Right now plugins can be used to extend OctoPrint's web interface, to execute specific tasks on server startup and -shutdown, to provide custom (API) endpoints with special functionality, to react on system events or to add support for -additional slicers. More plugin types are planned for the future. +################## +Developing Plugins +################## .. toctree:: :maxdepth: 3 - using.rst - concepts.rst gettingstarted.rst - infrastructure.rst - templates.rst + concepts.rst distributing.rst mixins.rst hooks.rst diff --git a/docs/plugins/infrastructure.rst b/docs/plugins/infrastructure.rst deleted file mode 100644 index 94a192c8..00000000 --- a/docs/plugins/infrastructure.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. _sec-plugins-infrastructure: - -Plugin Infrastructure -===================== - -.. _sec-plugins-infrastructure-controlproperties: - -Control Properties ------------------- - -``__plugin_name__`` - Name of your plugin, optional, overrides the name specified in ``setup.py`` if provided. -``__plugin_version__`` - Version of your plugin, optional, overrides the version specified in ``setup.py`` if provided. -``__plugin_description__`` - Description of your plugin, optional, overrides the description specified in ``setup.py`` if provided. -``__plugin_author__`` - Author of your plugin, optional, overrides the author specified in ``setup.py`` if provided. -``__plugin_url__`` - URL of the webpage of your plugin, e.g. the Github repository, optional, overrides the URL specified in ``setup.py`` if - provided. -``__plugin_license__`` - License of your plugin, optional, overrides the license specified in ``setup.py`` if provided. -``__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__`` - Method called upon discovery of the plugin by the plugin subsystem, should return ``True`` if the - plugin can be instantiated later on, ``False`` if there are reasons why not, e.g. if dependencies - are missing. -``__plugin_init__`` - Method called upon initializing of the plugin by the plugin subsystem, can be used to instantiate - plugin implementations, connecting them to hooks etc. - -.. _sec-plugins-infrastructure-injections: - -Injected Properties -------------------- - -``self._identifier`` - The plugin's identifier. -``self._plugin_name`` - The plugin's name, as taken from either the ``__plugin_name__`` control property or the package info. -``self._plugin_version`` - The plugin's version, as taken from either the ``__plugin_version__`` control property or the package info. -``self._basefolder`` - The plugin's base folder where it's installed. Can be used to refer to files relative to the plugin's installation - location, e.g. included scripts, templates or assets. -``self._logger`` - A `python logger instance `_ logging to the log target - ``octoprint.plugin.``. -``self._settings`` - The plugin's personalized settings manager, injected only into plugins that include the :class:`SettingsPlugin` mixin. -``self._plugin_manager`` - OctoPrint's plugin manager. -``self._printer_profile_manager`` - OctoPrint's printer profile manager. -``self._event_bus`` - OctoPrint's event bus. -``self._analysis_queue`` - OctoPrint's analysis queue for analyzing GCODEs or other files. -``self._slicing_manager`` - OctoPrint's slicing manager. -``self._file_manager`` - OctoPrint's file manager. -``self._printer`` - OctoPrint's printer management object. -``self._app_session_manager`` - OctoPrint's application session manager. diff --git a/docs/plugins/templates.rst b/docs/plugins/templates.rst deleted file mode 100644 index c569dcba..00000000 --- a/docs/plugins/templates.rst +++ /dev/null @@ -1,98 +0,0 @@ -.. _sec-plugins-templates: - -Component Templates -=================== - -OctoPrint allows plugins to extend the UI of OctoPrint through the use of the :class:`~octoprint.plugin.TemplatePlugin` -mixin. By implementing this mixing and providing templates and their configuration through it, Plugins may currently -create one or more of navbar, sidebar, tabs, settings and generic components. - -.. figure:: ../images/template-plugin-types-main.png - :align: center - :alt: Template injection types in the main part of the interface - - Template injection types in the main part of the interface - -.. figure:: ../images/template-plugin-types-settings.png - :align: center - :alt: Template injection types in the settings - - Template injection types in the settings - -You can find an example for a simple plugin which injects navbar, sidebar, tab and settings content into the interface in -`the "helloworld" plugin in OctoPrint's collection of plugin examples `_. - -.. _sec-plugins-templates-navbar: - -Navbar ------- - -The right part of the navigation bar located at the top of the UI can be enriched with additional links. Note that -with the current implementation, plugins will always be located *to the left* of the existing links. - -The included template must be called ``_navbar.jinja2`` (e.g. ``myplugin_navbar.jinja2``) unless -overridden by the configuration supplied through :func:`get_template_configs`. - -The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The -wrapper structure will have all additional classes and styles applied as specified via the configuration supplied -through :func:`get_template_configs`. - -.. _sec-plugins-templates-sidebar: - -Sidebar -------- - -The left side bar containing Connection, State and Files sections can be enriched with additional sections. Note -that with the current implementations, plugins will always be located *beneath* the existing sections. - -The included template must be called ``_sidebar.jinja2`` (e.g. ``myplugin_sidebar.jinja2``) unless -overridden by the configuration supplied through :func:`get_template_configs`. - -The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The -wrapper divs for both the whole box as well as the content pane will have all additional classes and styles applied -as specified via the configuration supplied through :func:`get_template_configs`. - -.. _sec-plugins-templates-tabs: - -Tabs ----- - -The available tabs of the main part of the interface may be extended with additional tabs originating from within -plugins. Note that with the current implementation, plugins will always be located *to the right* of the existing -tabs. - -The included template must be called ``_tab.jinja2`` (e.g. ``myplugin_tab.jinja2``) unless -overridden by the configuration supplied through :func:`get_template_configs`. - -The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The -wrapper div and the link in the navigation will have the additional classes and styles applied as specified via the -configuration supplied through :func:`get_template_configs`. - -.. _sec-plugins-templates-settings: - -Settings --------- - -Plugins may inject a dialog into the existing settings view. Note that with the current implementations, plugins -will always be listed beneath the "Plugins" header in the settings link list, ordered alphabetically after -their displayed name. - -The included template must be called ``_settings.jinja2`` (e.g. ``myplugin_settings.jinja2``) unless -overridden by the configuration supplied through :func:`get_template_configs`. - -The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The -wrapper div and the link in the navigation will have the additional classes and styles applied as defined via the -supplied configuration supplied through :func:`get_template_configs`. - -.. _sec-plugins-templates-generic: - -Generic -------- - -Plugins may also inject arbitrary templates into the page of the web interface itself, e.g. in order to -add overlays or dialogs to be called from within the plugin's javascript code. - -.. _sec-plugins-templates-replacement: - -Replacing existing components ------------------------------ diff --git a/docs/plugins/using.rst b/docs/plugins/using.rst deleted file mode 100644 index 898996e5..00000000 --- a/docs/plugins/using.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _sec-plugins-using: - -************* -Using Plugins -************* - -.. _sec-plugins-using-available: - -Finding Plugins -=============== - -Currently there's no such thing as a centralized plugin repository for available plugins. - -Plugins may be found in the lists provided in `the OctoPrint wiki `_ -and on the `OctoPrint organization Github page `_. - -.. _sec-plugins-using-installing: - -Installing Plugins -================== - -Plugins can be installed either by unpacking them into one of the configured plugin folders (regularly those are -``/plugins`` and ``~/.octoprint/plugins`` or by installing them as regular python modules via ``pip``. -Please refer to the documentation of the plugin for installations instructions. - -The latter is the more common case since all currently published plugins not bundled with OctoPrint can and should be installed -this way. - -For a plugin available on the Python Package Index (PyPi), the process is as simple as issuing a - -.. code-block:: bash - - pip install - -For plugins not available on PyPi, you'll have to give ``pip`` an URL from which to install the package (e.g. the URL to -a ZIP file of the current master branch of a Github repository hosting a plugin, or even a ``git+https`` URL), example: - -.. code-block:: bash - - pip install https://github.com/OctoPrint/OctoPrint-Growl/archive/master.zip - -See `the pip install documentation `_ for what URL -types are possible. - diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 5b1f831f..c98a58db 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -35,7 +35,7 @@ class PluginInfo(object): implementations, hooks and helpers. It works on Python module objects and extracts the relevant data from those via accessing the - :ref:`control properties `. + :ref:`control properties `. Arguments: key (str): Identifier of the plugin