Docs: Started on a basic plugin tutorial

This commit is contained in:
Gina Häußge 2015-01-27 14:21:41 +01:00
parent d96e56e3c7
commit 4223d48edd
9 changed files with 364 additions and 105 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,102 +0,0 @@
.. _sec-plugins-developing:
******************
Developing Plugins
******************
.. todo::
A section on how to develop plugins yourself, including a tutorial creating an exemplary plugin as well as
a documentation of the plugin API, will be added in the near future.
.. note::
This section is still a heavy WIP, so take it with a bit of caution ;)
.. _sec-plugins-developing-structure:
OctoPrint Plugin Structure
==========================
OctoPrint plugins are simple `Python modules or packages <https://docs.python.org/2/tutorial/modules.html>`_ providing
a couple of properties describing the module:
``__plugin_name__``
Name of your plugin
``__plugin_version__``
Version of your plugin
``__plugin_description__``
Description of your plugin
``__plugin_author__``
Author of your plugin
``__plugin_url__``
URL of the webpage of your plugin, e.g. the Github repository
``__plugin_implementations__``
Instances of one or more of the various :ref:`plugin mixins <sec-plugins-developing-mixins>`
``__plugin_hooks__``
Handlers for one or more of the various :ref:`plugin hooks <sec-plugins-developing-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.
A very simple example plugin which only hooks into OctoPrint's startup sequence and logs "Oh hello!" would
be the following snippet:
.. code-block:: python
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
__plugin_name__ = "Example Plugin"
__plugin_version__ = "0.1"
__plugin_description__ = "Logs \"Oh hello!\" upon OctoPrint's startup"
def __plugin_init__():
global __plugin_implementations__
__plugin_implementations__ = [ExamplePlugin()]
class ExamplePlugin(octoprint.plugin.StartupPlugin):
def on_startup(self, host, port):
self._logger.info("Oh hello!")
.. _sec-plugins-developing-distribution:
Distributing your plugin
========================
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]_.
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.
.. 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.
.. _sec-plugins-developing-mixins:
Available plugin mixins
=======================
.. automodule:: octoprint.plugin.types
:members:
:undoc-members:

View file

@ -0,0 +1,27 @@
.. _sec-plugins-distribution:
Distributing your plugin
========================
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]_.
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.
.. 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.

View file

@ -0,0 +1,245 @@
.. _sec-plugins-gettingstarted:
Getting Started
===============
Over the course of this little tutorial we'll build a full fledged, installable OctoPrint plugin that displays "Hello World!"
at various places throughout OctoPrint.
We'll start at the most basic form a plugin can take - just a couple of simple lines of Python code:
.. code-block:: python
# coding=utf-8
from __future__ import absolute_import
__plugin_name__ = "Hello World"
__plugin_version__ = "1.0"
__plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint"
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)
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
``__plugin_description__`` and stored it in an internal data structure, but we'll just ignore this for now.
Saying hello: How to make the plugin actually do something
----------------------------------------------------------
Apart from being discovered by OctoPrint, our plugin does nothing yet. We want to change that. Let's make it print
"Hello World!" to the log upon server startup. Modify our ``helloworld.py`` like this:
.. code-block:: python
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin):
def on_after_startup(self):
self._logger.info("Hello World!")
__plugin_name__ = "Hello World"
__plugin_version__ = "1.0"
__plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint"
__plugin_implementations__ = [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:`StartupPlugin` and another control property, ``__plugin_implementations__``, that instantiates
our plugin class and tells OctoPrint about it. Taking a look at the documentation of :class:`StartupPlugin` we see that
this mixin offers two methods that get called by OctoPrint during startup of the server, ``on_startup`` and
``on_after_startup``. We decided to add our logging output by overriding ``on_after_startup``, but we could also have
used ``on_startup`` instead, in which case our logging statement would be executed before the server was done starting
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,
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>``.
Growing up: How to make it distributable
----------------------------------------
If you now want to distribute this plugin to other OctoPrint users (since it is so awesome to be greeted upon server
startup), let's take a look at how you'd go about that now before our plugin gets more complicated.
You basically have two options to distribute your plugin. One would be about the exact same way we are using it now,
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
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>`_.
So let's begin. First checkout the `Plugin Skeleton <https://github.com/OctoPrint/OctoPrint-PluginSkeleton>`_ and rename
the ``octoprint_skeleton`` folder to something better suited to our "Hello World" plugin::
git clone https://github.com/OctoPrint/OctoPrint-PluginSkeleton.git OctoPrint-HelloWorld
cd OctoPrint-HelloWorld
mv octoprint_skeleton octoprint_helloworld
Then edit the configuration in the ``setup.py`` file to mirror our own "Hello World" plugin. The configuration should
look something like this:
.. code-block:: python
plugin_identifier = "helloworld"
plugin_name = "OctoPrint-HelloWorld"
plugin_version = "1.0"
plugin_description = "A quick \"Hello World\" example plugin for OctoPrint"
plugin_author = "You"
plugin_author_email = "you@somewhere.net"
plugin_url = "https://github.com/you/OctoPrint-HelloWorld"
Now all that's left to do is to move our ``helloworld.py`` into the ``octoprint_helloworld`` folder and renaming it to
``__init__.py``. Make sure to delete the copy under ``~/.octoprint/plugins`` in the process, including the `.pyc` file!
The plugin is now ready to be installed via ``python setup.py install``. However, since we are still
working on our plugin, it makes more sense to use ``python setup.py develop`` for now -- this way the plugin becomes
discoverable by OctoPrint, however we don't have to reinstall it after any changes we will still do::
$ python setup.py develop
running develop
running egg_info
creating OctoPrint_HelloWorld.egg-info
[...]
Finished processing dependencies for OctoPrint-HelloWorld==1.0
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:38,997 - octoprint.plugins.helloworld - INFO - Hello World!
Looks like it still works!
Something is still a bit ugly though. Take a look into ``__init__.py`` and ``setup.py``. It seems like we have a bunch
of information now defined twice:
.. code-block:: python
# __init__.py:
__plugin_name__ = "Hello World"
__plugin_version__ = "1.0"
__plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint"
# 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 it:
.. code-block:: python
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin):
def on_after_startup(self):
self._logger.info("Hello World!")
__plugin_implementations__ = [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)
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 ugly, so we'll override that bit via ``__plugin_name__`` again:
.. code-block:: python
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin):
def on_after_startup(self):
self._logger.info("Hello World!")
__plugin_name__ = "Hello World"
__plugin_implementations__ = [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)
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
overrides.
Following the README of the `Plugin Skeleton <https://github.com/OctoPrint/OctoPrint-PluginSkeleton>`_ you could now
already publish your plugin on Github and it would be directly installable by others using pip::
pip install https://github.com/you/OctoPrint-HelloWorld/archive/master.zip
But let's add some more features instead.
Frontend or get out: How to add functionality to OctoPrint's web interface
--------------------------------------------------------------------------
Outputting a log line upon server startup is all nice and well, but we want to greet not only the administrator of
our OctoPrint instance but actually everyone that opens OctoPrint in their browser. Therefore, we need to modify
OctoPrint's web interface itself.
We can do this using the :class:`TemplatePlugin` mixin. For now, let's start with a little "Hello World!" in OctoPrint's
navigation bar right at the top. For this we'll first add the :class:`TemplatePlugin` to our ``HelloWorldPlugin`` class:
.. code-block:: python
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin):
def on_after_startup(self):
self._logger.info("Hello World!")
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
Next, we'll create a sub folder ``templates`` underneath our ``octoprint_helloworld`` folder, and within that a file
``helloworld_navbar.jinja2`` like so:
.. code-block:: html
<a href="https://en.wikipedia.org/wiki/Hello_world">Hello World!</a>
Our plugin's directory structure should now look like this::
|-+ octoprint_helloworld
| |-+ templates
| | `- helloworld_navbar.jinja2
| `- __init__.py
|- README.md
|- requirements.txt
`- setup.py
Restart OctoPrint and open the web interface in your browser (make sure to clear your browser's cache!).
.. _fig-plugins-gettingstarted-helloworld_navbar:
.. figure:: ../images/plugins_gettingstarted_helloworld_navbar.png
:align: center
:alt: Our "Hello World" navigation bar element in action
Now look at that!

8
docs/plugins/hooks.rst Normal file
View file

@ -0,0 +1,8 @@
.. _sec-plugins-hooks:
Available plugin hooks
======================
.. todo::
Needs to be documented

View file

@ -1,3 +1,5 @@
.. automodule:: octoprint.plugin.types
.. _sec-plugins:
#######
@ -15,4 +17,8 @@ additional slicers. More plugin types are planned for the future.
:maxdepth: 3
using.rst
developing.rst
gettingstarted.rst
infrastructure.rst
distributing.rst
mixins.rst
hooks.rst

View file

@ -0,0 +1,68 @@
.. _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._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.

8
docs/plugins/mixins.rst Normal file
View file

@ -0,0 +1,8 @@
.. _sec-plugins-mixins:
Available plugin mixins
=======================
.. automodule:: octoprint.plugin.types
:members:
:undoc-members:

View file

@ -421,10 +421,9 @@ class Server():
pluginManager.initialize_implementations(dict(
plugin_manager=pluginManager,
printer_profile_manager=printerProfileManager,
event_manager=eventManager,
event_bus=eventManager,
analysis_queue=analysisQueue,
slicing_manager=slicingManager,
storage_managers=storage_managers,
file_manager=fileManager,
printer=printer,
app_session_manager=appSessionManager,