diff options
Diffstat (limited to 'src/mailman/plugins/docs')
| -rw-r--r-- | src/mailman/plugins/docs/__init__.py | 25 | ||||
| -rw-r--r-- | src/mailman/plugins/docs/intro.rst | 207 |
2 files changed, 232 insertions, 0 deletions
diff --git a/src/mailman/plugins/docs/__init__.py b/src/mailman/plugins/docs/__init__.py new file mode 100644 index 000000000..0b3957648 --- /dev/null +++ b/src/mailman/plugins/docs/__init__.py @@ -0,0 +1,25 @@ +# Copyright (C) 2017 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""REST layer for plugins.""" + + +from mailman.plugins.testing.layer import PluginRESTLayer +from public import public + +# For flufl.testing. +public(layer=PluginRESTLayer) diff --git a/src/mailman/plugins/docs/intro.rst b/src/mailman/plugins/docs/intro.rst new file mode 100644 index 000000000..be6fb365b --- /dev/null +++ b/src/mailman/plugins/docs/intro.rst @@ -0,0 +1,207 @@ +========= + Plugins +========= + +Mailman defines a plugin as a Python package on ``sys.path`` that provides +components matching the ``IPlugin`` interface. ``IPlugin`` implementations +can define a *pre-hook*, a *post-hook*, and a *REST resource*. Plugins are +enabled by adding a section to your ``mailman.cfg`` file, such as: + +.. literalinclude:: ../testing/hooks.cfg + +.. note:: + Because of a `design limitation`_ in the underlying configuration library, + you cannot name a plugin "master". Specifically you cannot define a + section in your ``mailman.cfg`` file named ``[plugin.master]``. + +We have such a configuration file handy. + + >>> from pkg_resources import resource_filename + >>> config_file = resource_filename('mailman.plugins.testing', 'hooks.cfg') + +The section must at least define the class implementing the ``IPlugin`` +interface, using a Python dotted-name import path. For the import to work, +you must include the top-level directory on ``sys.path``. + + >>> import os + >>> from pkg_resources import resource_filename + >>> plugin_path = os.path.join(os.path.dirname( + ... resource_filename('mailman.plugins', '__init__.py')), + ... 'testing') + + +Hooks +===== + +Plugins can add initialization hooks, which will be run at two stages in the +initialization process - one before the database is initialized and one after. +These correspond to methods the plugin defines, a ``pre_hook()`` method and a +``post_hook()`` method. Each of these methods are optional. + +Here is a plugin that defines these hooks: + +.. literalinclude:: ../testing/example/hooks.py + +To illustrate how the hooks work, we'll invoke a simple Mailman command to be +run in a subprocess. The plugin itself supports debugging hooking invocation +when an environment variable is set. + + >>> proc = run(['-C', config_file, 'info'], + ... DEBUG_HOOKS='1', + ... PYTHONPATH=plugin_path) + >>> print(proc.stdout) + I'm in my pre-hook + I'm in my post-hook + ... + + +Components +========== + +Plugins can also add components such as rules, chains, list styles, etc. By +default, components are searched for in the package matching the plugin's +name. So in the case above, the plugin is named ``example`` (because the +section is called ``[plugin.example]``, and there is a subpackage called +``rules`` under the ``example`` package. The file system layout looks like +this:: + + example/ + __init__.py + hooks.py + rules/ + __init__.py + rules.py + +And the contents of ``rules.py`` looks like: + +.. literalinclude:: ../testing/example/rules/rules.py + +To see that the plugin's rule get added, we invoke Mailman as an external +process, running a script that prints out all the defined rule names, +including our plugin's ``example-rule``. + + >>> proc = run(['-C', config_file, 'withlist', '-r', 'showrules'], + ... PYTHONPATH=plugin_path) + >>> print(proc.stdout) + administrivia + ... + example-rule + ... + +Component directories can live under any importable path, not just one named +after the plugin. By adding a ``component_package`` section to your plugin's +configuration, you can name an alternative location to search for components. + +.. literalinclude:: ../testing/alternate.cfg + +We use this configuration file and the following file system layout:: + + example/ + __init__.py + hooks.py + alternate/ + rules/ + __init__.py + rules.py + +Here, ``rules.py`` likes like: + +.. literalinclude:: ../testing/alternate/rules/rules.py + +You can see that this rule has a different name. If we use the +``alternate.cfg`` configuration file from above:: + + >>> config_file = resource_filename( + ... 'mailman.plugins.testing', 'alternate.cfg') + +we'll pick up the alternate rule when we print them out. + + >>> proc = run(['-C', config_file, 'withlist', '-r', 'showrules'], + ... PYTHONPATH=plugin_path) + >>> print(proc.stdout) + administrivia + alternate-rule + ... + + +REST +==== + +Plugins can also supply REST routes. Let's say we have a plugin defined like +so: + +.. literalinclude:: ../testing/example/rest.py + +which we can enable with the following configuration file: + +.. literalinclude:: ../testing/rest.cfg + +The plugin defines a ``resource`` attribute that exposes the root of the +plugin's resource tree. The plugin will show up when we navigate to the +``plugin`` resource. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins') + entry 0: + class: example.rest.ExamplePlugin + enabled: True + http_etag: "..." + name: example + http_etag: "..." + start: 0 + total_size: 1 + +The plugin may provide a ``GET`` on the resource itself. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins/example') + http_etag: "..." + my-child-resources: yes, no, echo + my-name: example-plugin + +And it may provide child resources. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins/example/yes') + http_etag: "..." + yes: True + +Plugins and their child resources can support any HTTP method, such as +``GET``... +:: + + >>> dump_json('http://localhost:9001/3.1/plugins/example/echo') + http_etag: "..." + number: 0 + +... or ``POST`` ... +:: + + >>> dump_json('http://localhost:9001/3.1/plugins/example/echo', + ... dict(number=7)) + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.1/plugins/example/echo') + http_etag: "..." + number: 7 + +... or ``DELETE``. + + >>> dump_json('http://localhost:9001/3.1/plugins/example/echo', + ... method='DELETE') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.1/plugins/example/echo') + http_etag: "..." + number: 0 + +It's up to the plugin of course. + + +.. _`design limitation`: https://bugs.launchpad.net/lazr.config/+bug/310619 |
