summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/commands.py2
-rw-r--r--src/mailman/app/docs/lifecycle.rst118
-rw-r--r--src/mailman/app/finder.py78
-rw-r--r--src/mailman/app/lifecycle.py12
-rw-r--r--src/mailman/app/tests/test_lifecycle.py50
-rw-r--r--src/mailman/app/tests/test_subscriptions.py4
-rw-r--r--src/mailman/bin/mailman.py2
-rw-r--r--src/mailman/config/schema.cfg14
-rw-r--r--src/mailman/core/chains.py2
-rw-r--r--src/mailman/core/pipelines.py3
-rw-r--r--src/mailman/core/rules.py2
-rw-r--r--src/mailman/database/schema/mm_20121015000000.py4
-rw-r--r--src/mailman/database/schema/sqlite_20121015000000_01.sql1
-rw-r--r--src/mailman/database/tests/test_migrations.py23
-rw-r--r--src/mailman/interfaces/mailinglist.py3
-rw-r--r--src/mailman/interfaces/styles.py48
-rw-r--r--src/mailman/model/mailinglist.py1
-rw-r--r--src/mailman/runners/tests/test_bounce.py6
-rw-r--r--src/mailman/styles/default.py8
-rw-r--r--src/mailman/styles/docs/styles.rst187
-rw-r--r--src/mailman/styles/manager.py37
-rw-r--r--src/mailman/utilities/modules.py53
22 files changed, 293 insertions, 365 deletions
diff --git a/src/mailman/app/commands.py b/src/mailman/app/commands.py
index 70f0a8fa6..ad0b9d4a0 100644
--- a/src/mailman/app/commands.py
+++ b/src/mailman/app/commands.py
@@ -27,9 +27,9 @@ __all__ = [
from zope.interface.verify import verifyObject
-from mailman.app.finder import find_components
from mailman.config import config
from mailman.interfaces.command import IEmailCommand
+from mailman.utilities.modules import find_components
diff --git a/src/mailman/app/docs/lifecycle.rst b/src/mailman/app/docs/lifecycle.rst
index f6bb7ddae..ee7185373 100644
--- a/src/mailman/app/docs/lifecycle.rst
+++ b/src/mailman/app/docs/lifecycle.rst
@@ -12,72 +12,18 @@ which performs additional tasks such as:
* validating the list's posting address (which also serves as the list's
fully qualified name);
* ensuring that the list's domain is registered;
- * applying all matching styles to the new list;
+ * :ref:`applying a list style <list-creation-styles>` to the new list;
* creating and assigning list owners;
* notifying watchers of list creation;
* creating ancillary artifacts (such as the list's on-disk directory)
-Posting address validation
-==========================
-
-If you try to use the higher-level interface to create a mailing list with a
-bogus posting address, you get an exception.
-
- >>> create_list('not a valid address')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: not a valid address
-
-If the posting address is valid, but the domain has not been registered with
-Mailman yet, you get an exception.
-
- >>> create_list('test@example.org')
- Traceback (most recent call last):
- ...
- BadDomainSpecificationError: example.org
-
-
-Creating a list applies its styles
-==================================
-
-Start by registering a test style.
-::
-
- >>> from zope.interface import implementer
- >>> from mailman.interfaces.styles import IStyle
- >>> @implementer(IStyle)
- ... class TestStyle(object):
- ... 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)
-
- >>> from zope.component import getUtility
- >>> from mailman.interfaces.styles import IStyleManager
- >>> getUtility(IStyleManager).register(TestStyle())
-
-Using the higher level interface for creating a list, applies all matching
-list styles.
-
- >>> mlist_1 = create_list('test_1@example.com')
- >>> print mlist_1.fqdn_listname
- test_1@example.com
- >>> print mlist_1.msg_footer
- test footer
-
-
Creating a list with owners
===========================
You can also specify a list of owner email addresses. If these addresses are
not yet known, they will be registered, and new users will be linked to them.
-However the addresses are not verified.
+::
>>> owners = [
... 'aperson@example.com',
@@ -85,12 +31,9 @@ However the addresses are not verified.
... 'cperson@example.com',
... 'dperson@example.com',
... ]
- >>> mlist_2 = create_list('test_2@example.com', owners)
- >>> print mlist_2.fqdn_listname
- test_2@example.com
- >>> print mlist_2.msg_footer
- test footer
- >>> dump_list(address.email for address in mlist_2.owners.addresses)
+
+ >>> ant = create_list('ant@example.com', owners)
+ >>> dump_list(address.email for address in ant.owners.addresses)
aperson@example.com
bperson@example.com
cperson@example.com
@@ -99,37 +42,33 @@ However the addresses are not verified.
None of the owner addresses are verified.
>>> any(address.verified_on is not None
- ... for address in mlist_2.owners.addresses)
+ ... for address in ant.owners.addresses)
False
However, all addresses are linked to users.
- >>> # The owners have no names yet
- >>> len(list(mlist_2.owners.users))
- 4
-
-If you create a mailing list with owner addresses that are already known to
-the system, they won't be created again.
-::
-
>>> from mailman.interfaces.usermanager import IUserManager
+ >>> from zope.component import getUtility
>>> user_manager = getUtility(IUserManager)
+ >>> for address in owners:
+ ... user = user_manager.get_user(address)
+ ... print int(user.user_id.int), list(user.addresses)[0]
+ 1 aperson@example.com
+ 2 bperson@example.com
+ 3 cperson@example.com
+ 4 dperson@example.com
- >>> user_a = user_manager.get_user('aperson@example.com')
- >>> user_b = user_manager.get_user('bperson@example.com')
- >>> user_c = user_manager.get_user('cperson@example.com')
- >>> user_d = user_manager.get_user('dperson@example.com')
- >>> user_a.display_name = 'Anne Person'
- >>> user_b.display_name = 'Bart Person'
- >>> user_c.display_name = 'Caty Person'
- >>> user_d.display_name = 'Dirk Person'
+If you create a mailing list with owner addresses that are already known to
+the system, they won't be created again.
- >>> mlist_3 = create_list('test_3@example.com', owners)
- >>> dump_list(user.display_name for user in mlist_3.owners.users)
- Anne Person
- Bart Person
- Caty Person
- Dirk Person
+ >>> bee = create_list('bee@example.com', owners)
+ >>> from operator import attrgetter
+ >>> for user in sorted(bee.owners.users, key=attrgetter('user_id')):
+ ... print int(user.user_id.int), list(user.addresses)[0]
+ 1 aperson@example.com
+ 2 bperson@example.com
+ 3 cperson@example.com
+ 4 dperson@example.com
Deleting a list
@@ -140,17 +79,16 @@ artifacts.
::
>>> from mailman.app.lifecycle import remove_list
- >>> remove_list(mlist_2)
+ >>> remove_list(bee)
>>> from mailman.interfaces.listmanager import IListManager
- >>> from zope.component import getUtility
- >>> print getUtility(IListManager).get('test_2@example.com')
+ >>> print getUtility(IListManager).get('bee@example.com')
None
We should now be able to completely recreate the mailing list.
- >>> mlist_2a = create_list('test_2@example.com', owners)
- >>> dump_list(address.email for address in mlist_2a.owners.addresses)
+ >>> buzz = create_list('bee@example.com', owners)
+ >>> dump_list(address.email for address in bee.owners.addresses)
aperson@example.com
bperson@example.com
cperson@example.com
diff --git a/src/mailman/app/finder.py b/src/mailman/app/finder.py
deleted file mode 100644
index 679da483a..000000000
--- a/src/mailman/app/finder.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (C) 2009-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 <http://www.gnu.org/licenses/>.
-
-"""Find various kinds of object in package space."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'find_components',
- 'scan_module',
- ]
-
-
-import os
-import sys
-
-from pkg_resources import resource_listdir
-
-
-
-def scan_module(module, interface):
- """Return all the items in a module that conform to an interface.
-
- :param module: A module object. The module's `__all__` will be scanned.
- :type module: module
- :param interface: The interface that returned objects must conform to.
- :type interface: `Interface`
- :return: The sequence of matching components.
- :rtype: objects implementing `interface`
- """
- missing = object()
- for name in module.__all__:
- component = getattr(module, name, missing)
- assert component is not missing, (
- '%s has bad __all__: %s' % (module, name))
- if interface.implementedBy(component):
- yield component
-
-
-def find_components(package, interface):
- """Find components which conform to a given interface.
-
- Search all the modules in a given package, returning an iterator over all
- objects found that conform to the given interface.
-
- :param package: The package path to search.
- :type package: string
- :param interface: The interface that returned objects must conform to.
- :type interface: `Interface`
- :return: The sequence of matching components.
- :rtype: objects implementing `interface`
- """
- for filename in resource_listdir(package, ''):
- basename, extension = os.path.splitext(filename)
- if extension != '.py':
- continue
- module_name = '{0}.{1}'.format(package, basename)
- __import__(module_name, fromlist='*')
- module = sys.modules[module_name]
- if not hasattr(module, '__all__'):
- continue
- for component in scan_module(module, interface):
- yield component
diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py
index 326498478..9dfa5a9a2 100644
--- a/src/mailman/app/lifecycle.py
+++ b/src/mailman/app/lifecycle.py
@@ -47,7 +47,7 @@ log = logging.getLogger('mailman.error')
-def create_list(fqdn_listname, owners=None):
+def create_list(fqdn_listname, owners=None, style_name=None):
"""Create the named list and apply styles.
The mailing may not exist yet, but the domain specified in `fqdn_listname`
@@ -57,6 +57,9 @@ def create_list(fqdn_listname, owners=None):
:type fqdn_listname: string
:param owners: The mailing list owners.
:type owners: list of string email addresses
+ :param style_name: The name of the style to apply to the newly created
+ list. If not given, the default is taken from the configuration file.
+ :type style_name: string
:return: The new mailing list.
:rtype: `IMailingList`
:raises BadDomainSpecificationError: when the hostname part of
@@ -66,13 +69,16 @@ def create_list(fqdn_listname, owners=None):
"""
if owners is None:
owners = []
- # This raises I
+ # This raises InvalidEmailAddressError if the address is not a valid
+ # posting address. Let these percolate up.
getUtility(IEmailValidator).validate(fqdn_listname)
listname, domain = fqdn_listname.split('@', 1)
if domain not in getUtility(IDomainManager):
raise BadDomainSpecificationError(domain)
mlist = getUtility(IListManager).create(fqdn_listname)
- for style in getUtility(IStyleManager).lookup(mlist):
+ style = getUtility(IStyleManager).get(
+ config.styles.default if style_name is None else style_name)
+ if style is not None:
style.apply(mlist)
# Coordinate with the MTA, as defined in the configuration file.
call_name(config.mta.incoming).create(mlist)
diff --git a/src/mailman/app/tests/test_lifecycle.py b/src/mailman/app/tests/test_lifecycle.py
new file mode 100644
index 000000000..f0bcdad3f
--- /dev/null
+++ b/src/mailman/app/tests/test_lifecycle.py
@@ -0,0 +1,50 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Test the high level list lifecycle API."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestLifecycle',
+ ]
+
+
+import unittest
+
+from mailman.interfaces.address import InvalidEmailAddressError
+from mailman.interfaces.domain import BadDomainSpecificationError
+from mailman.app.lifecycle import create_list
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestLifecycle(unittest.TestCase):
+ """Test the high level list lifecycle API."""
+
+ layer = ConfigLayer
+
+ def test_posting_address_validation(self):
+ # Creating a mailing list with a bogus address raises an exception.
+ self.assertRaises(InvalidEmailAddressError,
+ create_list, 'bogus address')
+
+ def test_unregistered_domain(self):
+ # Creating a list with an unregistered domain raises an exception.
+ self.assertRaises(BadDomainSpecificationError,
+ create_list, 'test@nodomain.example.org')
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index c4c8f2795..8771390c1 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -34,7 +34,6 @@ from mailman.app.lifecycle import create_list
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.subscriptions import (
MissingUserError, ISubscriptionService)
-from mailman.testing.helpers import reset_the_world
from mailman.testing.layers import ConfigLayer
@@ -46,9 +45,6 @@ class TestJoin(unittest.TestCase):
self._mlist = create_list('test@example.com')
self._service = getUtility(ISubscriptionService)
- def tearDown(self):
- reset_the_world()
-
def test_join_user_with_bogus_id(self):
# When `subscriber` is a missing user id, an exception is raised.
with self.assertRaises(MissingUserError) as cm:
diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py
index 6b15c9838..b3150148c 100644
--- a/src/mailman/bin/mailman.py
+++ b/src/mailman/bin/mailman.py
@@ -30,10 +30,10 @@ import argparse
from zope.interface.verify import verifyObject
-from mailman.app.finder import find_components
from mailman.core.i18n import _
from mailman.core.initialize import initialize
from mailman.interfaces.command import ICLISubCommand
+from mailman.utilities.modules import find_components
from mailman.version import MAILMAN_VERSION_FULL
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 37322418a..ae4ba735e 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -577,12 +577,16 @@ configuration: python:mailman.config.mail_archive
class: mailman.archiving.prototype.Prototype
-[style.master]
-# The style's priority, with 0 being the lowest priority.
-priority: 0
+[styles]
+# Python import paths inside which components are searched for which implement
+# the IStyle interface. Use one path per line.
+paths:
+ mailman.styles
-# The class implementing the IStyle interface, which applies the style.
-class: mailman.styles.default.DefaultStyle
+# The default style to apply if nothing else was requested. The value is the
+# name of an existing style. If no such style exists, no style will be
+# applied.
+default: default
[digests]
diff --git a/src/mailman/core/chains.py b/src/mailman/core/chains.py
index 61807a6c0..a02cfa972 100644
--- a/src/mailman/core/chains.py
+++ b/src/mailman/core/chains.py
@@ -28,10 +28,10 @@ __all__ = [
from zope.interface.verify import verifyObject
-from mailman.app.finder import find_components
from mailman.chains.base import Chain, TerminalChainBase
from mailman.config import config
from mailman.interfaces.chain import LinkAction, IChain
+from mailman.utilities.modules import find_components
diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py
index 972417c2c..fe007b9e2 100644
--- a/src/mailman/core/pipelines.py
+++ b/src/mailman/core/pipelines.py
@@ -36,12 +36,13 @@ from zope.interface import implementer
from zope.interface.verify import verifyObject
from mailman.app.bounces import bounce_message
-from mailman.app.finder import find_components
from mailman.config import config
from mailman.core import errors
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.pipeline import IPipeline
+from mailman.utilities.modules import find_components
+
dlog = logging.getLogger('mailman.debug')
vlog = logging.getLogger('mailman.vette')
diff --git a/src/mailman/core/rules.py b/src/mailman/core/rules.py
index 6aac0a64b..8a7ce2b4a 100644
--- a/src/mailman/core/rules.py
+++ b/src/mailman/core/rules.py
@@ -27,9 +27,9 @@ __all__ = [
from zope.interface.verify import verifyObject
-from mailman.app.finder import find_components
from mailman.config import config
from mailman.interfaces.rules import IRule
+from mailman.utilities.modules import find_components
diff --git a/src/mailman/database/schema/mm_20121015000000.py b/src/mailman/database/schema/mm_20121015000000.py
index 51e0602e7..bfbf5cb62 100644
--- a/src/mailman/database/schema/mm_20121015000000.py
+++ b/src/mailman/database/schema/mm_20121015000000.py
@@ -18,6 +18,9 @@
"""3.0b2 -> 3.0b3 schema migrations.
* bans.mailing_list -> bans.list_id
+
+Added:
+* mailinglist.style
"""
from __future__ import absolute_import, print_function, unicode_literals
@@ -84,5 +87,6 @@ def upgrade_postgres(database, store, version, module_path):
WHERE id = {1};
""".format(_make_listid(mailing_list), id))
store.execute('ALTER TABLE ban DROP COLUMN mailing_list;')
+ store.execute('ALTER TABLE mailinglist ADD COLUMN style_name;')
# Record the migration in the version table.
database.load_schema(store, version, None, module_path)
diff --git a/src/mailman/database/schema/sqlite_20121015000000_01.sql b/src/mailman/database/schema/sqlite_20121015000000_01.sql
index c0df75111..e9e3661e0 100644
--- a/src/mailman/database/schema/sqlite_20121015000000_01.sql
+++ b/src/mailman/database/schema/sqlite_20121015000000_01.sql
@@ -20,3 +20,4 @@ INSERT INTO ban_backup SELECT
FROM ban;
ALTER TABLE ban_backup ADD COLUMN list_id TEXT;
+ALTER TABLE mailinglist ADD COLUMN style_name TEXT;
diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py
index 4401b030f..e813d4341 100644
--- a/src/mailman/database/tests/test_migrations.py
+++ b/src/mailman/database/tests/test_migrations.py
@@ -24,6 +24,8 @@ __all__ = [
'TestMigration20120407MigratedData',
'TestMigration20120407Schema',
'TestMigration20120407UnchangedData',
+ 'TestMigration20121015MigratedData',
+ 'TestMigration20121015Schema',
]
@@ -151,8 +153,10 @@ class TestMigration20120407UnchangedData(MigrationTestBase):
'mailman.database.tests.data',
'migration_{0}_1.sql'.format(self._database.TAG))
self._database.load_sql(self._database.store, sample_data)
- # Update to the current migration we're testing.
- self._database.load_migrations('20120407000000')
+ # XXX 2012-12-28: We have to load the last migration defined in the
+ # system, otherwise the ORM model will not match the SQL table
+ # definitions and we'll get OperationalErrors from SQLite.
+ self._database.load_migrations('20121015000000')
def test_migration_domains(self):
# Test that the domains table, which isn't touched, doesn't change.
@@ -216,8 +220,10 @@ class TestMigration20120407MigratedData(MigrationTestBase):
self._database.load_sql(self._database.store, sample_data)
def _upgrade(self):
- # Update to the current migration we're testing.
- self._database.load_migrations('20120407000000')
+ # XXX 2012-12-28: We have to load the last migration defined in the
+ # system, otherwise the ORM model will not match the SQL table
+ # definitions and we'll get OperationalErrors from SQLite.
+ self._database.load_migrations('20121015000000')
def test_migration_archive_policy_never_0(self):
# Test that the new archive_policy value is updated correctly. In the
@@ -369,6 +375,10 @@ class TestMigration20121015Schema(MigrationTestBase):
['20121014999999'],
('list_id',),
('mailing_list',))
+ self._missing_present('mailinglist',
+ ['20121014999999'],
+ ('style_name',),
+ ())
def test_post_upgrade_column_migrations(self):
self._missing_present('ban',
@@ -376,6 +386,11 @@ class TestMigration20121015Schema(MigrationTestBase):
'20121015000000'],
('mailing_list',),
('list_id',))
+ self._missing_present('mailinglist',
+ ['20121014999999',
+ '20121015000000'],
+ (),
+ ('style_name',))
class TestMigration20121015MigratedData(MigrationTestBase):
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 8a4436a21..798680782 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -132,6 +132,9 @@ class IMailingList(Interface):
"""Advertise this mailing list when people ask for an overview of the
available mailing lists.""")
+ style_name = Attribute(
+ """The name of the last style applied, or None.""")
+
# Contact addresses
posting_address = Attribute(
diff --git a/src/mailman/interfaces/styles.py b/src/mailman/interfaces/styles.py
index 8dfd52145..4a7b69dcb 100644
--- a/src/mailman/interfaces/styles.py
+++ b/src/mailman/interfaces/styles.py
@@ -43,9 +43,6 @@ class IStyle(Interface):
name = Attribute(
"""The name of this style. Must be unique.""")
- priority = Attribute(
- """The priority of this style, as an integer.""")
-
def apply(mailing_list):
"""Apply the style to the mailing list.
@@ -53,50 +50,28 @@ class IStyle(Interface):
:param mailing_list: the mailing list to apply the style to.
"""
- def match(mailing_list, styles):
- """Give this style a chance to match the mailing list.
-
- If the style's internal matching rules match the `mailing_list`, then
- the style may append itself to the `styles` list. This list will be
- ordered when returned from `IStyleManager.lookup()`.
-
- :type mailing_list: `IMailingList`.
- :param mailing_list: the mailing list object.
- :param styles: ordered list of `IStyles` matched so far.
- """
-
-
class IStyleManager(Interface):
- """A manager of styles and style chains."""
+ """A manager of styles."""
def get(name):
"""Return the named style or None.
- :type name: Unicode
+ :type name: string
:param name: A style name.
:return: the named `IStyle` or None if the style doesn't exist.
"""
- def lookup(mailing_list):
- """Return a list of styles for the given mailing list.
-
- Use various registered rules to find an `IStyle` for the given mailing
- list. The returned styles are ordered by their priority.
+ styles = Attribute(
+ 'An iterator over all the styles known by this manager.')
- Style matches can be registered and reordered by plugins.
+ def populate():
+ """Populate the styles from the configuration files.
- :type mailing_list: `IMailingList`.
- :param mailing_list: The mailing list object to find a style for.
- :return: ordered list of `IStyles`. Zero is the lowest priority.
+ This clears the current set of styles and resets them from those
+ defined in the configuration files.
"""
- styles = Attribute(
- """An iterator over all the styles known by this manager.
-
- Styles are ordered by their priority, which may be changed.
- """)
-
def register(style):
"""Register a style with this manager.
@@ -111,10 +86,3 @@ class IStyleManager(Interface):
:param style: an IStyle.
:raises KeyError: If the style's name is not currently registered.
"""
-
- def populate():
- """Populate the styles from the configuration files.
-
- This clears the current set of styles and resets them from those
- defined in the configuration files.
- """
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 324e1a1c8..a78745936 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -87,6 +87,7 @@ class MailingList(Model):
include_rfc2369_headers = Bool()
advertised = Bool()
anonymous_list = Bool()
+ style_name = Unicode()
# Attributes not directly modifiable via the web u/i
created_at = DateTime()
admin_member_chunksize = Int()
diff --git a/src/mailman/runners/tests/test_bounce.py b/src/mailman/runners/tests/test_bounce.py
index 5c21000bf..54bbdba4f 100644
--- a/src/mailman/runners/tests/test_bounce.py
+++ b/src/mailman/runners/tests/test_bounce.py
@@ -243,15 +243,11 @@ class TestStyle:
"""See `IStyle`."""
name = 'test'
- priority = 10
def apply(self, mailing_list):
"""See `IStyle`."""
mailing_list.preferred_language = 'en'
- def match(self, mailing_list, styles):
- styles.append(self)
-
class TestBounceRunnerBug876774(unittest.TestCase):
@@ -271,7 +267,7 @@ class TestBounceRunnerBug876774(unittest.TestCase):
self._style_manager = getUtility(IStyleManager)
self._style_manager.register(self._style)
# Now we can create the mailing list.
- self._mlist = create_list('test@example.com')
+ self._mlist = create_list('test@example.com', style_name='test')
self._bounceq = config.switchboards['bounces']
self._processor = getUtility(IBounceProcessor)
self._runner = make_testable_runner(BounceRunner, 'bounces')
diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py
index cb4da396d..138d49872 100644
--- a/src/mailman/styles/default.py
+++ b/src/mailman/styles/default.py
@@ -47,7 +47,6 @@ class DefaultStyle:
"""The default (i.e. legacy) style."""
name = 'default'
- priority = 0 # the lowest priority style
def apply(self, mailing_list):
"""See `IStyle`."""
@@ -55,6 +54,7 @@ class DefaultStyle:
mlist = mailing_list
# List identity.
mlist.display_name = mlist.list_name.capitalize()
+ mlist.style_name = self.name
mlist.include_rfc2369_headers = True
mlist.allow_list_posts = True
# Most of these were ripped from the old MailList.InitVars() method.
@@ -217,9 +217,3 @@ from: .*@uplinkpro.com
mlist.owner_chain = 'default-owner-chain'
# The default pipeline to send -owner email through.
mlist.owner_pipeline = 'default-owner-pipeline'
-
- def match(self, mailing_list, styles):
- """See `IStyle`."""
- # If no other styles have matched, then the default style matches.
- if len(styles) == 0:
- styles.append(self)
diff --git a/src/mailman/styles/docs/styles.rst b/src/mailman/styles/docs/styles.rst
index 8f589f10b..ccacbdb88 100644
--- a/src/mailman/styles/docs/styles.rst
+++ b/src/mailman/styles/docs/styles.rst
@@ -3,58 +3,37 @@ 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.
+new mailing lists. Every style has a name, which must be unique.
-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.
+Styles are generally only applied when a mailing list is created, although
+there is no reason why styles can't be applied to an existing mailing list.
+However, when a style changes, the mailing lists using that style are not
+automatically updated. Instead, think of styles as the initial set of
+defaults for just about any mailing list attribute. In fact, application of a
+style to a mailing list can really modify the mailing list in any way.
-Let's start with a vanilla mailing list and a default style manager.
-::
+To start with, there is only one style, the default style.
- >>> 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]
+ >>> from mailman.interfaces.styles import IStyleManager
+ >>> manager = getUtility(IStyleManager)
+ >>> for style in manager.styles:
+ ... print style.name
default
+When you create a mailing list through the low-level `IListManager` API, no
+style is applied.
-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
+ >>> from mailman.interfaces.listmanager import IListManager
+ >>> mlist = getUtility(IListManager).create('ant@example.com')
+ >>> print mlist.style_name
+ None
-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.
+By applying a style, the style name gets assigned.
- >>> matched_styles = [style.name for style in style_manager.lookup(mlist)]
- >>> len(matched_styles)
- 1
- >>> print matched_styles[0]
- default
+ >>> manager.get('default').apply(mlist)
+ >>> print mlist.list_id, mlist.style_name
+ ant.example.com default
Registering styles
@@ -66,82 +45,88 @@ New styles must implement the ``IStyle`` interface.
>>> from mailman.interfaces.styles import IStyle
>>> @implementer(IStyle)
... class TestStyle:
- ... name = 'test'
- ... priority = 10
+ ... name = 'a-test-style'
... def apply(self, mailing_list):
... # Just does something very simple.
+ ... mailing_list.style_name = self.name
... 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())
+ >>> 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.
+All registered styles are returned in alphabetical order by style name.
- >>> 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
+ >>> for style in manager.styles:
+ ... print style.name
+ a-test-style
+ default
+You can also ask the style manager for the style, by name.
-Style priority
-==============
+ >>> test_style = manager.get('a-test-style')
+ >>> print test_style.name
+ a-test-style
-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'
+Unregistering styles
+====================
- >>> 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 unregister a style, making it unavailable in the future.
+
+ >>> manager.unregister(test_style)
+ >>> for style in manager.styles:
+ ... print style.name
+ default
+
+Asking for a missing style returns None.
+
+ >>> print manager.get('a-test-style')
+ None
+
+
+.. _list-creation-styles:
+
+Apply styles at list creation
+=============================
+
+You can specify a style to apply when creating a list through the high-level
+API. Let's start by registering the test style.
+
+ >>> manager.register(test_style)
+
+Now, when we use the high level API, we can ask for the style to be applied.
+
+ >>> from mailman.app.lifecycle import create_list
+ >>> mlist = create_list('bee@example.com', style_name=test_style.name)
+ >>> print mlist.list_id, mlist.style_name
+ bee.example.com a-test-style
-You can change the priority of a style, and if you reapply the styles, they
-will take effect in the new priority order.
+The style has been applied.
- >>> 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
+If no style name is provided when creating the list, the system default style
+(which may or may not be the style named 'default') is applied.
-Unregistering styles
-====================
+ >>> @implementer(IStyle)
+ ... class AnotherStyle:
+ ... name = 'another-style'
+ ... def apply(self, mailing_list):
+ ... # Just does something very simple.
+ ... mailing_list.style_name = self.name
+ ... mailing_list.style_thing = 'thing 2'
+ >>> another_style = AnotherStyle()
-You can unregister a style, making it unavailable in the future.
+We'll set up the system default to apply this newly registered style if no
+other style is explicitly given.
- >>> 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
+ >>> from mailman.testing.helpers import configuration
+ >>> with configuration('styles', default=another_style.name):
+ ... manager.register(another_style)
+ ... mlist = create_list('cat@example.com')
+ >>> print mlist.style_name
+ another-style
+ >>> print mlist.style_thing
+ thing 2
diff --git a/src/mailman/styles/manager.py b/src/mailman/styles/manager.py
index c2729abfb..5962d308f 100644
--- a/src/mailman/styles/manager.py
+++ b/src/mailman/styles/manager.py
@@ -26,7 +26,6 @@ __all__ = [
]
-from operator import attrgetter
from zope.component import getUtility
from zope.interface import implementer
from zope.interface.verify import verifyObject
@@ -34,7 +33,7 @@ from zope.interface.verify import verifyObject
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.styles import (
DuplicateStyleError, IStyle, IStyleManager)
-from mailman.utilities.modules import call_name
+from mailman.utilities.modules import find_components
@@ -50,35 +49,27 @@ class StyleManager:
self._styles.clear()
# Avoid circular imports.
from mailman.config import config
- # Install all the styles described by the configuration files.
- for section in config.style_configs:
- class_path = section['class']
- style = call_name(class_path)
- assert section.name.startswith('style'), (
- 'Bad style section name: %s' % section.name)
- style.name = section.name[6:]
- style.priority = int(section.priority)
- self.register(style)
+ # Calculate the Python import paths to search.
+ paths = filter(None, (path.strip()
+ for path in config.styles.paths.splitlines()))
+ for path in paths:
+ for style_class in find_components(path, IStyle):
+ style = style_class()
+ verifyObject(IStyle, style)
+ assert style.name not in self._styles, (
+ 'Duplicate style "{0}" found in {1}'.format(
+ style.name, style_class))
+ self._styles[style.name] = style
def get(self, name):
"""See `IStyleManager`."""
return self._styles.get(name)
- def lookup(self, mailing_list):
- """See `IStyleManager`."""
- matched_styles = []
- for style in self.styles:
- style.match(mailing_list, matched_styles)
- for style in matched_styles:
- yield style
-
@property
def styles(self):
"""See `IStyleManager`."""
- for style in sorted(self._styles.values(),
- key=attrgetter('priority'),
- reverse=True):
- yield style
+ for style_name in sorted(self._styles):
+ yield self._styles[style_name]
def register(self, style):
"""See `IStyleManager`."""
diff --git a/src/mailman/utilities/modules.py b/src/mailman/utilities/modules.py
index d8b788054..4ba549f99 100644
--- a/src/mailman/utilities/modules.py
+++ b/src/mailman/utilities/modules.py
@@ -22,12 +22,17 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'call_name',
+ 'find_components',
'find_name',
+ 'scan_module',
]
+import os
import sys
+from pkg_resources import resource_listdir
+
def find_name(dotted_name):
@@ -43,6 +48,7 @@ def find_name(dotted_name):
return getattr(sys.modules[package_path], object_name)
+
def call_name(dotted_name, *args, **kws):
"""Imports and calls the named object in package space.
@@ -57,3 +63,50 @@ def call_name(dotted_name, *args, **kws):
"""
named_callable = find_name(dotted_name)
return named_callable(*args, **kws)
+
+
+
+def scan_module(module, interface):
+ """Return all the items in a module that conform to an interface.
+
+ :param module: A module object. The module's `__all__` will be scanned.
+ :type module: module
+ :param interface: The interface that returned objects must conform to.
+ :type interface: `Interface`
+ :return: The sequence of matching components.
+ :rtype: objects implementing `interface`
+ """
+ missing = object()
+ for name in module.__all__:
+ component = getattr(module, name, missing)
+ assert component is not missing, (
+ '%s has bad __all__: %s' % (module, name))
+ if interface.implementedBy(component):
+ yield component
+
+
+
+def find_components(package, interface):
+ """Find components which conform to a given interface.
+
+ Search all the modules in a given package, returning an iterator over all
+ objects found that conform to the given interface.
+
+ :param package: The package path to search.
+ :type package: string
+ :param interface: The interface that returned objects must conform to.
+ :type interface: `Interface`
+ :return: The sequence of matching components.
+ :rtype: objects implementing `interface`
+ """
+ for filename in resource_listdir(package, ''):
+ basename, extension = os.path.splitext(filename)
+ if extension != '.py':
+ continue
+ module_name = '{0}.{1}'.format(package, basename)
+ __import__(module_name, fromlist='*')
+ module = sys.modules[module_name]
+ if not hasattr(module, '__all__'):
+ continue
+ for component in scan_module(module, interface):
+ yield component