From ce02817cd4d3cf982ca58dfcb177a0b62fa75d54 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 12 Mar 2012 18:03:35 -0700 Subject: Clean up of style test code, along with refactoring of corner cases to unittests where they belong. --- src/mailman/app/docs/styles.rst | 162 -------------------------------- src/mailman/model/tests/test_user.py | 3 + src/mailman/styles/docs/__init__.py | 0 src/mailman/styles/docs/styles.rst | 147 +++++++++++++++++++++++++++++ src/mailman/styles/tests/__init__.py | 0 src/mailman/styles/tests/test_styles.py | 90 ++++++++++++++++++ 6 files changed, 240 insertions(+), 162 deletions(-) delete mode 100644 src/mailman/app/docs/styles.rst create mode 100644 src/mailman/styles/docs/__init__.py create mode 100644 src/mailman/styles/docs/styles.rst create mode 100644 src/mailman/styles/tests/__init__.py create mode 100644 src/mailman/styles/tests/test_styles.py (limited to 'src') diff --git a/src/mailman/app/docs/styles.rst b/src/mailman/app/docs/styles.rst deleted file mode 100644 index 63ec999bf..000000000 --- a/src/mailman/app/docs/styles.rst +++ /dev/null @@ -1,162 +0,0 @@ -=========== -List styles -=========== - -List styles are a way to name and apply a canned collection of attribute -settings. Every style has a name, which must be unique within the context of -a specific style manager. There is usually only one global style manager. - -Styles also have a priority, which allows you to specify the order in which -multiple styles will be applied. A style has a `match` function which is used -to determine whether the style should be applied to a particular mailing list -or not. And finally, application of a style to a mailing list can really -modify the mailing list any way it wants. - -Let's start with a vanilla mailing list and a default style manager. -:: - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> mlist = getUtility(IListManager).create('_xtest@example.com') - - >>> from mailman.styles.manager import StyleManager - >>> style_manager = StyleManager() - >>> style_manager.populate() - >>> sorted(style.name for style in style_manager.styles) - ['default'] - - -The default style -================= - -There is a default style which implements the legacy application of list -defaults from previous versions of Mailman. This style only matching a -mailing list when no other styles match, and it has the lowest priority. The -low priority means that it is matched last and if it matches, it is applied -last. - - >>> default_style = style_manager.get('default') - >>> default_style.name - 'default' - >>> default_style.priority - 0 - >>> sorted(style.name for style in style_manager.styles) - ['default'] - -Given a mailing list, you can ask the style manager to find all the styles -that match the list. The registered styles will be sorted by decreasing -priority and each style's ``match()`` method will be called in turn. The -sorted list of matching styles will be returned -- but not applied -- by the -style manager's ``lookup()`` method. - - >>> [style.name for style in style_manager.lookup(mlist)] - ['default'] - - -Registering styles -================== - -New styles must implement the ``IStyle`` interface. - - >>> from zope.interface import implements - >>> from mailman.interfaces.styles import IStyle - >>> class TestStyle(object): - ... implements(IStyle) - ... name = 'test' - ... priority = 10 - ... def apply(self, mailing_list): - ... # Just does something very simple. - ... mailing_list.msg_footer = 'test footer' - ... def match(self, mailing_list, styles): - ... # Applies to any test list - ... if 'test' in mailing_list.fqdn_listname: - ... styles.append(self) - -You can register a new style with the style manager. - - >>> style_manager.register(TestStyle()) - -And now if you lookup matching styles, you should find only the new test -style. This is because the default style only gets applied when no other -styles match the mailing list. - - >>> sorted(style.name for style in style_manager.lookup(mlist)) - [u'test'] - >>> for style in style_manager.lookup(mlist): - ... style.apply(mlist) - >>> print mlist.msg_footer - test footer - - -Style priority -============== - -When multiple styles match a particular mailing list, they are applied in -descending order of priority. In other words, a priority zero style would be -applied last. -:: - - >>> class AnotherTestStyle(TestStyle): - ... name = 'another' - ... priority = 5 - ... # Use the base class's match() method. - ... def apply(self, mailing_list): - ... mailing_list.msg_footer = 'another footer' - - >>> mlist.msg_footer = '' - >>> mlist.msg_footer - u'' - >>> style_manager.register(AnotherTestStyle()) - >>> for style in style_manager.lookup(mlist): - ... style.apply(mlist) - >>> print mlist.msg_footer - another footer - -You can change the priority of a style, and if you reapply the styles, they -will take effect in the new priority order. - - >>> style_1 = style_manager.get('test') - >>> style_1.priority = 5 - >>> style_2 = style_manager.get('another') - >>> style_2.priority = 10 - >>> for style in style_manager.lookup(mlist): - ... style.apply(mlist) - >>> print mlist.msg_footer - test footer - - -Unregistering styles -==================== - -You can unregister a style, making it unavailable in the future. - - >>> style_manager.unregister(style_2) - >>> sorted(style.name for style in style_manager.lookup(mlist)) - [u'test'] - - -Corner cases -============ - -If you register a style with the same name as an already registered style, you -get an exception. - - >>> style_manager.register(TestStyle()) - Traceback (most recent call last): - ... - DuplicateStyleError: test - -If you try to register an object that isn't a style, you get an exception. - - >>> style_manager.register(object()) - Traceback (most recent call last): - ... - DoesNotImplement: An object does not implement interface - - -If you try to unregister a style that isn't registered, you get an exception. - - >>> style_manager.unregister(style_2) - Traceback (most recent call last): - ... - KeyError: u'another' diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py index c4c07ed22..2f04b7c3a 100644 --- a/src/mailman/model/tests/test_user.py +++ b/src/mailman/model/tests/test_user.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'TestUser', ] @@ -36,6 +37,8 @@ from mailman.utilities.datetime import now class TestUser(unittest.TestCase): + """Test users.""" + layer = ConfigLayer def setUp(self): diff --git a/src/mailman/styles/docs/__init__.py b/src/mailman/styles/docs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/mailman/styles/docs/styles.rst b/src/mailman/styles/docs/styles.rst new file mode 100644 index 000000000..90a02227b --- /dev/null +++ b/src/mailman/styles/docs/styles.rst @@ -0,0 +1,147 @@ +=========== +List styles +=========== + +List styles are a way to name and apply a template of attribute settings to +new mailing lists. Every style has a name, which must be unique within the +context of a specific style manager. There is usually only one global style +manager. + +Styles also have a priority, which allows you to specify the order in which +multiple styles will be applied. A style has a `match` function which is used +to determine whether the style should be applied to a particular mailing list +or not. And finally, application of a style to a mailing list can really +modify the mailing list any way it wants. + +Let's start with a vanilla mailing list and a default style manager. +:: + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> mlist = getUtility(IListManager).create('_xtest@example.com') + + >>> from mailman.styles.manager import StyleManager + >>> style_manager = StyleManager() + >>> style_manager.populate() + >>> styles = sorted(style.name for style in style_manager.styles) + >>> len(styles) + 1 + >>> print styles[0] + default + + +The default style +================= + +There is a default style which implements a legacy style roughly corresponding +to discussion mailing lists. This style matches when no other styles match, +and it has the lowest priority. The low priority means that it is matched +last and if it matches, it is applied last. + + >>> default_style = style_manager.get('default') + >>> print default_style.name + default + >>> default_style.priority + 0 + +Given a mailing list, you can ask the style manager to find all the styles +that match the list. The registered styles will be sorted by decreasing +priority and each style's ``match()`` method will be called in turn. The +sorted list of matching styles will be returned -- but not applied -- by the +style manager's ``lookup()`` method. + + >>> matched_styles = [style.name for style in style_manager.lookup(mlist)] + >>> len(matched_styles) + 1 + >>> print matched_styles[0] + default + + +Registering styles +================== + +New styles must implement the ``IStyle`` interface. + + >>> from zope.interface import implements + >>> from mailman.interfaces.styles import IStyle + >>> class TestStyle: + ... implements(IStyle) + ... name = 'test' + ... priority = 10 + ... def apply(self, mailing_list): + ... # Just does something very simple. + ... mailing_list.style_thing = 'thing 1' + ... def match(self, mailing_list, styles): + ... # Applies to any test list + ... if 'test' in mailing_list.fqdn_listname: + ... styles.append(self) + +You can register a new style with the style manager. + + >>> style_manager.register(TestStyle()) + +And now if you look up matching styles, you should find only the new test +style. This is because the default style only gets applied when no other +styles match the mailing list. + + >>> matched_styles = sorted( + ... style.name for style in style_manager.lookup(mlist)) + >>> len(matched_styles) + 1 + >>> print matched_styles[0] + test + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.style_thing + thing 1 + + +Style priority +============== + +When multiple styles match a particular mailing list, they are applied in +descending order of priority. In other words, a priority zero style would be +applied last. +:: + + >>> class AnotherTestStyle(TestStyle): + ... name = 'another' + ... priority = 5 + ... # Use the base class's match() method. + ... def apply(self, mailing_list): + ... mailing_list.style_thing = 'thing 2' + + >>> mlist.style_thing = 'thing 0' + >>> print mlist.style_thing + thing 0 + >>> style_manager.register(AnotherTestStyle()) + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.style_thing + thing 2 + +You can change the priority of a style, and if you reapply the styles, they +will take effect in the new priority order. + + >>> style_1 = style_manager.get('test') + >>> style_1.priority = 5 + >>> style_2 = style_manager.get('another') + >>> style_2.priority = 10 + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.style_thing + thing 1 + + +Unregistering styles +==================== + +You can unregister a style, making it unavailable in the future. + + >>> style_manager.unregister(style_2) + >>> matched_styles = sorted( + ... style.name for style in style_manager.lookup(mlist)) + >>> len(matched_styles) + 1 + >>> print matched_styles[0] + test diff --git a/src/mailman/styles/tests/__init__.py b/src/mailman/styles/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/mailman/styles/tests/test_styles.py b/src/mailman/styles/tests/test_styles.py new file mode 100644 index 000000000..ce8b5064d --- /dev/null +++ b/src/mailman/styles/tests/test_styles.py @@ -0,0 +1,90 @@ +# Copyright (C) 2012 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 . + +"""Test styles.""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + 'TestStyle', + ] + + +import unittest + +from zope.component import getUtility +from zope.interface import implements +from zope.interface.exceptions import DoesNotImplement + +from mailman.interfaces.styles import ( + DuplicateStyleError, IStyle, IStyleManager) +from mailman.testing.layers import ConfigLayer + + + +class DummyStyle: + implements(IStyle) + + name = 'dummy' + priority = 1 + + def apply(self, mlist): + pass + + def match(self, mlist, styles): + styles.append(self) + + + +class TestStyle(unittest.TestCase): + """Test styles.""" + + layer = ConfigLayer + + def setUp(self): + self.manager = getUtility(IStyleManager) + + def test_register_style_again(self): + # Registering a style with the same name as a previous style raises an + # exception. + self.manager.register(DummyStyle()) + try: + self.manager.register(DummyStyle()) + except DuplicateStyleError: + pass + else: + raise AssertionError('DuplicateStyleError exception expected') + + def test_register_a_non_style(self): + # You can't register something that doesn't implement the IStyle + # interface. + try: + self.manager.register(object()) + except DoesNotImplement: + pass + else: + raise AssertionError('DoesNotImplement exception expected') + + def test_unregister_a_non_registered_style(self): + # You cannot unregister a style that hasn't yet been registered. + try: + self.manager.unregister(DummyStyle()) + except KeyError: + pass + else: + raise AssertionError('KeyError expected') -- cgit v1.2.3-70-g09d2 From 5f69c4e7c9ee532065e4c19a862f915b2178d482 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 12 Mar 2012 21:30:49 -0700 Subject: * The experimental `maildir` runner is removed. Use LMTP. --- src/mailman/commands/docs/inject.rst | 1 - src/mailman/config/mailman.cfg | 5 - src/mailman/docs/NEWS.rst | 1 + src/mailman/runners/maildir.py | 195 ----------------------------------- src/mailman/testing/testing.cfg | 3 - 5 files changed, 1 insertion(+), 204 deletions(-) delete mode 100644 src/mailman/runners/maildir.py (limited to 'src') diff --git a/src/mailman/commands/docs/inject.rst b/src/mailman/commands/docs/inject.rst index 1c0843ff3..e8fc088c0 100644 --- a/src/mailman/commands/docs/inject.rst +++ b/src/mailman/commands/docs/inject.rst @@ -35,7 +35,6 @@ It's easy to find out which queues are available. digest in lmtp - maildir news out pipeline diff --git a/src/mailman/config/mailman.cfg b/src/mailman/config/mailman.cfg index 22c3a129b..0d37ceed9 100644 --- a/src/mailman/config/mailman.cfg +++ b/src/mailman/config/mailman.cfg @@ -61,11 +61,6 @@ class: mailman.runners.incoming.IncomingRunner [runner.lmtp] class: mailman.runners.lmtp.LMTPRunner -[runner.maildir] -class: mailman.runners.maildir.MaildirRunner -# This is still experimental. -start: no - [runner.news] class: mailman.runners.news.NewsRunner diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 7993795a1..80b17f30b 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -36,6 +36,7 @@ Architecture `owners_chain`. The default `built-in` chain is renamed to `default-posting-chain` while the `built-in` pipeline is renamed `default-posting-pipeline`. + * The experimental `maildir` runner is removed. Use LMTP. Database -------- diff --git a/src/mailman/runners/maildir.py b/src/mailman/runners/maildir.py deleted file mode 100644 index 2d3a49285..000000000 --- a/src/mailman/runners/maildir.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (C) 2002-2012 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 . - -"""Maildir runner. - -Most MTAs can be configured to deliver messages to a `Maildir'[1]. This -runner will read messages from a maildir's new/ directory and inject them into -Mailman's qfiles/in directory for processing in the normal pipeline. This -delivery mechanism contrasts with mail program delivery, where incoming -messages end up in qfiles/in via the MTA executing the scripts/post script -(and likewise for the other -aliases for each mailing list). - -The advantage to Maildir delivery is that it is more efficient; there's no -need to fork an intervening program just to take the message from the MTA's -standard output, to the qfiles/in directory. - -[1] http://cr.yp.to/proto/maildir.html - -We're going to use the :info flag == 1, experimental status flag for our own -purposes. The :1 can be followed by one of these letters: - -- P means that MaildirRunner's in the process of parsing and enqueuing the - message. If successful, it will delete the file. - -- X means something failed during the parse/enqueue phase. An error message - will be logged to log/error and the file will be renamed :1,X. - MaildirRunner will never automatically return to this file, but once the - problem is fixed, you can manually move the file back to the new/ directory - and MaildirRunner will attempt to re-process it. At some point we may do - this automatically. - -See the variable USE_MAILDIR in Defaults.py.in for enabling this delivery -mechanism. -""" - -# NOTE: Maildir delivery is experimental in Mailman 2.1, and untested in -# Mailman 3. Instead, use LMTP delivery for Mailman 3. - -import os -import errno -import logging - -from email.parser import Parser -from email.utils import parseaddr - -from mailman.config import config -from mailman.core.runner import Runner -from mailman.core.switchboard import Switchboard -from mailman.message import Message - - -log = logging.getLogger('mailman.error') - -# We only care about the listname and the subq as in listname@ or -# listname-request@ -subqnames = ('admin', 'bounces', 'confirm', 'join', 'leave', - 'owner', 'request', 'subscribe', 'unsubscribe') - - -def getlistq(address): - localpart, domain = address.split('@', 1) - # TK: FIXME I only know configs of Postfix. - if config.POSTFIX_STYLE_VIRTUAL_DOMAINS: - p = localpart.split(config.POSTFIX_VIRTUAL_SEPARATOR, 1) - if len(p) == 2: - localpart, domain = p - l = localpart.split('-') - if l[-1] in subqnames: - listname = '-'.join(l[:-1]) - subq = l[-1] - else: - listname = localpart - subq = None - return listname, subq, domain - - - -class MaildirRunner(Runner): - # This class is much different than most runners because it pulls files - # of a different format than what scripts/post and friends leaves. The - # files this runner reads are just single message files as dropped into - # the directory by the MTA. This runner will read the file, and enqueue - # it in the expected qfiles directory for normal processing. - def __init__(self, slice=None, numslices=1): - # Don't call the base class constructor, but build enough of the - # underlying attributes to use the base class's implementation. - self._stop = 0 - self._dir = os.path.join(config.MAILDIR_DIR, 'new') - self._cur = os.path.join(config.MAILDIR_DIR, 'cur') - self._parser = Parser(Message) - - def _one_iteration(self): - # Refresh this each time through the list. - listnames = list(config.list_manager.names) - # Cruise through all the files currently in the new/ directory - try: - files = os.listdir(self._dir) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - # Nothing's been delivered yet - return 0 - for file in files: - srcname = os.path.join(self._dir, file) - dstname = os.path.join(self._cur, file + ':1,P') - xdstname = os.path.join(self._cur, file + ':1,X') - try: - os.rename(srcname, dstname) - except OSError, e: - if e.errno == errno.ENOENT: - # Some other MaildirRunner beat us to it - continue - log.error('Could not rename maildir file: %s', srcname) - raise - # Now open, read, parse, and enqueue this message - try: - fp = open(dstname) - try: - msg = self._parser.parse(fp) - finally: - fp.close() - # Now we need to figure out which queue of which list this - # message was destined for. See get_verp() in - # mailman.app.bounces for why we do things this way. - vals = [] - for header in ('delivered-to', 'envelope-to', 'apparently-to'): - vals.extend(msg.get_all(header, [])) - for field in vals: - to = parseaddr(field)[1].lower() - if not to: - continue - listname, subq, domain = getlistq(to) - listname = listname + '@' + domain - if listname in listnames: - break - else: - # As far as we can tell, this message isn't destined for - # any list on the system. What to do? - log.error('Message apparently not for any list: %s', - xdstname) - os.rename(dstname, xdstname) - continue - # BAW: blech, hardcoded - msgdata = {'listname': listname} - # -admin is deprecated - if subq in ('bounces', 'admin'): - queue = Switchboard('bounces', config.BOUNCEQUEUE_DIR) - elif subq == 'confirm': - msgdata['toconfirm'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - elif subq in ('join', 'subscribe'): - msgdata['tojoin'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - elif subq in ('leave', 'unsubscribe'): - msgdata['toleave'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - elif subq == 'owner': - msgdata.update({ - 'toowner': True, - 'envsender': config.SITE_OWNER_ADDRESS, - 'pipeline': config.OWNER_PIPELINE, - }) - queue = Switchboard('in', config.INQUEUE_DIR) - elif subq is None: - msgdata['tolist'] = 1 - queue = Switchboard('in', config.INQUEUE_DIR) - elif subq == 'request': - msgdata['torequest'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - else: - log.error('Unknown sub-queue: %s', subq) - os.rename(dstname, xdstname) - continue - queue.enqueue(msg, msgdata) - os.unlink(dstname) - except Exception, e: - os.rename(dstname, xdstname) - log.error('%s', e) - - def _clean_up(self): - pass diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg index 89d986a7c..526093572 100644 --- a/src/mailman/testing/testing.cfg +++ b/src/mailman/testing/testing.cfg @@ -45,9 +45,6 @@ max_restarts: 1 [runner.lmtp] max_restarts: 1 -[runner.maildir] -max_restarts: 1 - [runner.news] max_restarts: 1 -- cgit v1.2.3-70-g09d2