1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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
|