diff options
| -rw-r--r-- | setup.py | 3 | ||||
| -rw-r--r-- | src/mailman/bin/__init__.py | 61 | ||||
| -rw-r--r-- | src/mailman/bin/list_lists.py | 104 | ||||
| -rw-r--r-- | src/mailman/bin/mailman.py | 79 | ||||
| -rw-r--r-- | src/mailman/bin/master.py | 5 | ||||
| -rw-r--r-- | src/mailman/bin/qrunner.py | 10 | ||||
| -rw-r--r-- | src/mailman/bin/version.py | 48 | ||||
| -rw-r--r-- | src/mailman/commands/docs/lists.txt | 127 | ||||
| -rw-r--r-- | src/mailman/commands/lists.py | 102 | ||||
| -rw-r--r-- | src/mailman/i18n.py | 107 | ||||
| -rw-r--r-- | src/mailman/interfaces/command.py | 19 |
11 files changed, 404 insertions, 261 deletions
@@ -23,7 +23,6 @@ from string import Template sys.path.insert(0, 'src') -import mailman.bin from mailman.version import VERSION as __version__ from setuptools import setup, find_packages @@ -62,7 +61,7 @@ for dirpath, dirnames, filenames in os.walk(start_dir): template = Template('$script = mailman.bin.$script:main') scripts = set( template.substitute(script=script) - for script in mailman.bin.__all__ + for script in ('mailman', 'mailmanctl', 'qrunner', 'master') ) diff --git a/src/mailman/bin/__init__.py b/src/mailman/bin/__init__.py index d61693c5e..e69de29bb 100644 --- a/src/mailman/bin/__init__.py +++ b/src/mailman/bin/__init__.py @@ -1,61 +0,0 @@ -# Copyright (C) 2007-2009 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/>. - -__all__ = [ - 'add_members', - 'arch', - 'bounces', - 'bumpdigests', - 'check_perms', - 'checkdbs', - 'cleanarch', - 'config_list', - 'confirm', - 'create_list', - 'disabled', - 'dumpdb', - 'export', - 'find_member', - 'gate_news', - 'genaliases', - 'import', - 'inject', - 'join', - 'leave', - 'list_lists', - 'list_members', - 'list_owners', - 'mailmanctl', - 'make_instance', - 'master', - 'mmsitepass', - 'nightly_gzip', - 'owner', - 'post', - 'qrunner', - 'remove_list', - 'request', - 'senddigests', - 'set_members', - 'show_config', - 'show_qfiles', - 'testall', - 'unshunt', - 'update', - 'version', - 'withlist', - ] diff --git a/src/mailman/bin/list_lists.py b/src/mailman/bin/list_lists.py deleted file mode 100644 index ea1640910..000000000 --- a/src/mailman/bin/list_lists.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (C) 1998-2009 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/>. - -from mailman.config import config -from mailman.i18n import _ -from mailman.options import Options - - - -class ScriptOptions(Options): - usage = _("""\ -%prog [options] - -List all mailing lists.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-a', '--advertised', - default=False, action='store_true', - help=_("""\ -List only those mailing lists that are publicly advertised""")) - self.parser.add_option( - '-b', '--bare', - default=False, action='store_true', - help=_("""\ -Displays only the list name, with no description.""")) - self.parser.add_option( - '-d', '--domain', - default=[], type='string', action='append', - dest='domains', help=_("""\ -List only those mailing lists that match the given virtual domain, which may -be either the email host or the url host name. Multiple -d options may be -given.""")) - self.parser.add_option( - '-f', '--full', - default=False, action='store_true', - help=_("""\ -Print the full list name, including the posting address.""")) - - def sanity_check(self): - if len(self.arguments) > 0: - self.parser.error(_('Unexpected arguments')) - - - -def main(): - options = ScriptOptions() - options.initialize() - - mlists = [] - longest = 0 - - listmgr = config.db.list_manager - for fqdn_name in sorted(listmgr.names): - mlist = listmgr.get(fqdn_name) - if options.options.advertised and not mlist.advertised: - continue - if options.options.domains: - for domain in options.options.domains: - if domain in mlist.web_page_url or domain == mlist.host_name: - mlists.append(mlist) - break - else: - mlists.append(mlist) - if options.options.full: - name = mlist.fqdn_listname - else: - name = mlist.real_name - longest = max(len(name), longest) - - if not mlists and not options.options.bare: - print _('No matching mailing lists found') - return - - if not options.options.bare: - num_mlists = len(mlists) - print _('$num_mlists matching mailing lists found:') - - format = '%%%ds - %%.%ds' % (longest, 77 - longest) - for mlist in mlists: - if options.options.full: - name = mlist.fqdn_listname - else: - name = mlist.real_name - if options.options.bare: - print name - else: - description = mlist.description or _('[no description available]') - print ' ', format % (name, description) diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py new file mode 100644 index 000000000..42b145e67 --- /dev/null +++ b/src/mailman/bin/mailman.py @@ -0,0 +1,79 @@ +# Copyright (C) 2009 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/>. + +"""The 'mailman' command dispatcher.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'main', + ] + + +import os +import argparse + +from zope.interface.verify import verifyObject + +from mailman.app.finder import find_components +from mailman.core.initialize import initialize +from mailman.i18n import _ +from mailman.interfaces.command import ICLISubCommand +from mailman.version import MAILMAN_VERSION_FULL + + + +def main(): + # Create the basic parser and add all globally common options. + parser = argparse.ArgumentParser( + description=_("""\ + The GNU Mailman mailing list management system + Copyright 1998-2009 by the Free Software Foundation, Inc. + http://www.list.org + """), + formatter_class=argparse.RawDescriptionHelpFormatter, + version=MAILMAN_VERSION_FULL) + parser.add_argument( + '-C', '--config', + help=_("""\ + Configuration file to use. If not given, the environment variable + MAILMAN_CONFIG_FILE is consulted and used if set. If neither are + given, a default configuration file is loaded.""")) + # Look at all modules in the mailman.bin package and if they are prepared + # to add a subcommand, let them do so. I'm still undecided as to whether + # this should be pluggable or not. If so, then we'll probably have to + # partially parse the arguments now, then initialize the system, then find + # the plugins. Punt on this for now. + subparser = parser.add_subparsers(title='Commands') + for command_class in find_components('mailman.commands', ICLISubCommand): + command = command_class() + verifyObject(ICLISubCommand, command) + command.add(subparser) + args = parser.parse_args() + if len(args.__dict__) == 0: + # No arguments or subcommands were given. + parser.print_help() + parser.exit() + # Before actually performing the subcommand, we need to initialize the + # Mailman system, and in particular, we must read the configuration file. + config_file = os.getenv('MAILMAN_CONFIG_FILE') + if config_file is None: + config_file = args.config + initialize(config_file) + # Perform the subcommand option. + args.func(args) diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py index 58e0776bd..829468883 100644 --- a/src/mailman/bin/master.py +++ b/src/mailman/bin/master.py @@ -17,10 +17,11 @@ """Master sub-process watcher.""" +from __future__ import absolute_import, unicode_literals + __metaclass__ = type __all__ = [ - 'Loop', - 'get_lock_data', + 'main', ] diff --git a/src/mailman/bin/qrunner.py b/src/mailman/bin/qrunner.py index bcda52ca6..73e701f16 100644 --- a/src/mailman/bin/qrunner.py +++ b/src/mailman/bin/qrunner.py @@ -15,6 +15,16 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. +"""The queue runner.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'main', + ] + + import sys import signal import logging diff --git a/src/mailman/bin/version.py b/src/mailman/bin/version.py deleted file mode 100644 index 31fc33ec2..000000000 --- a/src/mailman/bin/version.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 1998-2009 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/>. - -"""Print the Mailman version.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'main', - ] - - -# pylint: disable-msg=W0611 -from mailman.core.system import system -from mailman.i18n import _ -from mailman.options import Options - - - -class ScriptOptions(Options): - """See `Options`.""" - usage = _("""\ -%prog - -Print the Mailman version and exit.""") - - - -def main(): - """Main entry point.""" - options = ScriptOptions() - options.initialize() - print _('Using $system.mailman_version') diff --git a/src/mailman/commands/docs/lists.txt b/src/mailman/commands/docs/lists.txt new file mode 100644 index 000000000..85fe7edc2 --- /dev/null +++ b/src/mailman/commands/docs/lists.txt @@ -0,0 +1,127 @@ +========================= +Command line list display +========================= + +A system administrator can display all the mailing lists via the command +line. When there are no mailing lists, a helpful message is displayed. + + >>> class FakeArgs: + ... advertised = False + ... bare = False + ... domains = None + ... full = False + + >>> from mailman.commands.lists import Lists + >>> command = Lists() + >>> command.process(FakeArgs) + No matching mailing lists found + +When there are a few mailing lists, they are shown in alphabetical order by +their fully qualified list names, with a description. + + >>> from mailman.config import config + >>> from mailman.interfaces.domain import IDomainManager + >>> domain_mgr = IDomainManager(config) + >>> domain_mgr.add('example.net') + <Domain example.net...> + + >>> from mailman.app.lifecycle import create_list + >>> mlist_1 = create_list('list-one@example.com') + >>> mlist_1.description = 'List One' + + >>> mlist_2 = create_list('list-two@example.com') + >>> mlist_2.description = 'List Two' + + >>> mlist_3 = create_list('list-one@example.net') + >>> mlist_3.description = 'List One in Example.Net' + >>> transaction.commit() + + >>> command.process(FakeArgs) + 3 matching mailing lists found: + List-one - List One + List-one - List One in Example.Net + List-two - List Two + + +Full names +========== + +You can display the mailing lists' full names, i.e. their posting addresses, +with the --full switch. + + >>> FakeArgs.full = True + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com - List One + list-one@example.net - List One in Example.Net + list-two@example.com - List Two + + +Bare names +========== + +You can print less verbose output, such that only the mailing list's name is +shown, with the --bare option. + + >>> FakeArgs.bare = True + >>> FakeArgs.full = False + >>> command.process(FakeArgs) + List-one + List-one + List-two + +--full and --bare can be combined. + + >>> FakeArgs.full = True + >>> command.process(FakeArgs) + list-one@example.com + list-one@example.net + list-two@example.com + + +Specific domain +=============== + +You can narrow the search down to a specific domain with the --domain option. +A helpful message is displayed if no matching domains are given. + + >>> FakeArgs.bare = False + >>> FakeArgs.domains = ['example.org'] + >>> command.process(FakeArgs) + No matching mailing lists found + +But if a matching domain is given, only mailing lists in that domain are +shown. + + >>> FakeArgs.domains = ['example.net'] + >>> command.process(FakeArgs) + 1 matching mailing lists found: + list-one@example.net - List One in Example.Net + +More than one --domain argument can be given; then all mailing lists in +matching domains are shown. + + >>> FakeArgs.domains = ['example.com', 'example.net'] + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com - List One + list-one@example.net - List One in Example.Net + list-two@example.com - List Two + + +Advertised lists +================ + +Mailing lists can be 'advertised' meaning their existence is public +knowledge. Non-advertised lists are considered private. Display through the +command line can select on this attribute. + + >>> FakeArgs.domains = [] + >>> FakeArgs.advertised = True + >>> mlist_1.advertised = False + >>> transaction.commit() + + >>> command.process(FakeArgs) + 2 matching mailing lists found: + list-one@example.net - List One in Example.Net + list-two@example.com - List Two diff --git a/src/mailman/commands/lists.py b/src/mailman/commands/lists.py new file mode 100644 index 000000000..6757099d9 --- /dev/null +++ b/src/mailman/commands/lists.py @@ -0,0 +1,102 @@ +# Copyright (C) 2009 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/>. + +"""The 'lists' subcommand.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'Lists', + ] + + +from zope.interface import implements + +from mailman.config import config +from mailman.i18n import _ +from mailman.interfaces.command import ICLISubCommand + + + +class Lists: + """The `lists` subcommand.""" + + implements(ICLISubCommand) + + def add(self, subparser): + """See `ICLISubCommand`.""" + lists_parser = subparser.add_parser( + 'lists', help=_('List all mailing lists')) + lists_parser.add_argument( + '-a', '--advertised', + default=False, action='store_true', + help=_( + 'List only those mailing lists that are publicly advertised')) + lists_parser.add_argument( + '-b', '--bare', + default=False, action='store_true', + help=_('Show only the list name, with no description')) + lists_parser.add_argument( + '-d', '--domain', + action='append', help=_("""\ + List only those mailing lists hosted on the given domain, which + must be the email host name. Multiple -d options may be given. + """)) + lists_parser.add_argument( + '-f', '--full', + default=False, action='store_true', + help=_( + 'Show the full mailing list name (i.e. the posting address')) + lists_parser.set_defaults(func=self.process) + + def process(self, args): + """See `ICLISubCommand`.""" + mailing_lists = [] + list_manager = config.db.list_manager + # Gather the matching mailing lists. + for fqdn_name in sorted(list_manager.names): + mlist = list_manager.get(fqdn_name) + if args.advertised and not mlist.advertised: + continue + if args.domains and mlist.host_name not in args.domains: + continue + mailing_lists.append(mlist) + # Maybe no mailing lists matched. + if len(mailing_lists) == 0: + if not args.bare: + print _('No matching mailing lists found') + return + if not args.bare: + count = len(mailing_lists) + print _('$count matching mailing lists found:') + # Calculate the longest mailing list name. + longest = len( + max(mlist.fqdn_listname for mlist in mailing_lists) + if args.full else + max(mlist.real_name for mlist in mailing_lists)) + # Print it out. + for mlist in mailing_lists: + name = (mlist.fqdn_listname if args.full else mlist.real_name) + if args.bare: + print name + else: + description = (mlist.description + if mlist.description is not None + else _('[no description available]')) + print '{0:{2}} - {1:{3}}'.format( + name, description, longest, 77 - longest) diff --git a/src/mailman/i18n.py b/src/mailman/i18n.py index 51330ef2e..7d63a2b86 100644 --- a/src/mailman/i18n.py +++ b/src/mailman/i18n.py @@ -17,10 +17,11 @@ """Internationalization support.""" -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'Translator', '_', 'get_translation', 'set_language', @@ -35,6 +36,8 @@ import time import string import gettext +from textwrap import dedent + import mailman.messages from mailman.utilities.string import expand @@ -127,48 +130,68 @@ if _translation is None: -def _(s): - """Translate the string. +class Translator: + def __init__(self, dedent=True): + """Create a translation context. - :param s: The string to transate - :type s: string - :return: The translated string - :rtype: string - """ - if s == '': - return '' - assert s, 'Cannot translate: {0}'.format(s) - # Do translation of the given string into the current language, and do PEP - # 292 style $-string interpolation into the resulting string. - # - # This lets you write something like: - # - # now = time.ctime(time.time()) - # print _('The current time is: $now') - # - # and have it Just Work. Note that the lookup order for keys in the - # original string is 1) locals dictionary, 2) globals dictionary. - # - # Get the frame of the caller. - # pylint: disable-msg=W0212 - frame = sys._getframe(1) - # A `safe' dictionary is used so we won't get an exception if there's a - # missing key in the dictionary. - raw_dict = frame.f_globals.copy() - raw_dict.update(frame.f_locals) - # Mailman must be unicode safe internally (i.e. all strings inside Mailman - # must be unicodes). The translation service is one boundary to the - # outside world, so to honor this constraint, make sure that all strings - # to come out of _() are unicodes, even if the translated string or - # dictionary values are 8-bit strings. - tns = _translation.ugettext(s) - charset = _translation.charset() or 'us-ascii' - # Python requires ** dictionaries to have str, not unicode keys. For our - # purposes, keys should always be ascii. Values though should be unicode. - translated_string = expand(tns, attrdict(raw_dict), Template) - if isinstance(translated_string, str): - translated_string = unicode(translated_string, charset) - return translated_string + :param dedent: Whether the input string should be dedented. + :type dedent: bool + """ + self.dedent = dedent + + def _(self, original): + """Translate the string. + + :param original: The original string to translate. + :type original: string + :return: The translated string. + :rtype: string + """ + if original == '': + return '' + assert original, 'Cannot translate: {0}'.format(s) + # Because the original string is what the text extractors put into the + # catalog, we must first look up the original unadulterated string in + # the catalog. Use the global translation context for this. + # + # Mailman must be unicode safe internally (i.e. all strings inside + # Mailman are unicodes). The translation service is one boundary to + # the outside world, so to honor this constraint, make sure that all + # strings to come out of _() are unicodes, even if the translated + # string or dictionary values are 8-bit strings. + tns = _translation.ugettext(original) + charset = _translation.charset() or 'us-ascii' + # Do PEP 292 style $-string interpolation into the resulting string. + # + # This lets you write something like: + # + # now = time.ctime(time.time()) + # print _('The current time is: $now') + # + # and have it Just Work. Note that the lookup order for keys in the + # original string is 1) locals dictionary, 2) globals dictionary. + # + # Get the frame of the caller. + # pylint: disable-msg=W0212 + frame = sys._getframe(1) + # A 'safe' dictionary is used so we won't get an exception if there's + # a missing key in the dictionary. + raw_dict = frame.f_globals.copy() + raw_dict.update(frame.f_locals) + # Python requires ** dictionaries to have str, not unicode keys. For + # our purposes, keys should always be ascii. Values though should be + # unicode. + translated_string = expand(tns, attrdict(raw_dict), Template) + if isinstance(translated_string, str): + translated_string = unicode(translated_string, charset) + # Dedent the string if so desired. + if self.dedent: + translated_string = dedent(translated_string) + return translated_string + + +# Global defaults. +_ = Translator()._ diff --git a/src/mailman/interfaces/command.py b/src/mailman/interfaces/command.py index 9995b80a9..14e7f9dd6 100644 --- a/src/mailman/interfaces/command.py +++ b/src/mailman/interfaces/command.py @@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ 'ContinueProcessing', + 'ICLISubCommand', 'IEmailCommand', 'IEmailResults', ] @@ -69,5 +70,19 @@ class IEmailCommand(Interface): -class IBinCommand(Interface): - """A command line (i.e. bin) command.""" +class ICLISubCommand(Interface): + """A command line interface subcommand.""" + + def add(subparser): + """Add the subcommand to the subparser. + + :param subparser: The argument subparser. + :type subparser: `argparse.ArgumentParser` + """ + + def process(args): + """Process the subcommand. + + :param args: The namespace, as passed in by argparse. + :type args: `argparse.Namespace` + """ |
