Merge branch 'devel' into dev/pluginLifecycleMgmt
Conflicts: src/octoprint/plugin/core.py
This commit is contained in:
commit
3ebf5e5240
33 changed files with 904 additions and 468 deletions
|
|
@ -37,7 +37,8 @@
|
|||
* Controls for adjusting feed and flow rate factor added to Controls ([#362](https://github.com/foosel/OctoPrint/issues/362))
|
||||
* Custom controls now also support slider controls
|
||||
* Custom controls now support a row layout
|
||||
* Users can now define custom GCODE scripts to run upon starting/pausing/resuming/success/failure of a print
|
||||
* Users can now define custom GCODE scripts to run upon starting/pausing/resuming/success/failure of a print or for
|
||||
custom controls ([#457](https://github.com/foosel/OctoPrint/issues/457), [#347](https://github.com/foosel/OctoPrint/issues/347))
|
||||
|
||||
### Improvements
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
--------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ Features
|
|||
custom_controls.rst
|
||||
gcode_scripts.rst
|
||||
action_commands.rst
|
||||
plugins.rst
|
||||
|
|
|
|||
64
docs/features/plugins.rst
Normal file
64
docs/features/plugins.rst
Normal 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``.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.. _sec-plugins-templates:
|
||||
|
||||
Templates
|
||||
=========
|
||||
|
||||
.. todo::
|
||||
|
||||
Needs to be written.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -257,11 +257,11 @@ class FileManager(object):
|
|||
|
||||
if progress_int:
|
||||
def call_plugins(slicer, source_location, source_path, dest_location, dest_path, progress):
|
||||
for name, plugin in self._progress_plugins.items():
|
||||
for plugin in self._progress_plugins:
|
||||
try:
|
||||
plugin.on_slicing_progress(slicer, source_location, source_path, dest_location, dest_path, progress)
|
||||
except:
|
||||
self._logger.exception("Exception while sending slicing progress to plugin %s" % name)
|
||||
self._logger.exception("Exception while sending slicing progress to plugin %s" % plugin._identifier)
|
||||
|
||||
import threading
|
||||
thread = threading.Thread(target=call_plugins, args=(slicer, source_location, source_path, dest_location, dest_path, progress_int))
|
||||
|
|
|
|||
|
|
@ -164,16 +164,16 @@ def call_plugin(types, method, args=None, kwargs=None, callback=None, error_call
|
|||
kwargs = dict()
|
||||
|
||||
plugins = plugin_manager().get_implementations(*types)
|
||||
for name, plugin in plugins.items():
|
||||
for plugin in plugins:
|
||||
if hasattr(plugin, method):
|
||||
try:
|
||||
result = getattr(plugin, method)(*args, **kwargs)
|
||||
if callback:
|
||||
callback(name, plugin, result)
|
||||
callback(plugin._identifier, plugin, result)
|
||||
except Exception as exc:
|
||||
logging.getLogger(__name__).exception("Error while calling plugin %s" % name)
|
||||
logging.getLogger(__name__).exception("Error while calling plugin %s" % plugin._identifier)
|
||||
if error_callback:
|
||||
error_callback(name, plugin, exc)
|
||||
error_callback(plugin._identifier, plugin, exc)
|
||||
|
||||
|
||||
class PluginSettings(object):
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -420,4 +420,4 @@ __plugin_author__ = "Gina Häußge"
|
|||
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
|
||||
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
|
||||
__plugin_license__ = "AGPLv3"
|
||||
__plugin_implementations__ = [CuraPlugin()]
|
||||
__plugin_implementation__ = CuraPlugin()
|
||||
|
|
@ -33,20 +33,20 @@ def __plugin_init__():
|
|||
# no pybonjour available, we can't use that
|
||||
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available")
|
||||
|
||||
discovery_plugin = DiscoveryPlugin()
|
||||
plugin = DiscoveryPlugin()
|
||||
|
||||
global __plugin_implementations__
|
||||
__plugin_implementations__ = [discovery_plugin]
|
||||
global __plugin_implementation__
|
||||
__plugin_implementation__ = plugin
|
||||
|
||||
global __plugin_helpers__
|
||||
__plugin_helpers__ = dict(
|
||||
ssdp_browse=discovery_plugin.ssdp_browse
|
||||
ssdp_browse=plugin.ssdp_browse
|
||||
)
|
||||
if pybonjour:
|
||||
__plugin_helpers__.update(dict(
|
||||
zeroconf_browse=discovery_plugin.zeroconf_browse,
|
||||
zeroconf_register=discovery_plugin.zeroconf_register,
|
||||
zeroconf_unregister=discovery_plugin.zeroconf_unregister
|
||||
zeroconf_browse=plugin.zeroconf_browse,
|
||||
zeroconf_register=plugin.zeroconf_register,
|
||||
zeroconf_unregister=plugin.zeroconf_unregister
|
||||
))
|
||||
|
||||
class DiscoveryPlugin(octoprint.plugin.StartupPlugin,
|
||||
|
|
|
|||
|
|
@ -172,11 +172,11 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
filename = self._selectedFile["filename"]
|
||||
|
||||
def call_plugins(storage, filename, progress):
|
||||
for name, plugin in self._progressPlugins.items():
|
||||
for plugin in self._progressPlugins:
|
||||
try:
|
||||
plugin.on_print_progress(storage, filename, progress)
|
||||
except:
|
||||
self._logger.exception("Exception while sending print progress to plugin %s" % name)
|
||||
self._logger.exception("Exception while sending print progress to plugin %s" % plugin._identifier)
|
||||
|
||||
thread = threading.Thread(target=call_plugins, args=(storage, filename, progress))
|
||||
thread.daemon = False
|
||||
|
|
|
|||
|
|
@ -175,7 +175,8 @@ def index():
|
|||
assets["stylesheets"].append(("css", url_for('static', filename='css/octoprint.css')))
|
||||
|
||||
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
|
||||
for name, implementation in asset_plugins.items():
|
||||
for implementation in asset_plugins:
|
||||
name = implementation._identifier
|
||||
all_assets = implementation.get_assets()
|
||||
|
||||
if "js" in all_assets:
|
||||
|
|
@ -285,8 +286,11 @@ def index():
|
|||
)
|
||||
|
||||
plugin_vars = dict()
|
||||
plugin_names = template_plugins.keys()
|
||||
for name, implementation in template_plugins.items():
|
||||
plugin_names = set()
|
||||
for implementation in template_plugins:
|
||||
name = implementation._identifier
|
||||
plugin_names.add(name)
|
||||
|
||||
vars = implementation.get_template_vars()
|
||||
if not isinstance(vars, dict):
|
||||
vars = dict()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -48,11 +48,14 @@ api.after_request(corsResponseHandler)
|
|||
|
||||
@api.route("/plugin/<string:name>", methods=["GET"])
|
||||
def pluginData(name):
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
|
||||
if not name in api_plugins:
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.SimpleApiPlugin)
|
||||
if not api_plugins:
|
||||
return make_response("Not found", 404)
|
||||
|
||||
api_plugin = api_plugins[name]
|
||||
if len(api_plugins) > 1:
|
||||
return make_response("More than one api provider registered for {name}, can't proceed".format(name=name), 500)
|
||||
|
||||
api_plugin = api_plugins[0]
|
||||
response = api_plugin.on_api_get(request)
|
||||
|
||||
if response is not None:
|
||||
|
|
@ -64,11 +67,15 @@ def pluginData(name):
|
|||
@api.route("/plugin/<string:name>", methods=["POST"])
|
||||
@restricted_access
|
||||
def pluginCommand(name):
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
|
||||
if not name in api_plugins:
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.SimpleApiPlugin)
|
||||
|
||||
if not api_plugins:
|
||||
return make_response("Not found", 404)
|
||||
|
||||
api_plugin = api_plugins[name]
|
||||
if len(api_plugins) > 1:
|
||||
return make_response("More than one api provider registered for {name}, can't proceed".format(name=name), 500)
|
||||
|
||||
api_plugin = api_plugins[0]
|
||||
valid_commands = api_plugin.get_api_commands()
|
||||
if valid_commands is None:
|
||||
return make_response("Method not allowed", 405)
|
||||
|
|
|
|||
|
|
@ -224,9 +224,10 @@ def setSettings():
|
|||
s.saveScript("gcode", name, script.replace("\r\n", "\n").replace("\r", "\n"))
|
||||
|
||||
if "plugins" in data:
|
||||
for name, plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin).items():
|
||||
if name in data["plugins"]:
|
||||
plugin.on_settings_save(data["plugins"][name])
|
||||
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
|
||||
plugin_id = plugin._identifier
|
||||
if plugin_id in data["plugins"]:
|
||||
plugin.on_settings_save(data["plugins"][plugin_id])
|
||||
|
||||
|
||||
if s.save():
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ def _get_registered_apps():
|
|||
apps[app]["enabled"] = True
|
||||
|
||||
app_plugins = octoprint.server.pluginManager.get_implementations(octoprint.plugin.AppPlugin)
|
||||
for name, plugin in app_plugins.items():
|
||||
for plugin in app_plugins:
|
||||
additional_apps = plugin.get_additional_apps()
|
||||
any_version_enabled = dict()
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
|
|||
self._printer.register_callback(self)
|
||||
self._fileManager.register_slicingprogress_callback(self)
|
||||
octoprint.timelapse.registerCallback(self)
|
||||
self._pluginManager.register_client(self)
|
||||
self._pluginManager.register_message_receiver(self.on_plugin_message)
|
||||
|
||||
self._eventManager.fire(Events.CLIENT_OPENED, {"remoteAddress": self._remoteAddress})
|
||||
for event in octoprint.events.all_events():
|
||||
|
|
@ -67,7 +67,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
|
|||
self._printer.unregister_callback(self)
|
||||
self._fileManager.unregister_slicingprogress_callback(self)
|
||||
octoprint.timelapse.unregisterCallback(self)
|
||||
self._pluginManager.unregister_client(self)
|
||||
self._pluginManager.unregister_message_receiver(self.on_plugin_message)
|
||||
|
||||
self._eventManager.fire(Events.CLIENT_CLOSED, {"remoteAddress": self._remoteAddress})
|
||||
for event in octoprint.events.all_events():
|
||||
|
|
@ -119,7 +119,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
|
|||
dict(slicer=slicer, source_location=source_location, source_path=source_path, dest_location=dest_location, dest_path=dest_path, progress=progress)
|
||||
)
|
||||
|
||||
def sendPluginMessage(self, plugin, data):
|
||||
def on_plugin_message(self, plugin, data):
|
||||
self._emit("plugin", dict(plugin=plugin, data=data))
|
||||
|
||||
def on_printer_add_log(self, data):
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ default_settings = {
|
|||
"apps": {}
|
||||
},
|
||||
"terminalFilters": [
|
||||
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok T\d*:)" },
|
||||
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok (B|T\d*):)" },
|
||||
{ "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" }
|
||||
],
|
||||
"plugins": {},
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
16
tests/plugin/_plugins/deprecated_plugin.py
Normal file
16
tests/plugin/_plugins/deprecated_plugin.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
class TestDeprecatedAssetPlugin(octoprint.plugin.AssetPlugin):
|
||||
pass
|
||||
|
||||
|
||||
class TestSecondaryDeprecatedAssetPlugin(octoprint.plugin.AssetPlugin):
|
||||
pass
|
||||
|
||||
|
||||
__plugin_name__ = "Deprecated Plugin"
|
||||
__plugin_description__ = "Test deprecated plugin"
|
||||
__plugin_implementations__ = [TestDeprecatedAssetPlugin(), TestSecondaryDeprecatedAssetPlugin()]
|
||||
|
|
@ -14,4 +14,4 @@ class TestMixedPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsP
|
|||
|
||||
__plugin_name__ = "Mixed Plugin"
|
||||
__plugin_description__ = "Test mixed plugin"
|
||||
__plugin_implementations__ = (TestMixedPlugin(),)
|
||||
__plugin_implementation__ = TestMixedPlugin()
|
||||
|
|
@ -9,4 +9,4 @@ class TestSettingsPlugin(octoprint.plugin.SettingsPlugin):
|
|||
|
||||
__plugin_name__ = "Settings Plugin"
|
||||
__plugin_description__ = "Test settings plugin"
|
||||
__plugin_implementations__ = (TestSettingsPlugin(),)
|
||||
__plugin_implementation__ = TestSettingsPlugin()
|
||||
|
|
@ -9,4 +9,4 @@ class TestStartupPlugin(octoprint.plugin.StartupPlugin):
|
|||
|
||||
__plugin_name__ = "Startup Plugin"
|
||||
__plugin_description__ = "Test startup plugin"
|
||||
__plugin_implementations__ = (TestStartupPlugin(),)
|
||||
__plugin_implementation__ = TestStartupPlugin()
|
||||
|
|
@ -17,25 +17,33 @@ class PluginTestCase(unittest.TestCase):
|
|||
self.plugin_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "_plugins")
|
||||
|
||||
plugin_folders = [self.plugin_folder]
|
||||
plugin_types = [octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin]
|
||||
plugin_types = [octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.AssetPlugin]
|
||||
plugin_entry_points = None
|
||||
self.plugin_manager = octoprint.plugin.core.PluginManager(plugin_folders, plugin_types, plugin_entry_points, plugin_disabled_list=[], logging_prefix="logging_prefix.")
|
||||
self.plugin_manager.initialize_implementations()
|
||||
|
||||
def test_plugin_loading(self):
|
||||
self.assertEquals(4, len(self.plugin_manager.plugins))
|
||||
self.assertEquals(5, len(self.plugin_manager.plugins))
|
||||
self.assertEquals(1, len(self.plugin_manager.plugin_hooks))
|
||||
self.assertEquals(3, len(self.plugin_manager.plugin_implementations))
|
||||
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type))
|
||||
self.assertEquals(4, len(self.plugin_manager.plugin_implementations))
|
||||
self.assertEquals(3, len(self.plugin_manager.plugin_implementations_by_type))
|
||||
|
||||
# hook_plugin
|
||||
self.assertTrue("octoprint.core.startup" in self.plugin_manager.plugin_hooks)
|
||||
self.assertEquals(1, len(self.plugin_manager.plugin_hooks["octoprint.core.startup"]))
|
||||
|
||||
# TestStartupPlugin & TestMixedPlugin
|
||||
self.assertTrue(octoprint.plugin.StartupPlugin in self.plugin_manager.plugin_implementations_by_type)
|
||||
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.StartupPlugin]))
|
||||
|
||||
# TestSettingsPlugin & TestMixedPlugin
|
||||
self.assertTrue(octoprint.plugin.SettingsPlugin in self.plugin_manager.plugin_implementations_by_type)
|
||||
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.SettingsPlugin]))
|
||||
|
||||
# TestDeprecatedAssetPlugin, NOT TestSecondaryDeprecatedAssetPlugin
|
||||
self.assertTrue(octoprint.plugin.AssetPlugin in self.plugin_manager.plugin_implementations_by_type)
|
||||
self.assertEquals(1, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.AssetPlugin]))
|
||||
|
||||
def test_plugin_initializing(self):
|
||||
|
||||
def test_factory(name, implementation):
|
||||
|
|
@ -55,11 +63,8 @@ class PluginTestCase(unittest.TestCase):
|
|||
)
|
||||
|
||||
all_implementations = self.plugin_manager.plugin_implementations
|
||||
self.assertEquals(3, len(all_implementations))
|
||||
for name, implementations in all_implementations.items():
|
||||
self.assertEquals(1, len(implementations))
|
||||
impl = implementations.pop()
|
||||
|
||||
self.assertEquals(4, len(all_implementations))
|
||||
for name, impl in all_implementations.items():
|
||||
self.assertTrue(name in self.plugin_manager.plugins)
|
||||
plugin = self.plugin_manager.plugins[name]
|
||||
|
||||
|
|
@ -97,6 +102,14 @@ class PluginTestCase(unittest.TestCase):
|
|||
plugin = self.plugin_manager.get_plugin("unknown_plugin")
|
||||
self.assertIsNone(plugin)
|
||||
|
||||
def test_get_plugin_info(self):
|
||||
plugin_info = self.plugin_manager.get_plugin_info("hook_plugin")
|
||||
self.assertIsNotNone(plugin_info)
|
||||
self.assertEquals("Hook Plugin", plugin_info.name)
|
||||
|
||||
plugin_info = self.plugin_manager.get_plugin_info("unknown_plugin")
|
||||
self.assertIsNone(plugin_info)
|
||||
|
||||
def test_get_hooks(self):
|
||||
hooks = self.plugin_manager.get_hooks("octoprint.core.startup")
|
||||
self.assertEquals(1, len(hooks))
|
||||
|
|
@ -108,44 +121,49 @@ class PluginTestCase(unittest.TestCase):
|
|||
|
||||
def test_get_implementation(self):
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.StartupPlugin)
|
||||
self.assertEquals(2, len(implementations))
|
||||
self.assertTrue('startup_plugin' in implementations)
|
||||
self.assertTrue('mixed_plugin' in implementations)
|
||||
self.assertEquals(2, len(implementations)) # startup_plugin, mixed_plugin
|
||||
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
self.assertEquals(2, len(implementations))
|
||||
self.assertTrue('settings_plugin' in implementations)
|
||||
self.assertTrue('mixed_plugin' in implementations)
|
||||
self.assertEquals(2, len(implementations)) # settings_plugin, mixed_plugin
|
||||
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin)
|
||||
self.assertEquals(1, len(implementations))
|
||||
self.assertTrue('mixed_plugin' in implementations)
|
||||
self.assertEquals(1, len(implementations)) # mixed_plugin
|
||||
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.AssetPlugin)
|
||||
self.assertEquals(1, len(implementations)) # deprecated_plugin, but only first implementation!
|
||||
|
||||
def test_client_registration(self):
|
||||
client = mock.Mock()
|
||||
def test_client(*args, **kwargs):
|
||||
pass
|
||||
|
||||
self.assertEquals(0, len(self.plugin_manager.registered_clients))
|
||||
|
||||
self.plugin_manager.register_client(client)
|
||||
self.plugin_manager.register_message_receiver(test_client)
|
||||
|
||||
self.assertEquals(1, len(self.plugin_manager.registered_clients))
|
||||
self.assertIn(client, self.plugin_manager.registered_clients)
|
||||
self.assertIn(test_client, self.plugin_manager.registered_clients)
|
||||
|
||||
self.plugin_manager.unregister_client(client)
|
||||
self.plugin_manager.unregister_message_receiver(test_client)
|
||||
|
||||
self.assertEquals(0, len(self.plugin_manager.registered_clients))
|
||||
self.assertNotIn(client, self.plugin_manager.registered_clients)
|
||||
self.assertNotIn(test_client, self.plugin_manager.registered_clients)
|
||||
|
||||
def test_send_plugin_message(self):
|
||||
client1 = mock.Mock()
|
||||
client2 = mock.Mock()
|
||||
|
||||
self.plugin_manager.register_client(client1)
|
||||
self.plugin_manager.register_client(client2)
|
||||
self.plugin_manager.register_message_receiver(client1.on_plugin_message)
|
||||
self.plugin_manager.register_message_receiver(client2.on_plugin_message)
|
||||
|
||||
plugin = "some plugin"
|
||||
data = "some data"
|
||||
self.plugin_manager.send_plugin_message(plugin, data)
|
||||
client1.sendPluginMessage.assert_called_once_with(plugin, data)
|
||||
client2.sendPluginMessage.assert_called_once_with(plugin, data)
|
||||
client1.on_plugin_message.assert_called_once_with(plugin, data)
|
||||
client2.on_plugin_message.assert_called_once_with(plugin, data)
|
||||
|
||||
def test_validate_plugin(self):
|
||||
self.assertTrue("deprecated_plugin" in self.plugin_manager.plugins)
|
||||
|
||||
plugin = self.plugin_manager.plugins["deprecated_plugin"]
|
||||
self.assertTrue(hasattr(plugin.instance, plugin.__class__.attr_implementation))
|
||||
self.assertFalse(hasattr(plugin.instance, plugin.__class__.attr_implementations))
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class TestSlicingManager(unittest.TestCase):
|
|||
def get_implementations(*types):
|
||||
import octoprint.plugin
|
||||
if octoprint.plugin.SlicerPlugin in types:
|
||||
return dict(("slicer_" + plugin.get_slicer_properties()["type"], plugin) for plugin in plugins)
|
||||
return plugins
|
||||
return dict()
|
||||
self.plugin_manager.return_value.get_implementations.side_effect = get_implementations
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue