diff options
Diffstat (limited to 'src/mailman/app')
| -rw-r--r-- | src/mailman/app/commands.py | 4 | ||||
| -rw-r--r-- | src/mailman/app/docs/hooks.rst | 121 | ||||
| -rw-r--r-- | src/mailman/app/docs/plugins.rst | 210 |
3 files changed, 212 insertions, 123 deletions
diff --git a/src/mailman/app/commands.py b/src/mailman/app/commands.py index 7cf3bc4c5..ca257ffda 100644 --- a/src/mailman/app/commands.py +++ b/src/mailman/app/commands.py @@ -19,11 +19,11 @@ from mailman.config import config from mailman.interfaces.command import IEmailCommand -from mailman.utilities.modules import add_components +from mailman.utilities.plugins import add_pluggable_components from public import public @public def initialize(): """Initialize the email commands.""" - add_components('mailman.commands', IEmailCommand, config.commands) + add_pluggable_components('commands', IEmailCommand, config.commands) diff --git a/src/mailman/app/docs/hooks.rst b/src/mailman/app/docs/hooks.rst deleted file mode 100644 index 07d0ec33c..000000000 --- a/src/mailman/app/docs/hooks.rst +++ /dev/null @@ -1,121 +0,0 @@ -===== -Hooks -===== - -Mailman defines two initialization hooks, one which is run early in the -initialization process and the other run late in the initialization process. -Hooks name an importable callable so it must be accessible on ``sys.path``. -:: - - >>> import os, sys - >>> from mailman.config import config - >>> config_directory = os.path.dirname(config.filename) - >>> sys.path.insert(0, config_directory) - - >>> hook_path = os.path.join(config_directory, 'hooks.py') - >>> with open(hook_path, 'w') as fp: - ... print("""\ - ... counter = 1 - ... def pre_hook(): - ... global counter - ... print('pre-hook:', counter) - ... counter += 1 - ... - ... def post_hook(): - ... global counter - ... print('post-hook:', counter) - ... counter += 1 - ... """, file=fp) - >>> fp.close() - - -Pre-hook -======== - -We can set the pre-hook in the configuration file. - - >>> config_path = os.path.join(config_directory, 'hooks.cfg') - >>> with open(config_path, 'w') as fp: - ... print("""\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... """, file=fp) - -The hooks are run in the second and third steps of initialization. However, -we can't run those initialization steps in process, so call a command line -script that will produce no output to force the hooks to run. -:: - - >>> import subprocess - >>> from mailman.testing.layers import ConfigLayer - >>> def call(): - ... exe = os.path.join(os.path.dirname(sys.executable), 'mailman') - ... env = os.environ.copy() - ... env.update( - ... MAILMAN_CONFIG_FILE=config_path, - ... PYTHONPATH=config_directory, - ... ) - ... test_cfg = os.environ.get('MAILMAN_EXTRA_TESTING_CFG') - ... if test_cfg is not None: - ... env['MAILMAN_EXTRA_TESTING_CFG'] = test_cfg - ... proc = subprocess.Popen( - ... [exe, 'lists', '--domain', 'ignore', '-q'], - ... cwd=ConfigLayer.root_directory, env=env, - ... universal_newlines=True, - ... stdout=subprocess.PIPE, stderr=subprocess.PIPE) - ... stdout, stderr = proc.communicate() - ... assert proc.returncode == 0, stderr - ... print(stdout) - - >>> call() - pre-hook: 1 - <BLANKLINE> - - >>> os.remove(config_path) - - -Post-hook -========= - -We can set the post-hook in the configuration file. -:: - - >>> with open(config_path, 'w') as fp: - ... print("""\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... post_hook: hooks.post_hook - ... """, file=fp) - - >>> call() - post-hook: 1 - <BLANKLINE> - - >>> os.remove(config_path) - - -Running both hooks -================== - -We can set the pre- and post-hooks in the configuration file. -:: - - >>> with open(config_path, 'w') as fp: - ... print("""\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... post_hook: hooks.post_hook - ... """, file=fp) - - >>> call() - pre-hook: 1 - post-hook: 2 - <BLANKLINE> diff --git a/src/mailman/app/docs/plugins.rst b/src/mailman/app/docs/plugins.rst new file mode 100644 index 000000000..4430179ae --- /dev/null +++ b/src/mailman/app/docs/plugins.rst @@ -0,0 +1,210 @@ +======= +Plugins +======= + +Mailman defines a plugin as a package accessible on ``sys.path`` that provides +components Mailman will use. Such add handlers, rules, chains, etc... +First we create an example plugin that provides one additional rule: +:: + + >>> import os, sys + >>> from mailman.config import config + >>> config_directory = os.path.dirname(config.filename) + >>> sys.path.insert(0, config_directory) + + >>> example_plugin_path = os.path.join(config_directory, 'example_plugin') + >>> rules_path = os.path.join(example_plugin_path, 'rules') + >>> os.makedirs(rules_path) + >>> open(os.path.join(example_plugin_path, '__init__.py'), 'a').close() + >>> open(os.path.join(rules_path, '__init__.py'), 'a').close() + >>> rule_path = os.path.join(rules_path, 'example_rule.py') + >>> with open(rule_path, 'w') as fp: + ... print("""\ + ... from mailman.interfaces.rules import IRule + ... from public import public + ... from zope.interface import implementer + ... + ... @public + ... @implementer(IRule) + ... class ExampleRule: + ... + ... name = 'example' + ... description = 'Example rule to show pluggable components.' + ... record = True + ... + ... def check(self, mlist, msg, msgdata): + ... return msg.original_size > 1024 + ... """, file=fp) + >>> fp.close() + +Then enable the example plugin in config. +:: + + >>> example_config = """\ + ... [plugin.example] + ... path: example_plugin + ... enable: yes + ... """ + >>> config.push('example_cfg', example_config) + +Now the `example` rule can be seen as an ``IRule`` component and will be used +when any chain uses a link called `example`. +:: + + >>> from mailman.interfaces.rules import IRule + >>> from mailman.utilities.plugins import find_pluggable_components + >>> rules = sorted([rule.name + ... for rule in find_pluggable_components('rules', IRule)]) + >>> for rule in rules: + ... print(rule) + administrivia + any + approved + banned-address + dmarc-mitigation + emergency + example + implicit-dest + loop + max-recipients + max-size + member-moderation + news-moderation + no-senders + no-subject + nonmember-moderation + suspicious-header + truth + + + >>> config.pop('example_cfg') + >>> from shutil import rmtree + >>> rmtree(example_plugin_path) + + +Hooks +===== + +Plugins can also add initialization hooks, which will be run during the +initialization process. By creating a class implementing the IPlugin interface. +One which is run early in the process and the other run late in the +initialization process. +:: + + >>> hook_path = os.path.join(config_directory, 'hooks.py') + >>> with open(hook_path, 'w') as fp: + ... print("""\ + ... from mailman.interfaces.plugin import IPlugin + ... from public import public + ... from zope.interface import implementer + ... + ... counter = 1 + ... + ... @public + ... @implementer(IPlugin) + ... class ExamplePlugin: + ... + ... def pre_hook(self): + ... global counter + ... print('pre-hook:', counter) + ... counter += 1 + ... + ... def post_hook(self): + ... global counter + ... print('post-hook:', counter) + ... counter += 1 + ... + ... def rest_object(self): + ... pass + ... """, file=fp) + +Running the hooks +----------------- + +We can set the plugin class in the config file. +:: + + >>> config_path = os.path.join(config_directory, 'hooks.cfg') + >>> with open(config_path, 'w') as fp: + ... print("""\ + ... [meta] + ... extends: test.cfg + ... + ... [plugin.hook_example] + ... class: hooks.ExamplePlugin + ... enable: yes + ... """, file=fp) + +The hooks are run in the second and third steps of initialization. However, +we can't run those initialization steps in process, so call a command line +script that will produce no output to force the hooks to run. +:: + + >>> import subprocess + >>> from mailman.testing.layers import ConfigLayer + >>> def call(cfg_path, python_path): + ... exe = os.path.join(os.path.dirname(sys.executable), 'mailman') + ... env = os.environ.copy() + ... env.update( + ... MAILMAN_CONFIG_FILE=cfg_path, + ... PYTHONPATH=python_path, + ... ) + ... test_cfg = os.environ.get('MAILMAN_EXTRA_TESTING_CFG') + ... if test_cfg is not None: + ... env['MAILMAN_EXTRA_TESTING_CFG'] = test_cfg + ... proc = subprocess.Popen( + ... [exe, 'lists', '--domain', 'ignore', '-q'], + ... cwd=ConfigLayer.root_directory, env=env, + ... universal_newlines=True, + ... stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ... stdout, stderr = proc.communicate() + ... assert proc.returncode == 0, stderr + ... print(stdout) + ... print(stderr) + + >>> call(config_path, config_directory) + pre-hook: 1 + post-hook: 2 + <BLANKLINE> + <BLANKLINE> + + >>> os.remove(config_path) + +Deprecated hooks +---------------- + +The old-style `pre_hook` and `post_hook` callables are deprecated and are no +longer called upon startup. +:: + + >>> deprecated_hook_path = os.path.join(config_directory, 'deprecated_hooks.py') + >>> with open(deprecated_hook_path, 'w') as fp: + ... print("""\ + ... def do_something(): + ... print("does something") + ... + ... def do_something_else(): + ... print("does something else") + ... + ... """, file=fp) + + >>> deprecated_config_path = os.path.join(config_directory, 'deprecated.cfg') + >>> with open(deprecated_config_path, 'w') as fp: + ... print("""\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... pre_hook: deprecated_hooks.do_something + ... post_hook: deprecated_hooks.do_something_else + ... """, file=fp) + + >>> call(deprecated_config_path, config_directory) + does something + does something else + <BLANKLINE> + ... UserWarning: The pre_hook configuration value has been replaced by the plugins infrastructure. ... + ... UserWarning: The post_hook configuration value has been replaced by the plugins infrastructure. ... + <BLANKLINE> + + >>> os.remove(deprecated_config_path) |
