Merge branch 'devel' into dev/pluginLifecycleMgmt

Conflicts:
	src/octoprint/plugin/core.py
This commit is contained in:
Gina Häußge 2015-04-01 11:02:05 +02:00
commit 3ebf5e5240
33 changed files with 904 additions and 468 deletions

View file

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

View file

@ -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 <https://github.com/foosel/OctoPrint/wiki/Reverse-proxy-configuration-examples>`_ 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 "<Name> [OctoPrint]") and in the
# navigation bar (as "OctoPrint: <Name>")
name: My Printer Model
# Use this to give your printer a name. It will be displayed in the title bar
# (as "<Name> [OctoPrint]") and in the navigation bar (as "OctoPrint: <Name>")
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_<plugin identifier>`` where you want them to appear.
Example: If you want the tab of the :ref:`Hello World Plugin <sec-plugins-gettingstarted>` 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 <https://developer.mozilla.org/en/docs/Web/J
- name: Suppress M27 requests/responses
regex: '(Send: M27)|(Recv: SD printing byte)'
.. _sec-configuration-config_yaml-api:
API
---
@ -357,6 +488,8 @@ Settings for the REST API:
# current API key needed for accessing the API
apikey: ...
.. _sec-configuration-config_yaml-devel:
Development settings
--------------------

View file

@ -10,3 +10,4 @@ Features
custom_controls.rst
gcode_scripts.rst
action_commands.rst
plugins.rst

64
docs/features/plugins.rst Normal file
View file

@ -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 <https://github.com/foosel/OctoPrint/wiki#plugins>`_
and on the `OctoPrint organization Github page <https://github.com/OctoPrint>`_.
.. _sec-features-plugins-installing:
Installing Plugins
==================
Plugins can be installed either by unpacking them into one of the configured plugin folders (regularly those are
``<octoprint source root>/plugins`` and ``<octoprint config folder>/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 <plugin_name>
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 <http://pip.readthedocs.org/en/latest/reference/pip_install.html>`_ for what URL
types are possible.
.. _sec-features-plugins-developing:
Developing Plugins
==================
See :ref:`Developing Plugins <sec-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``.

View file

@ -4,7 +4,7 @@ Concepts
========
OctoPrint's plugins are `Python Packages <https://docs.python.org/2/tutorial/modules.html#packages>`_ which in their
top-level module define a bunch of :ref:`control properties <sec-plugins-infrastructure-controlproperties>` defining
top-level module define a bunch of :ref:`control properties <sec-plugin-concepts-controlproperties>` 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 <sec-plugin-concepts-hooks>` or by offering :ref:`helper <sec-plugin-concepts-helpers>` functionality to be
used by other plugins.
Plugin mixin implementations will get a bunch of :ref:`properties injected <sec-plugins-concepts-injectedproperties>`
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 <sec-plugins-mixins>`.
``__plugin_hooks__``
Handlers for one or more of the various :ref:`plugin hooks <sec-plugins-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
@ -20,7 +71,7 @@ Mixins
Plugin mixins are the heart of OctoPrint's plugin system. They are :ref:`special base classes <sec-plugins-mixins>`
which are to be subclassed and extended to add functionality to OctoPrint. Plugins declare their instances that
implement one or multiple mixins using the ``__plugin_implementations__`` control property. OctoPrint's plugin core
implement one or multiple mixins using the ``__plugin_implementation__`` control property. OctoPrint's plugin core
collects those from the plugins and offers methods to access them based on the mixin type, which get used at multiple
locations within OctoPrint.
@ -135,7 +186,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 +204,122 @@ property instead, manually instantiate your implementation instance and then add
Helpers
-------
Helpers are methods that plugin can exposed to other plugins in order to make common functionality available on the
system. They are registered with the OctoPrint plugin system through the use of the control property ``__plugin_helpers__``.
An example for providing a couple of helper functions to the system can be found in the
`Discovery Plugin <https://github.com/foosel/OctoPrint/wiki/Plugin:-Discovery>`_,
which provides it's SSDP browsing and Zeroconf browsing and publishing functions as helper methods.
.. code-block:: python
:linenos:
:emphasize-lines: 11-20
:caption: Excerpt from the Discovery Plugin showing the declaration of its exported helpers.
:name: sec-plugin-concepts-helpers-example-export
def __plugin_init__():
if not pybonjour:
# no pybonjour available, we can't use that
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available")
plugin = DiscoveryPlugin()
global __plugin_implementation__
__plugin_implementation__ = plugin
global __plugin_helpers__
__plugin_helpers__ = dict(
ssdp_browse=plugin.ssdp_browse
)
if pybonjour:
__plugin_helpers__.update(dict(
zeroconf_browse=plugin.zeroconf_browse,
zeroconf_register=plugin.zeroconf_register,
zeroconf_unregister=plugin.zeroconf_unregister
))
An example of how to use helpers can be found in the `Growl Plugin <https://github.com/OctoPrint/OctoPrint-Growl>`_.
Using :meth:`~octoprint.plugin.code.PluginManager.get_helpers` plugins can retrieve exported helper methods and call
them as (hopefully) documented.
.. code-block:: python
:linenos:
:emphasize-lines: 6-8,20
:caption: Excerpt from the Growl Plugin showing utilization of the helpers published by the Discovery Plugin.
:name: sec-plugin-concepts-helpers-example-usage
def on_after_startup(self):
host = self._settings.get(["hostname"])
port = self._settings.getInt(["port"])
password = self._settings.get(["password"])
helpers = self._plugin_manager.get_helpers("discovery", "zeroconf_browse")
if helpers and "zeroconf_browse" in helpers:
self.zeroconf_browse = helpers["zeroconf_browse"]
self.growl, _ = self._register_growl(host, port, password=password)
# ...
def on_api_get(self, request):
if not self.zeroconf_browse:
return flask.jsonify(dict(
browsing_enabled=False
))
browse_results = self.zeroconf_browse("_gntp._tcp", block=True)
growl_instances = [dict(name=v["name"], host=v["host"], port=v["port"]) for v in browse_results]
return flask.jsonify(dict(
browsing_enabled=True,
growl_instances=growl_instances
))
.. _sec-plugins-concepts-injectedproperties:
Injected Properties
-------------------
OctoPrint's plugin subsystem will inject a bunch of properties into each :ref:`mixin implementation <sec-plugin-concepts-mixins>`.
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 <https://docs.python.org/2/library/logging.html>`_ logging to the log target
``octoprint.plugin.<plugin identifier>``.
``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 <sec-plugins-mixins>`
Some mixin types trigger the injection of additional properties.

View file

@ -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 <https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins>`_ ``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 <https://github.com/OctoPrint/Plugin-Examples/tree/master/helloworld>`_.
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 <https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins>`_ ``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 <https://github.com/OctoPrint/Plugin-Examples/tree/master/helloworld>`_.
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 <https://github.com/OctoPrint/OctoPrint-PluginSkeleton>`_
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 <sec-plugins-gettingstarted>` for an
:ref:`example <sec-plugins-gettingstarted-growingup>` 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``.

View file

@ -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
@ -53,14 +58,14 @@ Apart from being discovered by OctoPrint, our plugin does nothing yet. We want t
__plugin_name__ = "Hello World"
__plugin_version__ = "1.0"
__plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
and restart OctoPrint. You now get this output in the log::
2015-01-27 11:17:10,792 - octoprint.plugins.helloworld - INFO - Hello World!
Neat, isn't it? We added a custom class that subclasses one of OctoPrint's :ref:`plugin mixins <sec-plugins-mixins>`
with :class:`~octoprint.plugin.StartupPlugin` and another control property, ``__plugin_implementations__``, that instantiates
with :class:`~octoprint.plugin.StartupPlugin` and another control property, ``__plugin_implementation__``, that instantiates
our plugin class and tells OctoPrint about it. Taking a look at the documentation of :class:`~octoprint.plugin.StartupPlugin` we see that
this mixin offers two methods that get called by OctoPrint during startup of the server, :func:`~octoprint.plugin.StartupPlugin.on_startup` and
:func:`~octoprint.plugin.StartupPlugin.on_after_startup`. We decided to add our logging output by overriding :func:`~octoprint.plugin.StartupPlugin.on_after_startup`, but we could also have
@ -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 <sec-plugins-infrastructure-injections>` into our plugin implementation classes,
injects :ref:`a couple of useful objects <sec-plugins-concepts-injectedproperties>` into our plugin implementation classes,
one of those being a fully instantiated `python logger <https://docs.python.org/2/library/logging.html>`_ 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.<plugin identifier>``.
@ -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 ``<plugin identifier>.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 <sec-plugins-distribution>` 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 <https://github.com/OctoPrint/OctoPrint-PluginManager>`_.
@ -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,20 +151,23 @@ 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"
The nice thing about our plugin now being a proper python package is that OctoPrint can and will access the metadata defined
within ``setup.py``! So, we don't really need to define all this data twice. Remove ``__plugin_name__``, ``__plugin_version__``
and ``__plugin_description__``:
and ``__plugin_description__`` from ``__init__.py``:
.. code-block:: python
:linenos:
@ -169,11 +181,14 @@ and ``__plugin_description__``:
def on_after_startup(self):
self._logger.info("Hello World!")
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
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:
@ -192,15 +207,18 @@ 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::
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 <sec-plugins-infrastructure-controlproperties>` for all available
take a look at :ref:`the available control properties <sec-plugin-concepts-controlproperties>` for all available
overrides.
Following the README of the `Plugin Skeleton <https://github.com/OctoPrint/OctoPrint-PluginSkeleton>`_ you could now
@ -238,7 +256,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 +322,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 +359,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 +469,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 +568,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 +749,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 +819,7 @@ Then adjust our returned assets to include our LESS file as well:
)
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
__plugin_implementation__ = HelloWorldPlugin()
and enable LESS mode by adjusting one of OctoPrint's ``devel`` flags via the ``config.yaml`` file:

View file

@ -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

View file

@ -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_implementations__``
Instances of one or more of the various :ref:`plugin mixins <sec-plugins-mixins>`
``__plugin_hooks__``
Handlers for one or more of the various :ref:`plugin hooks <sec-plugins-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 <https://docs.python.org/2/library/logging.html>`_ logging to the log target
``octoprint.plugin.<plugin identifier>``.
``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.

View file

@ -1,8 +0,0 @@
.. _sec-plugins-templates:
Templates
=========
.. todo::
Needs to be written.

View file

@ -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 <https://github.com/foosel/OctoPrint/wiki#plugins>`_
and on the `OctoPrint organization Github page <https://github.com/OctoPrint>`_.
.. _sec-plugins-using-installing:
Installing Plugins
==================
Plugins can be installed either by unpacking them into one of the configured plugin folders (regularly those are
``<octoprint-root>/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 <plugin_name>
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 <http://pip.readthedocs.org/en/latest/reference/pip_install.html>`_ for what URL
types are possible.

View file

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

View file

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

View file

@ -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 <sec-plugins-infrastructure-controlproperties>`.
:ref:`control properties <sec-plugin-concepts-controlproperties>`.
Arguments:
key (str): Identifier of the plugin
@ -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. """
@ -109,6 +121,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)
@ -146,24 +177,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):
@ -244,7 +274,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.
@ -252,7 +282,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):
@ -347,7 +377,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.implementation_injects = dict()
@ -502,12 +532,23 @@ class PluginManager(object):
for name, plugin in plugins.items():
self.load_plugin(name, plugin)
# 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 )
plugin_implementations = plugin.get_implementations()
if len(plugin_implementations):
self.plugin_implementations[name].update(plugin_implementations)
except:
self.logger.exception("There was an error loading plugin %s" % name)
if len(self.plugins) <= 0:
self.logger.info("No plugins found")
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()))
))
@ -707,17 +748,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:
@ -729,9 +825,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]
@ -742,17 +869,35 @@ class PluginManager(object):
else:
return all_helpers
def register_client(self, client):
def register_message_receiver(self, client):
"""
Registers a ``client`` for receiving plugin messages. The ``client`` needs to be a callable accepting two
input arguments, ``plugin`` (the sending plugin's identifier) and ``data`` (the message itself).
"""
if client is None:
return
self.registered_clients.append(client)
def unregister_client(self, client):
def unregister_message_receiver(self, client):
"""
Unregisters a ``client`` for receiving plugin messages.
"""
self.registered_clients.remove(client)
def send_plugin_message(self, plugin, data):
"""
Sends ``data`` in the name of ``plugin`` to all currently registered message receivers by invoking them
with the two arguments.
Arguments:
plugin (str): The sending plugin's identifier.
data (object): The message.
"""
for client in self.registered_clients:
try: client.sendPluginMessage(plugin, data)
try: client(plugin, data)
except: self.logger.exception("Exception while sending plugin data to client")

View file

@ -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 <https://github.com/OctoPrint/Plugin-Examples/tree/master/helloworld>`_.
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 <sec-plugins-gettingstarted>`.
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``: ``<pluginname>_navbar.jinja2``
* ``sidebar``: ``<pluginname>_sidebar.jinja2``
* ``tab``: ``<pluginname>_tab.jinja2``
* ``settings``: ``<pluginname>_settings.jinja2``
* ``generic``: ``<pluginname>.jinja2``
* - suffix
- Suffix to attach to the component identifier and the div identifier of the injected template. Will be
``_<index>`` 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 ``<type>_plugin_<pluginname>`` 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 <sec-configuration-config_yaml-appearance>`. 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 ``<pluginname>_navbar.jinja2``.
* - suffix
- Suffix to attach to the element ID of the injected template, will be ``_<index>`` 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_<pluginname>`` 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 <sec-plugins-templates>` 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 ``<pluginname>_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 ``_<index>`` 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_<pluginname>`` 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 <sec-plugins-templates>` 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 ``<pluginname>_tab.jinja2``.
* - suffix
- Suffix to attach to the element ID of the injected template, will be ``_<index>`` 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_<pluginname>`` 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 <sec-plugins-templates>` 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 ``<pluginname>_settings.jinja2``.
* - suffix
- Suffix to attach to the element ID of the injected template, will be ``_<index>`` 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_<pluginname>`` 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 <sec-plugins-templates>` 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 ``<pluginname>.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 []
@ -522,7 +460,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 +573,7 @@ class BlueprintPlugin(OctoPrintPlugin):
return flask.make_response("Expected a text to echo back.", 400)
return flask.request.values["text"]
__plugin_implementations__ = [MyBlueprintPlugin()]
__plugin_implementation__ = MyBlueprintPlugin()
Your blueprint will be published by OctoPrint under the base URL ``/plugin/<plugin identifier>/``, so the above
example of a plugin with the identifier "myblueprintplugin" would be reachable under
@ -761,7 +699,7 @@ class SettingsPlugin(OctoPrintPlugin):
some_flag = self._settings.get_boolean(["sub", "some_flag"])
self._logger.info("some_setting = {some_setting}, some_value = {some_value}, sub.some_flag = {some_flag}".format(**locals())
__plugin_implementations__ = [MySettingsPlugin()]
__plugin_implementation__ = MySettingsPlugin()
Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the
default implementations do not fit your requirements.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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):

View file

@ -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):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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