summaryrefslogtreecommitdiff
path: root/src/mailman/plugins/docs
diff options
context:
space:
mode:
authorBarry Warsaw2017-08-29 14:07:54 +0000
committerBarry Warsaw2017-08-29 14:07:54 +0000
commitae0042a90220119414f61aeb20c6b58bfacb8af2 (patch)
tree6fd2038427fbb36d8173fe338d277351cd19727b /src/mailman/plugins/docs
parentf847e15407bfbf824236547bdf728a1ae00bd405 (diff)
downloadmailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.tar.gz
mailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.tar.zst
mailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.zip
Diffstat (limited to 'src/mailman/plugins/docs')
-rw-r--r--src/mailman/plugins/docs/__init__.py25
-rw-r--r--src/mailman/plugins/docs/intro.rst207
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