diff options
| author | Barry Warsaw | 2009-08-09 10:49:35 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2009-08-09 10:49:35 -0400 |
| commit | 9cb2d844baf1ec47bed25fd48e5762ab641b5498 (patch) | |
| tree | 071f4c18deec29c8c270bef8d8d3fd94b6cdaec8 | |
| parent | c4a053b184f16156cafd984b01c055d70414c4b6 (diff) | |
| download | mailman-9cb2d844baf1ec47bed25fd48e5762ab641b5498.tar.gz mailman-9cb2d844baf1ec47bed25fd48e5762ab641b5498.tar.zst mailman-9cb2d844baf1ec47bed25fd48e5762ab641b5498.zip | |
A start on the 'mailman' subcommand layout, with the help of argparse. Right
now the only subcommand is 'lists' which displays all mailing lists like the
old bin/list_lists command did (which is now removed).
Remove bin/version since 'bin/mailman --version' does this for us.
Simplify the calculation of the bin scripts; there will be many fewer of them.
Extend i18n to use a class based structure. By default, all i18n strings are
dedented after translation and substitution, which improves command line
help. The class structure allows for overriding this behavior.
Diffstat (limited to '')
| -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` + """ |
