summaryrefslogtreecommitdiff
path: root/src/mailman/plugins
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
parentf847e15407bfbf824236547bdf728a1ae00bd405 (diff)
downloadmailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.tar.gz
mailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.tar.zst
mailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.zip
Diffstat (limited to 'src/mailman/plugins')
-rw-r--r--src/mailman/plugins/__init__.py0
-rw-r--r--src/mailman/plugins/docs/__init__.py25
-rw-r--r--src/mailman/plugins/docs/intro.rst207
-rw-r--r--src/mailman/plugins/initialize.py54
-rw-r--r--src/mailman/plugins/testing/__init__.py0
-rw-r--r--src/mailman/plugins/testing/alternate.cfg7
-rw-r--r--src/mailman/plugins/testing/alternate/__init__.py0
-rw-r--r--src/mailman/plugins/testing/alternate/rules/__init__.py0
-rw-r--r--src/mailman/plugins/testing/alternate/rules/rules.py14
-rw-r--r--src/mailman/plugins/testing/example/__init__.py0
-rw-r--r--src/mailman/plugins/testing/example/hooks.py21
-rw-r--r--src/mailman/plugins/testing/example/rest.py79
-rw-r--r--src/mailman/plugins/testing/example/rules/__init__.py0
-rw-r--r--src/mailman/plugins/testing/example/rules/rules.py14
-rw-r--r--src/mailman/plugins/testing/hooks.cfg3
-rw-r--r--src/mailman/plugins/testing/layer.py53
-rw-r--r--src/mailman/plugins/testing/rest.cfg6
-rw-r--r--src/mailman/plugins/testing/showrules.py6
-rw-r--r--src/mailman/plugins/tests/__init__.py0
-rw-r--r--src/mailman/plugins/tests/test_plugins.py146
20 files changed, 635 insertions, 0 deletions
diff --git a/src/mailman/plugins/__init__.py b/src/mailman/plugins/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/__init__.py
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
diff --git a/src/mailman/plugins/initialize.py b/src/mailman/plugins/initialize.py
new file mode 100644
index 000000000..431f69a6f
--- /dev/null
+++ b/src/mailman/plugins/initialize.py
@@ -0,0 +1,54 @@
+# 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/>.
+
+"""Initialize the plugins."""
+
+import logging
+
+from lazr.config import as_boolean
+from mailman.config import config
+from mailman.interfaces.plugin import IPlugin
+from mailman.utilities.modules import call_name
+from public import public
+from zope.interface.exceptions import DoesNotImplement
+from zope.interface.verify import verifyObject
+
+
+log = logging.getLogger('mailman.plugins')
+
+
+@public
+def initialize():
+ """Initialize all enabled plugins."""
+ for name, plugin_config in config.plugin_configs:
+ class_path = plugin_config['class'].strip()
+ if not as_boolean(plugin_config['enabled']) or len(class_path) == 0:
+ log.info('Plugin not enabled, or empty class path: {}'.format(
+ name))
+ continue
+ if name in config.plugins:
+ log.error('Duplicate plugin name: {}'.format(name))
+ continue
+ plugin = call_name(class_path)
+ try:
+ verifyObject(IPlugin, plugin)
+ except DoesNotImplement:
+ log.error('Plugin class does not implement IPlugin: {}'.format(
+ class_path))
+ continue
+ plugin.name = name
+ config.plugins[name] = plugin
diff --git a/src/mailman/plugins/testing/__init__.py b/src/mailman/plugins/testing/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/testing/__init__.py
diff --git a/src/mailman/plugins/testing/alternate.cfg b/src/mailman/plugins/testing/alternate.cfg
new file mode 100644
index 000000000..6cf6ea4f3
--- /dev/null
+++ b/src/mailman/plugins/testing/alternate.cfg
@@ -0,0 +1,7 @@
+[plugin.example]
+class: example.hooks.ExamplePlugin
+enabled: yes
+component_package: alternate
+
+[logging.plugins]
+propagate: yes
diff --git a/src/mailman/plugins/testing/alternate/__init__.py b/src/mailman/plugins/testing/alternate/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/testing/alternate/__init__.py
diff --git a/src/mailman/plugins/testing/alternate/rules/__init__.py b/src/mailman/plugins/testing/alternate/rules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/testing/alternate/rules/__init__.py
diff --git a/src/mailman/plugins/testing/alternate/rules/rules.py b/src/mailman/plugins/testing/alternate/rules/rules.py
new file mode 100644
index 000000000..f8b8d725c
--- /dev/null
+++ b/src/mailman/plugins/testing/alternate/rules/rules.py
@@ -0,0 +1,14 @@
+from mailman.interfaces.rules import IRule
+from public import public
+from zope.interface import implementer
+
+
+@public
+@implementer(IRule)
+class AlternateRule:
+ name = 'alternate-rule'
+ description = 'An alternate rule.'
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ return 'alternate' in msgdata
diff --git a/src/mailman/plugins/testing/example/__init__.py b/src/mailman/plugins/testing/example/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/testing/example/__init__.py
diff --git a/src/mailman/plugins/testing/example/hooks.py b/src/mailman/plugins/testing/example/hooks.py
new file mode 100644
index 000000000..55c09c4a5
--- /dev/null
+++ b/src/mailman/plugins/testing/example/hooks.py
@@ -0,0 +1,21 @@
+import os
+
+from mailman.interfaces.plugin import IPlugin
+from public import public
+from zope.interface import implementer
+
+
+@public
+@implementer(IPlugin)
+class ExamplePlugin:
+ def pre_hook(self):
+ if os.environ.get('DEBUG_HOOKS'):
+ print("I'm in my pre-hook")
+
+ def post_hook(self):
+ if os.environ.get('DEBUG_HOOKS'):
+ print("I'm in my post-hook")
+
+ @property
+ def resource(self):
+ return None
diff --git a/src/mailman/plugins/testing/example/rest.py b/src/mailman/plugins/testing/example/rest.py
new file mode 100644
index 000000000..1d83ce2cd
--- /dev/null
+++ b/src/mailman/plugins/testing/example/rest.py
@@ -0,0 +1,79 @@
+from mailman.config import config
+from mailman.interfaces.plugin import IPlugin
+from mailman.rest.helpers import bad_request, child, etag, no_content, okay
+from mailman.rest.validator import Validator
+from public import public
+from zope.interface import implementer
+
+
+@public
+class Yes:
+ def on_get(self, request, response):
+ okay(response, etag(dict(yes=True)))
+
+
+@public
+class No:
+ def on_get(self, request, response):
+ bad_request(response, etag(dict(no=False)))
+
+
+@public
+class NumberEcho:
+ def __init__(self):
+ self._plugin = config.plugins['example']
+
+ def on_get(self, request, response):
+ okay(response, etag(dict(number=self._plugin.number)))
+
+ def on_post(self, request, response):
+ try:
+ resource = Validator(number=int)(request)
+ self._plugin.number = resource['number']
+ except ValueError as error:
+ bad_request(response, str(error))
+ else:
+ no_content(response)
+
+ def on_delete(self, request, response):
+ self._plugin.number = 0
+ no_content(response)
+
+
+@public
+class RESTExample:
+ def on_get(self, request, response):
+ resource = {
+ 'my-name': 'example-plugin',
+ 'my-child-resources': 'yes, no, echo',
+ }
+ okay(response, etag(resource))
+
+ @child()
+ def yes(self, context, segments):
+ return Yes(), []
+
+ @child()
+ def no(self, context, segments):
+ return No(), []
+
+ @child()
+ def echo(self, context, segments):
+ return NumberEcho(), []
+
+
+@public
+@implementer(IPlugin)
+class ExamplePlugin:
+ def __init__(self):
+ self.number = 0
+
+ def pre_hook(self):
+ pass
+
+ def post_hook(self):
+ pass
+
+ @property
+ def resource(self):
+ return RESTExample()
diff --git a/src/mailman/plugins/testing/example/rules/__init__.py b/src/mailman/plugins/testing/example/rules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/testing/example/rules/__init__.py
diff --git a/src/mailman/plugins/testing/example/rules/rules.py b/src/mailman/plugins/testing/example/rules/rules.py
new file mode 100644
index 000000000..7e9ba6ce7
--- /dev/null
+++ b/src/mailman/plugins/testing/example/rules/rules.py
@@ -0,0 +1,14 @@
+from mailman.interfaces.rules import IRule
+from public import public
+from zope.interface import implementer
+
+
+@public
+@implementer(IRule)
+class ExampleRule:
+ name = 'example-rule'
+ description = 'An example rule.'
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ return 'example' in msgdata
diff --git a/src/mailman/plugins/testing/hooks.cfg b/src/mailman/plugins/testing/hooks.cfg
new file mode 100644
index 000000000..15fb31815
--- /dev/null
+++ b/src/mailman/plugins/testing/hooks.cfg
@@ -0,0 +1,3 @@
+[plugin.example]
+class: example.hooks.ExamplePlugin
+enabled: yes
diff --git a/src/mailman/plugins/testing/layer.py b/src/mailman/plugins/testing/layer.py
new file mode 100644
index 000000000..311a419f4
--- /dev/null
+++ b/src/mailman/plugins/testing/layer.py
@@ -0,0 +1,53 @@
+# 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."""
+
+import os
+
+from contextlib import ExitStack
+from mailman.testing.helpers import (
+ TestableMaster, hackenv, wait_for_webservice)
+from mailman.testing.layers import SMTPLayer
+from pkg_resources import resource_filename
+from public import public
+
+
+# Don't inherit from RESTLayer since layers get run in bottom up order,
+# meaning RESTLayer will get setUp() before this layer does, and that won't
+# use the configuration file we need it to use.
+@public
+class PluginRESTLayer(SMTPLayer):
+ @classmethod
+ def setUp(cls):
+ cls.resources = ExitStack()
+ plugin_path = os.path.join(
+ os.path.dirname(
+ resource_filename('mailman.plugins', '__init__.py')),
+ 'testing')
+ config_file = resource_filename('mailman.plugins.testing', 'rest.cfg')
+ cls.resources.enter_context(
+ hackenv('MAILMAN_CONFIG_FILE', config_file))
+ cls.resources.enter_context(
+ hackenv('PYTHONPATH', plugin_path))
+ cls.server = TestableMaster(wait_for_webservice)
+ cls.server.start('rest')
+ cls.resources.callback(cls.server.stop)
+
+ @classmethod
+ def tearDown(cls):
+ cls.resources.close()
diff --git a/src/mailman/plugins/testing/rest.cfg b/src/mailman/plugins/testing/rest.cfg
new file mode 100644
index 000000000..a9b4b9bd9
--- /dev/null
+++ b/src/mailman/plugins/testing/rest.cfg
@@ -0,0 +1,6 @@
+[plugin.example]
+class: example.rest.ExamplePlugin
+enabled: yes
+
+[webservice]
+port: 9001
diff --git a/src/mailman/plugins/testing/showrules.py b/src/mailman/plugins/testing/showrules.py
new file mode 100644
index 000000000..926ef0f70
--- /dev/null
+++ b/src/mailman/plugins/testing/showrules.py
@@ -0,0 +1,6 @@
+from mailman.config import config
+
+
+def showrules():
+ for name in sorted(config.rules):
+ print(name)
diff --git a/src/mailman/plugins/tests/__init__.py b/src/mailman/plugins/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/plugins/tests/__init__.py
diff --git a/src/mailman/plugins/tests/test_plugins.py b/src/mailman/plugins/tests/test_plugins.py
new file mode 100644
index 000000000..4b432f603
--- /dev/null
+++ b/src/mailman/plugins/tests/test_plugins.py
@@ -0,0 +1,146 @@
+# 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/>.
+
+"""Test some additional plugin stuff."""
+
+import sys
+import unittest
+
+from contextlib import ExitStack
+from mailman.interfaces.plugin import IPlugin
+from mailman.plugins.initialize import initialize
+from mailman.plugins.testing.layer import PluginRESTLayer
+from mailman.testing.helpers import call_api
+from mailman.testing.layers import ConfigLayer
+from tempfile import TemporaryDirectory
+from types import SimpleNamespace
+from unittest.mock import patch
+from urllib.error import HTTPError
+from zope.interface import implementer
+
+
+class TestRESTPlugin(unittest.TestCase):
+ layer = PluginRESTLayer
+
+ def test_plugin_raises_exception(self):
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.1/plugins/example/no')
+ self.assertEqual(cm.exception.code, 400)
+
+
+@implementer(IPlugin)
+class TestablePlugin:
+ def pre_hook(self):
+ pass
+
+ def post_hook(self):
+ pass
+
+ resource = None
+
+
+class TestInitializePlugins(unittest.TestCase):
+ layer = ConfigLayer
+
+ def test_duplicate_plugin_name(self):
+ with ExitStack() as resources:
+ system_path = resources.enter_context(TemporaryDirectory())
+ fake_plugin_config = {
+ 'path': system_path,
+ 'enabled': 'yes',
+ 'class': 'ExamplePlugin',
+ }
+ log_mock = resources.enter_context(
+ patch('mailman.plugins.initialize.log'))
+ fake_mailman_config = SimpleNamespace(
+ plugin_configs=[('example', fake_plugin_config)],
+ plugins=['example'],
+ )
+ resources.enter_context(patch(
+ 'mailman.plugins.initialize.config', fake_mailman_config))
+ initialize()
+ log_mock.error.assert_called_once_with(
+ 'Duplicate plugin name: example')
+
+ def test_does_not_implement(self):
+ with ExitStack() as resources:
+ system_path = resources.enter_context(TemporaryDirectory())
+ fake_plugin_config = {
+ 'path': system_path,
+ 'enabled': 'yes',
+ 'class': 'ExamplePlugin',
+ }
+ log_mock = resources.enter_context(
+ patch('mailman.plugins.initialize.log'))
+ fake_mailman_config = SimpleNamespace(
+ plugin_configs=[('example', fake_plugin_config)],
+ plugins=[],
+ )
+ resources.enter_context(patch(
+ 'mailman.plugins.initialize.config', fake_mailman_config))
+ resources.enter_context(patch(
+ 'mailman.plugins.initialize.call_name',
+ # object() does not implement IPlugin.
+ return_value=object()))
+ initialize()
+ log_mock.error.assert_called_once_with(
+ 'Plugin class does not implement IPlugin: ExamplePlugin')
+ self.assertNotIn(system_path, sys.path)
+
+ def test_adds_plugins_to_config(self):
+ with ExitStack() as resources:
+ system_path = resources.enter_context(TemporaryDirectory())
+ fake_plugin_config = {
+ 'path': system_path,
+ 'enabled': 'yes',
+ 'class': 'ExamplePlugin',
+ }
+ fake_mailman_config = SimpleNamespace(
+ plugin_configs=[('example', fake_plugin_config)],
+ plugins={},
+ )
+ resources.enter_context(patch(
+ 'mailman.plugins.initialize.config', fake_mailman_config))
+ testable_plugin = TestablePlugin()
+ resources.enter_context(patch(
+ 'mailman.plugins.initialize.call_name',
+ # object() does not implement IPlugin.
+ return_value=testable_plugin))
+ initialize()
+ self.assertIn('example', fake_mailman_config.plugins)
+ self.assertEqual(
+ fake_mailman_config.plugins['example'],
+ testable_plugin)
+
+ def test_not_enabled(self):
+ with ExitStack() as resources:
+ fake_plugin_config = {
+ 'path': '/does/not/exist',
+ 'enabled': 'no',
+ 'class': 'ExamplePlugin',
+ }
+ log_mock = resources.enter_context(
+ patch('mailman.plugins.initialize.log'))
+ fake_mailman_config = SimpleNamespace(
+ plugin_configs=[('example', fake_plugin_config)],
+ plugins={},
+ )
+ resources.enter_context(patch(
+ 'mailman.plugins.initialize.config', fake_mailman_config))
+ initialize()
+ log_mock.info.assert_called_once_with(
+ 'Plugin not enabled, or empty class path: example')