Commit ae3d0cc3 authored by Barry Warsaw's avatar Barry Warsaw

Several important cleanups.

* Turn on absolute_import and unicode_literals everywhere, and deal with the
  aftermath.
* Use 'except X as Y' everywhere.
* Make the module prologues much more consistent.
* Use '{}'.format() consistently, except for logger interface.
* Because of the problems with calling ** args with unicode keywords, hide
  calls to Template.substitute() behind an API.
parent a3f7d07c
......@@ -36,13 +36,14 @@ import email.Iterators
from email.Errors import HeaderParseError
from lazr.config import as_boolean
from string import ascii_letters, digits, whitespace, Template
from string import ascii_letters, digits, whitespace
import mailman.templates
from mailman import passwords
from mailman.config import config
from mailman.core import errors
from mailman.utilities.string import expand
AT = '@'
......@@ -418,7 +419,7 @@ def UnobscureEmail(addr):
class OuterExit(Exception):
pass
def findtext(templatefile, dict=None, raw=False, lang=None, mlist=None):
def findtext(templatefile, raw_dict=None, raw=False, lang=None, mlist=None):
# Make some text from a template file. The order of searches depends on
# whether mlist and lang are provided. Once the templatefile is found,
# string substitution is performed by interpolation in `dict'. If `raw'
......@@ -524,12 +525,8 @@ def findtext(templatefile, dict=None, raw=False, lang=None, mlist=None):
fp.close()
template = unicode(template, GetCharSet(lang), 'replace')
text = template
if dict is not None:
try:
text = Template(template).safe_substitute(**dict)
except (TypeError, ValueError):
# The template is really screwed up
log.exception('broken template: %s', filename)
if raw_dict is not None:
text = expand(template, raw_dict)
if raw:
return text, filename
return wrap(text), filename
......
......@@ -17,6 +17,9 @@
"""Application level bounce handling."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'bounce_message',
]
......
......@@ -17,6 +17,8 @@
"""Initialize the email commands."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'initialize',
......@@ -37,6 +39,6 @@ def initialize():
if not IEmailCommand.implementedBy(command_class):
continue
assert command_class.name not in config.commands, (
'Duplicate email command "%s" found in %s' %
(command_class.name, module))
'Duplicate email command "{0}" found in {1}'.format(
command_class.name, module))
config.commands[command_class.name] = command_class()
......@@ -17,6 +17,8 @@
"""Application level list creation."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'create_list',
......@@ -83,7 +85,7 @@ def remove_list(fqdn_listname, mailing_list=None, archives=True):
# Do the MTA-specific list deletion tasks
module_name, class_name = config.mta.incoming.rsplit('.', 1)
__import__(module_name)
getattr(sys.modules[module_name], class_name)().create(mlist)
getattr(sys.modules[module_name], class_name)().create(mailing_list)
# Remove the list directory.
removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname))
# Remove any stale locks associated with the list.
......
......@@ -17,6 +17,8 @@
"""Application support for membership management."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'add_member',
......@@ -99,7 +101,7 @@ def add_member(mlist, address, realname, password, delivery_mode, language):
break
else:
raise AssertionError(
'User should have had linked address: %s', address)
'User should have had linked address: {0}'.format(address))
# Create the member and set the appropriate preferences.
member = address_obj.subscribe(mlist, MemberRole.member)
member.preferences.preferred_language = language
......
......@@ -17,6 +17,8 @@
"""Application support for moderators."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'handle_message',
......@@ -144,7 +146,7 @@ def handle_message(mlist, id, action,
# processing.
config.switchboards['in'].enqueue(msg, _metadata=msgdata)
else:
raise AssertionError('Unexpected action: %s' % action)
raise AssertionError('Unexpected action: {0}'.format(action))
# Forward the message.
if forward:
# Get a copy of the original message from the message store.
......@@ -257,7 +259,7 @@ def handle_subscription(mlist, id, action, comment=None):
delivery_mode, formataddr((realname, address)),
'via admin approval')
else:
raise AssertionError('Unexpected action: %s' % action)
raise AssertionError('Unexpected action: {0}'.format(action))
# Delete the request from the database.
requestdb.delete_request(id)
......@@ -313,7 +315,7 @@ def handle_unsubscription(mlist, id, action, comment=None):
pass
slog.info('%s: deleted %s', mlist.fqdn_listname, address)
else:
raise AssertionError('Unexpected action: %s' % action)
raise AssertionError('Unexpected action: {0}'.format(action))
# Delete the request from the database.
requestdb.delete_request(id)
......
......@@ -17,6 +17,8 @@
"""Sending notifications."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'send_admin_subscription_notice',
......
......@@ -17,6 +17,8 @@
"""Implementation of the IUserRegistrar interface."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'Registrar',
......
......@@ -21,6 +21,9 @@
# mailing list. The reply governor should really apply site-wide per
# recipient (I think).
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'autorespond_to_sender',
'can_acknowledge',
......
......@@ -17,6 +17,8 @@
"""The Mail-Archive.com archiver."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'MailArchive',
......
......@@ -17,6 +17,8 @@
"""MHonArc archiver."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'MHonArc',
......@@ -28,12 +30,12 @@ import logging
import subprocess
from base64 import b32encode
from string import Template
from urlparse import urljoin
from zope.interface import implements
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
from mailman.utilities.string import expand
log = logging.getLogger('mailman.archiver')
......@@ -52,11 +54,11 @@ class MHonArc:
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
web_host = config.domains[mlist.host_name].url_host
return Template(config.archiver.mhonarc.base_url).safe_substitute(
listname=mlist.fqdn_listname,
hostname=web_host,
fqdn_listname=mlist.fqdn_listname,
)
return expand(config.archiver.mhonarc.base_url,
dict(listname=mlist.fqdn_listname,
hostname=web_host,
fqdn_listname=mlist.fqdn_listname,
))
@staticmethod
def permalink(mlist, msg):
......@@ -83,8 +85,7 @@ class MHonArc:
"""See `IArchiver`."""
substitutions = config.__dict__.copy()
substitutions['listname'] = mlist.fqdn_listname
command = Template(config.archiver.mhonarc.command).safe_substitute(
substitutions)
command = expand(config.archiver.mhonarc.command, substitutions)
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True)
......
......@@ -17,6 +17,8 @@
"""Pipermail archiver."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Pipermail',
......@@ -26,7 +28,6 @@ __all__ = [
import os
from cStringIO import StringIO
from string import Template
from zope.interface import implements
from zope.interface.interface import adapter_hooks
......@@ -34,6 +35,7 @@ from mailman.Utils import makedirs
from mailman.config import config
from mailman.interfaces.archiver import IArchiver, IPipermailMailingList
from mailman.interfaces.mailinglist import IMailingList
from mailman.utilities.string import expand
from mailman.Archiver.HyperArch import HyperArchive
......@@ -94,12 +96,11 @@ class Pipermail:
url = mlist.script_url('private') + '/index.html'
else:
web_host = config.domains[mlist.host_name].url_host
url = Template(config.archiver.pipermail.base_url).safe_substitute(
listname=mlist.fqdn_listname,
hostname=web_host,
fqdn_listname=mlist.fqdn_listname,
)
return url
return expand(config.archiver.pipermail.base_url,
dict(listname=mlist.fqdn_listname,
hostname=web_host,
fqdn_listname=mlist.fqdn_listname,
))
@staticmethod
def permalink(mlist, message):
......
......@@ -17,6 +17,8 @@
"""Prototypical permalinking archiver."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Prototype',
......
......@@ -17,8 +17,12 @@
"""The terminal 'accept' chain."""
__all__ = ['AcceptChain']
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AcceptChain',
]
import logging
......
......@@ -17,6 +17,8 @@
"""Base class for terminal chains."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Chain',
......@@ -78,7 +80,8 @@ class Chain:
implements(IMutableChain)
def __init__(self, name, description):
assert name not in config.chains, 'Duplicate chain name: %s' % name
assert name not in config.chains, (
'Duplicate chain name: {0}'.format(name))
self.name = name
self.description = description
self._links = []
......
......@@ -17,6 +17,8 @@
"""The default built-in starting chain."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'BuiltInChain',
......@@ -78,9 +80,7 @@ class BuiltInChain:
# Get the named rule.
rule = config.rules[rule_name]
# Get the chain, if one is defined.
if chain_name is None:
chain = None
else:
chain = config.chains[chain_name]
chain = (None if chain_name is None
else config.chains[chain_name])
links.append(Link(rule, action, chain))
return iter(self._cached_links)
......@@ -17,8 +17,12 @@
"""The terminal 'discard' chain."""
__all__ = ['DiscardChain']
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'DiscardChain',
]
import logging
......
......@@ -17,6 +17,8 @@
"""The header-matching chain."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'HeaderMatchChain',
......@@ -56,7 +58,7 @@ def make_link(entry):
# We don't assert that the chain exists here because the jump
# chain may not yet have been created.
else:
raise AssertionError('Bad link description: %s' % entry)
raise AssertionError('Bad link description: {0}'.format(entry))
rule = HeaderMatchRule(header, pattern)
chain = config.chains[chain_name]
return Link(rule, LinkAction.jump, chain)
......@@ -73,9 +75,9 @@ class HeaderMatchRule:
def __init__(self, header, pattern):
self._header = header
self._pattern = pattern
self.name = 'header-match-%002d' % HeaderMatchRule._count
self.name = 'header-match-{0:02}'.format(HeaderMatchRule._count)
HeaderMatchRule._count += 1
self.description = u'%s: %s' % (header, pattern)
self.description = '{0}: {1}'.format(header, pattern)
# XXX I think we should do better here, somehow recording that a
# particular header matched a particular pattern, but that gets ugly
# with RFC 2822 headers. It also doesn't match well with the rule
......
......@@ -17,6 +17,8 @@
"""The terminal 'hold' chain."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'HoldChain',
......@@ -91,14 +93,14 @@ class HoldChain(TerminalChainBase):
original_subject = _('(no subject)')
else:
original_subject = oneline(original_subject, charset)
substitutions = {
'listname' : mlist.fqdn_listname,
'subject' : original_subject,
'sender' : sender,
'reason' : 'XXX', #reason,
'confirmurl' : '%s/%s' % (mlist.script_url('confirm'), token),
'admindb_url': mlist.script_url('admindb'),
}
substitutions = dict(
listname = mlist.fqdn_listname,
subject = original_subject,
sender = sender,
reason = 'XXX', #reason,
confirmurl = '{0}/{1}'.format(mlist.script_url('confirm'), token),
admindb_url = mlist.script_url('admindb'),
)
# At this point the message is held, but now we have to craft at least
# two responses. The first will go to the original author of the
# message and it will contain the token allowing them to approve or
......@@ -167,7 +169,7 @@ also appear in the first line of the body of the reply.""")),
nmsg.attach(text)
nmsg.attach(MIMEMessage(msg))
nmsg.attach(MIMEMessage(dmsg))
nmsg.send(mlist, **{'tomoderators': 1})
nmsg.send(mlist, **dict(tomoderators=True))
# Log the held message
# XXX reason
reason = 'n/a'
......
......@@ -17,8 +17,12 @@
"""The terminal 'reject' chain."""
__all__ = ['RejectChain']
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'RejectChain',
]
import logging
......
......@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
"""Mailman configuration package."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'config',
......
......@@ -17,6 +17,8 @@
"""Configuration file loading and management."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Configuration',
......
......@@ -17,6 +17,9 @@
"""Various constants and enumerations."""
from __future__ import unicode_literals
__metaclass__ = type
__all__ = [
'SystemDefaultPreferences',
]
......@@ -29,7 +32,7 @@ from mailman.interfaces.preferences import IPreferences
class SystemDefaultPreferences(object):
class SystemDefaultPreferences:
implements(IPreferences)
acknowledge_posts = False
......
......@@ -17,6 +17,8 @@
"""Application support for chain processing."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'initialize',
......@@ -56,7 +58,7 @@ def process(mlist, msg, msgdata, start_chain='built-in'):
# we can capture a chain's link iterator in mid-flight. This supports
# the 'detour' link action
try:
link = chain_iter.next()
link = next(chain_iter)
except StopIteration:
# This chain is exhausted. Pop the last chain on the stack and
# continue iterating through it. If there's nothing left on the
......@@ -90,7 +92,8 @@ def process(mlist, msg, msgdata, start_chain='built-in'):
elif link.action is LinkAction.run:
link.function(mlist, msg, msgdata)
else:
raise AssertionError('Bad link action: %s' % link.action)
raise AssertionError(
'Bad link action: {0}'.format(link.action))
else:
# The rule did not match; keep going.
if link.rule.record:
......@@ -103,7 +106,7 @@ def initialize():
for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain):
chain = chain_class()
assert chain.name not in config.chains, (
'Duplicate chain name: %s' % chain.name)
'Duplicate chain name: {0}'.format(chain.name))
config.chains[chain.name] = chain
# Set up a couple of other default chains.
chain = BuiltInChain()
......
......@@ -17,6 +17,34 @@
"""Mailman errors."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AlreadyReceivingDigests',
'AlreadyReceivingRegularDeliveries',
'BadDomainSpecificationError',
'BadPasswordSchemeError',
'CantDigestError',
'DiscardMessage',
'EmailAddressError',
'HandlerError',
'HoldMessage',
'HostileSubscriptionError',
'InvalidEmailAddress',
'LostHeldMessage',
'MailmanError',
'MailmanException',
'MemberError',
'MembershipIsBanned',
'MustDigestError',
'NotAMemberError',
'PasswordError',
'RejectMessage',
'SomeRecipientsFailed',
'SubscriptionError',
]
# Base class for all exceptions raised in Mailman (XXX except legacy string
......
......@@ -24,6 +24,17 @@ line argument parsing, since some of the initialization behavior is controlled
by the command line arguments.
"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'initialize',
'initialize_1',
'initialize_2',
'initialize_3',
]
import os
from zope.interface.interface import adapter_hooks
......
......@@ -17,7 +17,7 @@
"""Logging initialization, using Python's standard logging package."""
from __future__ import absolute_import
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
......@@ -74,11 +74,10 @@ class ReopenableFileHandler(logging.Handler):
stream = (self._stream if self._stream else sys.stderr)
try:
msg = self.format(record)
fs = '%s\n'
try:
stream.write(fs % msg)
stream.write('{0}'.format(msg))
except UnicodeError:
stream.write(fs % msg.encode('string-escape'))
stream.write('{0}'.format(msg.encode('string-escape')))
self.flush()
except:
self.handleError(record)
......
......@@ -17,6 +17,8 @@
"""Pipeline processor."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'initialize',
......@@ -114,8 +116,8 @@ def initialize():
handler = handler_class()
verifyObject(IHandler, handler)
assert handler.name not in config.handlers, (
'Duplicate handler "%s" found in %s' %
(handler.name, handler_finder))
'Duplicate handler "{0}" found in {1}'.format(
handler.name, handler_finder))
config.handlers[handler.name] = handler
# Set up some pipelines.
for pipeline_class in (BuiltInPipeline, VirginPipeline):
......
......@@ -17,6 +17,13 @@
"""Get a requested plugin."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
]
import pkg_resources
......@@ -36,7 +43,8 @@ def get_plugin(group):
"""
entry_points = list(pkg_resources.iter_entry_points(group))
if len(entry_points) == 0:
raise RuntimeError('No entry points found for group: %s' % group)
raise RuntimeError(
'No entry points found for group: {0}'.format(group))
elif len(entry_points) == 1:
# Okay, this is the one to use.
return entry_points[0].load()
......@@ -44,14 +52,15 @@ def get_plugin(group):
# Find the one /not/ named 'stock'.
entry_points = [ep for ep in entry_points if ep.name <> 'stock']
if len(entry_points) == 0:
raise RuntimeError('No stock plugin found for group: %s' % group)
raise RuntimeError(
'No stock plugin found for group: {0}'.format(group))
elif len(entry_points) == 2:
raise RuntimeError('Too many stock plugins defined')
else:
raise AssertionError('Insanity')
return entry_points[0].load()
else:
raise RuntimeError('Too many plugins for group: %s' % group)
raise RuntimeError('Too many plugins for group: {0}'.format(group))
......
......@@ -17,6 +17,8 @@
"""Various rule helpers"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'initialize',
......@@ -39,5 +41,6 @@ def initialize():
rule = rule_class()
verifyObject(IRule, rule)
assert rule.name not in config.rules, (
'Duplicate rule "%s" found in %s' % (rule.name, rule_finder))
'Duplicate rule "{0}" found in {1}'.format(
rule.name, rule_finder))
config.rules[rule.name] = rule
......@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.