From eba20e601520adef966da23ff149150da362ebb6 Mon Sep 17 00:00:00 2001 From: Greg Ward Date: Sat, 31 Jul 2004 16:15:44 +0000 Subject: Upgrade optparse module and tests to Optik 1.5a1: * add expansion of default values in help text: the string "%default" in an option's help string is expanded to str() of that option's default value, or "none" if no default value. * bug #955889: option default values that happen to be strings are now processed in the same way as values from the command line; this allows generation of nicer help when using custom types. Can be disabled with parser.set_process_default_values(False). * bug #960515: don't crash when generating help for callback options that specify 'type', but not 'dest' or 'metavar'. * feature #815264: change the default help format for short options that take an argument from e.g. "-oARG" to "-o ARG"; add set_short_opt_delimiter() and set_long_opt_delimiter() methods to HelpFormatter to allow (slight) customization of the formatting. * patch #736940: internationalize Optik: all built-in user- targeted literal strings are passed through gettext.gettext(). (If you want translations (.po files), they're not included with Python -- you'll find them in the Optik source distribution from http://optik.sourceforge.net/ .) * bug #878453: respect $COLUMNS environment variable for wrapping help output. * feature #988122: expand "%prog" in the 'description' passed to OptionParser, just like in the 'usage' and 'version' strings. (This is *not* done in the 'description' passed to OptionGroup.) --- Lib/optparse.py | 548 +++++++++++++++++++++++++++++----------------- Lib/test/test_optparse.py | 540 +++++++++++++++++++++++++++++++++------------ 2 files changed, 747 insertions(+), 341 deletions(-) diff --git a/Lib/optparse.py b/Lib/optparse.py index d3593d3..f30fc45 100644 --- a/Lib/optparse.py +++ b/Lib/optparse.py @@ -16,13 +16,11 @@ For support, use the optik-users@lists.sourceforge.net mailing list # Python developers: please do not make changes to this file, since # it is automatically generated from the Optik source code. -__version__ = "1.4.1+" +__version__ = "1.5a1" __all__ = ['Option', 'SUPPRESS_HELP', 'SUPPRESS_USAGE', - 'STD_HELP_OPTION', - 'STD_VERSION_OPTION', 'Values', 'OptionContainer', 'OptionGroup', @@ -37,7 +35,8 @@ __all__ = ['Option', 'BadOptionError'] __copyright__ = """ -Copyright (c) 2001-2003 Gregory P. Ward. All rights reserved. +Copyright (c) 2001-2004 Gregory P. Ward. All rights reserved. +Copyright (c) 2002-2004 Python Software Foundation. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -70,12 +69,23 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sys, os import types import textwrap +from gettext import gettext as _ + +def _repr(self): + return "<%s at 0x%x: %s>" % (self.__class__.__name__, id(self), self) + + +# This file was generated from: +# Id: option_parser.py,v 1.67 2004/07/24 23:21:21 gward Exp +# Id: option.py,v 1.33 2004/07/24 23:21:21 gward Exp +# Id: help.py,v 1.15 2004/07/24 23:21:21 gward Exp +# Id: errors.py,v 1.9 2004/07/24 23:21:21 gward Exp class OptParseError (Exception): - def __init__ (self, msg): + def __init__(self, msg): self.msg = msg - def __str__ (self): + def __str__(self): return self.msg @@ -85,11 +95,11 @@ class OptionError (OptParseError): inconsistent arguments. """ - def __init__ (self, msg, option): + def __init__(self, msg, option): self.msg = msg self.option_id = str(option) - def __str__ (self): + def __str__(self): if self.option_id: return "option %s: %s" % (self.option_id, self.msg) else: @@ -120,6 +130,8 @@ class HelpFormatter: formatting help; by default IndentedHelpFormatter is used. Instance attributes: + parser : OptionParser + the controlling OptionParser instance indent_increment : int the number of columns to indent per nesting level max_help_position : int @@ -128,51 +140,108 @@ class HelpFormatter: the calculated starting column for option help text; initially the same as the maximum width : int - total number of columns for output + total number of columns for output (pass None to constructor for + this value to be taken from the $COLUMNS environment variable) level : int current indentation level current_indent : int current indentation level (in columns) help_width : int number of columns available for option help text (calculated) + default_tag : str + text to replace with each option's default value, "%default" + by default. Set to false value to disable default value expansion. + option_strings : { Option : str } + maps Option instances to the snippet of help text explaining + the syntax of that option, e.g. "-h, --help" or + "-fFILE, --file=FILE" + _short_opt_fmt : str + format string controlling how short options with values are + printed in help text. Must be either "%s%s" ("-fFILE") or + "%s %s" ("-f FILE"), because those are the two syntaxes that + Optik supports. + _long_opt_fmt : str + similar but for long options; must be either "%s %s" ("--file FILE") + or "%s=%s" ("--file=FILE"). """ - def __init__ (self, - indent_increment, - max_help_position, - width, - short_first): + NO_DEFAULT_VALUE = "none" + + def __init__(self, + indent_increment, + max_help_position, + width, + short_first): + self.parser = None self.indent_increment = indent_increment self.help_position = self.max_help_position = max_help_position + if width is None: + try: + width = int(os.environ['COLUMNS']) + except (KeyError, ValueError): + width = 80 + width -= 2 self.width = width self.current_indent = 0 self.level = 0 - self.help_width = width - max_help_position + self.help_width = None # computed later self.short_first = short_first + self.default_tag = "%default" + self.option_strings = {} + self._short_opt_fmt = "%s %s" + self._long_opt_fmt = "%s=%s" + + def set_parser(self, parser): + self.parser = parser + + def set_short_opt_delimiter(self, delim): + if delim not in ("", " "): + raise ValueError( + "invalid metavar delimiter for short options: %r" % delim) + self._short_opt_fmt = "%s" + delim + "%s" - def indent (self): + def set_long_opt_delimiter(self, delim): + if delim not in ("=", " "): + raise ValueError( + "invalid metavar delimiter for long options: %r" % delim) + self._long_opt_fmt = "%s" + delim + "%s" + + def indent(self): self.current_indent += self.indent_increment self.level += 1 - def dedent (self): + def dedent(self): self.current_indent -= self.indent_increment assert self.current_indent >= 0, "Indent decreased below 0." self.level -= 1 - def format_usage (self, usage): + def format_usage(self, usage): raise NotImplementedError, "subclasses must implement" - def format_heading (self, heading): + def format_heading(self, heading): raise NotImplementedError, "subclasses must implement" - def format_description (self, description): + def format_description(self, description): + if not description: + return "" desc_width = self.width - self.current_indent indent = " "*self.current_indent - return textwrap.fill(description, desc_width, + return textwrap.fill(description, + desc_width, initial_indent=indent, - subsequent_indent=indent) + subsequent_indent=indent) + "\n" + + def expand_default(self, option): + if self.parser is None or not self.default_tag: + return option.help + + default_value = self.parser.defaults.get(option.dest) + if default_value is NO_DEFAULT or default_value is None: + default_value = self.NO_DEFAULT_VALUE - def format_option (self, option): + return option.help.replace(self.default_tag, str(default_value)) + + def format_option(self, option): # The help for each option consists of two parts: # * the opt strings and metavars # eg. ("-x", or "-fFILENAME, --file=FILENAME") @@ -188,7 +257,7 @@ class HelpFormatter: # -fFILENAME, --file=FILENAME # read data from FILENAME result = [] - opts = option.option_strings + opts = self.option_strings[option] opt_width = self.help_position - self.current_indent - 2 if len(opts) > opt_width: opts = "%*s%s\n" % (self.current_indent, "", opts) @@ -198,7 +267,8 @@ class HelpFormatter: indent_first = 0 result.append(opts) if option.help: - help_lines = textwrap.wrap(option.help, self.help_width) + help_text = self.expand_default(option) + help_lines = textwrap.wrap(help_text, self.help_width) result.append("%*s%s\n" % (indent_first, "", help_lines[0])) result.extend(["%*s%s\n" % (self.help_position, "", line) for line in help_lines[1:]]) @@ -206,29 +276,32 @@ class HelpFormatter: result.append("\n") return "".join(result) - def store_option_strings (self, parser): + def store_option_strings(self, parser): self.indent() max_len = 0 for opt in parser.option_list: strings = self.format_option_strings(opt) - opt.option_strings = strings + self.option_strings[opt] = strings max_len = max(max_len, len(strings) + self.current_indent) self.indent() for group in parser.option_groups: for opt in group.option_list: strings = self.format_option_strings(opt) - opt.option_strings = strings + self.option_strings[opt] = strings max_len = max(max_len, len(strings) + self.current_indent) self.dedent() self.dedent() self.help_position = min(max_len + 2, self.max_help_position) + self.help_width = self.width - self.help_position - def format_option_strings (self, option): + def format_option_strings(self, option): """Return a comma-separated list of option strings & metavariables.""" if option.takes_value(): metavar = option.metavar or option.dest.upper() - short_opts = [sopt + metavar for sopt in option._short_opts] - long_opts = [lopt + "=" + metavar for lopt in option._long_opts] + short_opts = [self._short_opt_fmt % (sopt, metavar) + for sopt in option._short_opts] + long_opts = [self._long_opt_fmt % (lopt, metavar) + for lopt in option._long_opts] else: short_opts = option._short_opts long_opts = option._long_opts @@ -244,18 +317,18 @@ class IndentedHelpFormatter (HelpFormatter): """Format help with indented section bodies. """ - def __init__ (self, - indent_increment=2, - max_help_position=24, - width=79, - short_first=1): + def __init__(self, + indent_increment=2, + max_help_position=24, + width=None, + short_first=1): HelpFormatter.__init__( self, indent_increment, max_help_position, width, short_first) - def format_usage (self, usage): - return "usage: %s\n" % usage + def format_usage(self, usage): + return _("usage: %s\n") % usage - def format_heading (self, heading): + def format_heading(self, heading): return "%*s%s:\n" % (self.current_indent, "", heading) @@ -263,34 +336,33 @@ class TitledHelpFormatter (HelpFormatter): """Format help with underlined section headers. """ - def __init__ (self, - indent_increment=0, - max_help_position=24, - width=79, - short_first=0): + def __init__(self, + indent_increment=0, + max_help_position=24, + width=None, + short_first=0): HelpFormatter.__init__ ( self, indent_increment, max_help_position, width, short_first) - def format_usage (self, usage): - return "%s %s\n" % (self.format_heading("Usage"), usage) + def format_usage(self, usage): + return "%s %s\n" % (self.format_heading(_("Usage")), usage) - def format_heading (self, heading): + def format_heading(self, heading): return "%s\n%s\n" % (heading, "=-"[self.level] * len(heading)) -_builtin_cvt = { "int" : (int, "integer"), - "long" : (long, "long integer"), - "float" : (float, "floating-point"), - "complex" : (complex, "complex") } +_builtin_cvt = { "int" : (int, _("integer")), + "long" : (long, _("long integer")), + "float" : (float, _("floating-point")), + "complex" : (complex, _("complex")) } -def check_builtin (option, opt, value): +def check_builtin(option, opt, value): (cvt, what) = _builtin_cvt[option.type] try: return cvt(value) except ValueError: raise OptionValueError( - #"%s: invalid %s argument %r" % (opt, what, value)) - "option %s: invalid %s value: %r" % (opt, what, value)) + _("option %s: invalid %s value: %r") % (opt, what, value)) def check_choice(option, opt, value): if value in option.choices: @@ -298,12 +370,12 @@ def check_choice(option, opt, value): else: choices = ", ".join(map(repr, option.choices)) raise OptionValueError( - "option %s: invalid choice: %r (choose from %s)" + _("option %s: invalid choice: %r (choose from %s)") % (opt, value, choices)) # Not supplying a default is different from a default of None, # so we need an explicit "not supplied" value. -NO_DEFAULT = "NO"+"DEFAULT" +NO_DEFAULT = ("NO", "DEFAULT") class Option: @@ -392,7 +464,7 @@ class Option: TYPE_CHECKER = { "int" : check_builtin, "long" : check_builtin, "float" : check_builtin, - "complex" : check_builtin, + "complex": check_builtin, "choice" : check_choice, } @@ -410,7 +482,7 @@ class Option: # -- Constructor/initialization methods ---------------------------- - def __init__ (self, *opts, **attrs): + def __init__(self, *opts, **attrs): # Set _short_opts, _long_opts attrs from 'opts' tuple. # Have to be set now, in case no option strings are supplied. self._short_opts = [] @@ -429,7 +501,7 @@ class Option: for checker in self.CHECK_METHODS: checker(self) - def _check_opt_strings (self, opts): + def _check_opt_strings(self, opts): # Filter out None because early versions of Optik had exactly # one short option and one long option, either of which # could be None. @@ -438,7 +510,7 @@ class Option: raise TypeError("at least one option string must be supplied") return opts - def _set_opt_strings (self, opts): + def _set_opt_strings(self, opts): for opt in opts: if len(opt) < 2: raise OptionError( @@ -459,7 +531,7 @@ class Option: self) self._long_opts.append(opt) - def _set_attrs (self, attrs): + def _set_attrs(self, attrs): for attr in self.ATTRS: if attrs.has_key(attr): setattr(self, attr, attrs[attr]) @@ -477,13 +549,13 @@ class Option: # -- Constructor validation methods -------------------------------- - def _check_action (self): + def _check_action(self): if self.action is None: self.action = "store" elif self.action not in self.ACTIONS: raise OptionError("invalid action: %r" % self.action, self) - def _check_type (self): + def _check_type(self): if self.type is None: # XXX should factor out another class attr here: list of # actions that *require* a type @@ -495,6 +567,12 @@ class Option: # No type given? "string" is the most sensible default. self.type = "string" else: + # Allow type objects as an alternative to their names. + if type(self.type) is type: + self.type = self.type.__name__ + if self.type == "str": + self.type = "string" + if self.type not in self.TYPES: raise OptionError("invalid option type: %r" % self.type, self) if self.action not in self.TYPED_ACTIONS: @@ -514,9 +592,13 @@ class Option: raise OptionError( "must not supply choices for type %r" % self.type, self) - def _check_dest (self): - if self.action in self.STORE_ACTIONS and self.dest is None: - # No destination given, and we need one for this action. + def _check_dest(self): + # No destination given, and we need one for this action. The + # self.type check is for callbacks that take a value. + takes_value = (self.action in self.STORE_ACTIONS or + self.type is not None) + if self.dest is None and takes_value: + # Glean a destination from the first long option string, # or from the first short option string if no long options. if self._long_opts: @@ -525,13 +607,13 @@ class Option: else: self.dest = self._short_opts[0][1] - def _check_const (self): + def _check_const(self): if self.action != "store_const" and self.const is not None: raise OptionError( "'const' must not be supplied for action %r" % self.action, self) - def _check_nargs (self): + def _check_nargs(self): if self.action in self.TYPED_ACTIONS: if self.nargs is None: self.nargs = 1 @@ -540,7 +622,7 @@ class Option: "'nargs' must not be supplied for action %r" % self.action, self) - def _check_callback (self): + def _check_callback(self): if self.action == "callback": if not callable(self.callback): raise OptionError( @@ -579,31 +661,42 @@ class Option: # -- Miscellaneous methods ----------------------------------------- - def __str__ (self): + def __str__(self): return "/".join(self._short_opts + self._long_opts) - def takes_value (self): + __repr__ = _repr + + def takes_value(self): return self.type is not None + def get_opt_string(self): + if self._long_opts: + return self._long_opts[0] + else: + return self._short_opts[0] + # -- Processing methods -------------------------------------------- - def check_value (self, opt, value): + def check_value(self, opt, value): checker = self.TYPE_CHECKER.get(self.type) if checker is None: return value else: return checker(self, opt, value) - def process (self, opt, value, values, parser): - - # First, convert the value(s) to the right type. Howl if any - # value(s) are bogus. + def convert_value(self, opt, value): if value is not None: if self.nargs == 1: - value = self.check_value(opt, value) + return self.check_value(opt, value) else: - value = tuple([self.check_value(opt, v) for v in value]) + return tuple([self.check_value(opt, v) for v in value]) + + def process(self, opt, value, values, parser): + + # First, convert the value(s) to the right type. Howl if any + # value(s) are bogus. + value = self.convert_value(opt, value) # And then take whatever action is expected of us. # This is a separate method to make life easier for @@ -611,7 +704,7 @@ class Option: return self.take_action( self.action, self.dest, opt, value, values, parser) - def take_action (self, action, dest, opt, value, values, parser): + def take_action(self, action, dest, opt, value, values, parser): if action == "store": setattr(values, dest, value) elif action == "store_const": @@ -642,33 +735,44 @@ class Option: # class Option -def get_prog_name (): - return os.path.basename(sys.argv[0]) - - SUPPRESS_HELP = "SUPPRESS"+"HELP" SUPPRESS_USAGE = "SUPPRESS"+"USAGE" -STD_HELP_OPTION = Option("-h", "--help", - action="help", - help="show this help message and exit") -STD_VERSION_OPTION = Option("--version", - action="version", - help="show program's version number and exit") +# For compatibility with Python 2.2 +try: + True, False +except NameError: + (True, False) = (1, 0) +try: + basestring +except NameError: + basestring = (str, unicode) class Values: - def __init__ (self, defaults=None): + def __init__(self, defaults=None): if defaults: for (attr, val) in defaults.items(): setattr(self, attr, val) - def __repr__ (self): - return ("<%s at 0x%x: %r>" - % (self.__class__.__name__, id(self), self.__dict__)) + def __str__(self): + return str(self.__dict__) + + __repr__ = _repr - def _update_careful (self, dict): + def __eq__(self, other): + if isinstance(other, Values): + return self.__dict__ == other.__dict__ + elif isinstance(other, dict): + return self.__dict__ == other + else: + return false + + def __ne__(self, other): + return not (self == other) + + def _update_careful(self, dict): """ Update the option values from an arbitrary dictionary, but only use keys from dict that already have a corresponding attribute @@ -681,7 +785,7 @@ class Values: if dval is not None: setattr(self, attr, dval) - def _update_loose (self, dict): + def _update_loose(self, dict): """ Update the option values from an arbitrary dictionary, using all keys from the dictionary regardless of whether @@ -689,7 +793,7 @@ class Values: """ self.__dict__.update(dict) - def _update (self, dict, mode): + def _update(self, dict, mode): if mode == "careful": self._update_careful(dict) elif mode == "loose": @@ -697,17 +801,17 @@ class Values: else: raise ValueError, "invalid update mode: %r" % mode - def read_module (self, modname, mode="careful"): + def read_module(self, modname, mode="careful"): __import__(modname) mod = sys.modules[modname] self._update(vars(mod), mode) - def read_file (self, filename, mode="careful"): + def read_file(self, filename, mode="careful"): vars = {} execfile(filename, vars) self._update(vars, mode) - def ensure_value (self, attr, value): + def ensure_value(self, attr, value): if not hasattr(self, attr) or getattr(self, attr) is None: setattr(self, attr, value) return getattr(self, attr) @@ -745,7 +849,7 @@ class OptionContainer: """ - def __init__ (self, option_class, conflict_handler, description): + def __init__(self, option_class, conflict_handler, description): # Initialize the option list and related data structures. # This method must be provided by subclasses, and it must # initialize at least the following instance attributes: @@ -756,7 +860,7 @@ class OptionContainer: self.set_conflict_handler(conflict_handler) self.set_description(description) - def _create_option_mappings (self): + def _create_option_mappings(self): # For use by OptionParser constructor -- create the master # option mappings used by this OptionParser and all # OptionGroups that it owns. @@ -765,25 +869,28 @@ class OptionContainer: self.defaults = {} # maps option dest -> default value - def _share_option_mappings (self, parser): + def _share_option_mappings(self, parser): # For use by OptionGroup constructor -- use shared option # mappings from the OptionParser that owns this OptionGroup. self._short_opt = parser._short_opt self._long_opt = parser._long_opt self.defaults = parser.defaults - def set_conflict_handler (self, handler): + def set_conflict_handler(self, handler): if handler not in ("ignore", "error", "resolve"): raise ValueError, "invalid conflict_resolution value %r" % handler self.conflict_handler = handler - def set_description (self, description): + def set_description(self, description): self.description = description + def get_description(self): + return self.description + # -- Option-adding methods ----------------------------------------- - def _check_conflict (self, option): + def _check_conflict(self, option): conflict_opts = [] for opt in option._short_opts: if self._short_opt.has_key(opt): @@ -812,7 +919,7 @@ class OptionContainer: if not (c_option._short_opts or c_option._long_opts): c_option.container.option_list.remove(c_option) - def add_option (self, *args, **kwargs): + def add_option(self, *args, **kwargs): """add_option(Option) add_option(opt_str, ..., kwarg=val, ...) """ @@ -842,21 +949,21 @@ class OptionContainer: return option - def add_options (self, option_list): + def add_options(self, option_list): for option in option_list: self.add_option(option) # -- Option query/removal methods ---------------------------------- - def get_option (self, opt_str): + def get_option(self, opt_str): return (self._short_opt.get(opt_str) or self._long_opt.get(opt_str)) - def has_option (self, opt_str): + def has_option(self, opt_str): return (self._short_opt.has_key(opt_str) or self._long_opt.has_key(opt_str)) - def remove_option (self, opt_str): + def remove_option(self, opt_str): option = self._short_opt.get(opt_str) if option is None: option = self._long_opt.get(opt_str) @@ -872,7 +979,7 @@ class OptionContainer: # -- Help-formatting methods --------------------------------------- - def format_option_help (self, formatter): + def format_option_help(self, formatter): if not self.option_list: return "" result = [] @@ -881,38 +988,36 @@ class OptionContainer: result.append(formatter.format_option(option)) return "".join(result) - def format_description (self, formatter): - if self.description: - return formatter.format_description(self.description) - else: - return "" + def format_description(self, formatter): + return formatter.format_description(self.get_description()) - def format_help (self, formatter): + def format_help(self, formatter): + result = [] if self.description: - desc = self.format_description(formatter) + "\n" - else: - desc = "" - return desc + self.format_option_help(formatter) + result.append(self.format_description(formatter)) + if self.option_list: + result.append(self.format_option_help(formatter)) + return "\n".join(result) class OptionGroup (OptionContainer): - def __init__ (self, parser, title, description=None): + def __init__(self, parser, title, description=None): self.parser = parser OptionContainer.__init__( self, parser.option_class, parser.conflict_handler, description) self.title = title - def _create_option_list (self): + def _create_option_list(self): self.option_list = [] self._share_option_mappings(self.parser) - def set_title (self, title): + def set_title(self, title): self.title = title # -- Help-formatting methods --------------------------------------- - def format_help (self, formatter): + def format_help(self, formatter): result = formatter.format_heading(self.title) formatter.indent() result += OptionContainer.format_help(self, formatter) @@ -937,7 +1042,12 @@ class OptionParser (OptionContainer): the name of the current program (to override os.path.basename(sys.argv[0])). - allow_interspersed_args : boolean = true + option_groups : [OptionGroup] + list of option groups in this parser (option groups are + irrelevant for parsing the command-line, but very useful + for generating help) + + allow_interspersed_args : bool = true if true, positional arguments may be interspersed with options. Assuming -a and -b each take a single argument, the command-line -ablah foo bar -bboo baz @@ -950,6 +1060,14 @@ class OptionParser (OptionContainer): Python's getopt module, Perl's Getopt::Std, and other argument- parsing libraries, but it is generally annoying to users.) + process_default_values : bool = true + if true, option default values are processed similarly to option + values from the command line: that is, they are passed to the + type-checking function for the option's type (as long as the + default value is a string). (This really only matters if you + have defined custom types; see SF bug #955889.) Set it to false + to restore the behaviour of Optik 1.4.1 and earlier. + rargs : [string] the argument list currently being parsed. Only set when parse_args() is active, and continually trimmed down as @@ -972,30 +1090,32 @@ class OptionParser (OptionContainer): standard_option_list = [] - def __init__ (self, - usage=None, - option_list=None, - option_class=Option, - version=None, - conflict_handler="error", - description=None, - formatter=None, - add_help_option=1, - prog=None): + def __init__(self, + usage=None, + option_list=None, + option_class=Option, + version=None, + conflict_handler="error", + description=None, + formatter=None, + add_help_option=True, + prog=None): OptionContainer.__init__( self, option_class, conflict_handler, description) self.set_usage(usage) self.prog = prog self.version = version - self.allow_interspersed_args = 1 + self.allow_interspersed_args = True + self.process_default_values = True if formatter is None: formatter = IndentedHelpFormatter() self.formatter = formatter + self.formatter.set_parser(self) # Populate the option list; initial sources are the # standard_option_list class attribute, the 'option_list' - # argument, and the STD_VERSION_OPTION (if 'version' supplied) - # and STD_HELP_OPTION globals. + # argument, and (if applicable) the _add_version_option() and + # _add_help_option() methods. self._populate_option_list(option_list, add_help=add_help_option) @@ -1004,65 +1124,90 @@ class OptionParser (OptionContainer): # -- Private methods ----------------------------------------------- # (used by our or OptionContainer's constructor) - def _create_option_list (self): + def _create_option_list(self): self.option_list = [] self.option_groups = [] self._create_option_mappings() - def _populate_option_list (self, option_list, add_help=1): + def _add_help_option(self): + self.add_option("-h", "--help", + action="help", + help=_("show this help message and exit")) + + def _add_version_option(self): + self.add_option("--version", + action="version", + help=_("show program's version number and exit")) + + def _populate_option_list(self, option_list, add_help=True): if self.standard_option_list: self.add_options(self.standard_option_list) if option_list: self.add_options(option_list) if self.version: - self.add_option(STD_VERSION_OPTION) + self._add_version_option() if add_help: - self.add_option(STD_HELP_OPTION) + self._add_help_option() - def _init_parsing_state (self): + def _init_parsing_state(self): # These are set in parse_args() for the convenience of callbacks. self.rargs = None self.largs = None self.values = None - def _get_prog_name(self): - if self.prog: - return self.prog - else: - return get_prog_name() # -- Simple modifier methods --------------------------------------- - def set_usage (self, usage): + def set_usage(self, usage): if usage is None: - self.usage = "%prog [options]" + self.usage = _("%prog [options]") elif usage is SUPPRESS_USAGE: self.usage = None - elif usage.lower().startswith("usage: "): - # for backwards compatibility with Optik 1.3 and earlier + # For backwards compatibility with Optik 1.3 and earlier. + elif usage.startswith("usage:" + " "): self.usage = usage[7:] else: self.usage = usage - def enable_interspersed_args (self): - self.allow_interspersed_args = 1 + def enable_interspersed_args(self): + self.allow_interspersed_args = True + + def disable_interspersed_args(self): + self.allow_interspersed_args = False - def disable_interspersed_args (self): - self.allow_interspersed_args = 0 + def set_process_default_values(self, process): + self.process_default_values = process - def set_default (self, dest, value): + def set_default(self, dest, value): self.defaults[dest] = value - def set_defaults (self, **kwargs): + def set_defaults(self, **kwargs): self.defaults.update(kwargs) - def get_default_values (self): - return Values(self.defaults) + def _get_all_options(self): + options = self.option_list[:] + for group in self.option_groups: + options.extend(group.option_list) + return options + + def get_default_values(self): + if not self.process_default_values: + # Old, pre-Optik 1.5 behaviour. + return Values(self.defaults) + + defaults = self.defaults.copy() + for option in self._get_all_options(): + default = defaults.get(option.dest) + if isinstance(default, basestring): + opt_str = option.get_opt_string() + defaults[option.dest] = option.check_value(opt_str, default) + + return Values(defaults) # -- OptionGroup methods ------------------------------------------- - def add_option_group (self, *args, **kwargs): + def add_option_group(self, *args, **kwargs): # XXX lots of overlap with OptionContainer.add_option() if type(args[0]) is types.StringType: group = OptionGroup(self, *args, **kwargs) @@ -1078,7 +1223,7 @@ class OptionParser (OptionContainer): self.option_groups.append(group) return group - def get_option_group (self, opt_str): + def get_option_group(self, opt_str): option = (self._short_opt.get(opt_str) or self._long_opt.get(opt_str)) if option and option.container is not self: @@ -1088,13 +1233,13 @@ class OptionParser (OptionContainer): # -- Option-parsing methods ---------------------------------------- - def _get_args (self, args): + def _get_args(self, args): if args is None: return sys.argv[1:] else: return args[:] # don't modify caller's list - def parse_args (self, args=None, values=None): + def parse_args(self, args=None, values=None): """ parse_args(args : [string] = sys.argv[1:], values : Values = None) @@ -1133,7 +1278,7 @@ class OptionParser (OptionContainer): args = largs + rargs return self.check_values(values, args) - def check_values (self, values, args): + def check_values(self, values, args): """ check_values(values : Values, args : [string]) -> (values : Values, args : [string]) @@ -1146,7 +1291,7 @@ class OptionParser (OptionContainer): """ return (values, args) - def _process_args (self, largs, rargs, values): + def _process_args(self, largs, rargs, values): """_process_args(largs : [string], rargs : [string], values : Values) @@ -1197,7 +1342,7 @@ class OptionParser (OptionContainer): # *empty* -- still a subset of [arg0, ..., arg(i-1)], but # not a very interesting subset! - def _match_long_opt (self, opt): + def _match_long_opt(self, opt): """_match_long_opt(opt : string) -> string Determine which long option string 'opt' matches, ie. which one @@ -1206,7 +1351,7 @@ class OptionParser (OptionContainer): """ return _match_abbrev(opt, self._long_opt) - def _process_long_opt (self, rargs, values): + def _process_long_opt(self, rargs, values): arg = rargs.pop(0) # Value explicitly attached to arg? Pretend it's the next @@ -1214,10 +1359,10 @@ class OptionParser (OptionContainer): if "=" in arg: (opt, next_arg) = arg.split("=", 1) rargs.insert(0, next_arg) - had_explicit_value = 1 + had_explicit_value = True else: opt = arg - had_explicit_value = 0 + had_explicit_value = False opt = self._match_long_opt(opt) option = self._long_opt[opt] @@ -1225,9 +1370,9 @@ class OptionParser (OptionContainer): nargs = option.nargs if len(rargs) < nargs: if nargs == 1: - self.error("%s option requires a value" % opt) + self.error(_("%s option requires an argument") % opt) else: - self.error("%s option requires %d values" + self.error(_("%s option requires %d arguments") % (opt, nargs)) elif nargs == 1: value = rargs.pop(0) @@ -1236,16 +1381,16 @@ class OptionParser (OptionContainer): del rargs[0:nargs] elif had_explicit_value: - self.error("%s option does not take a value" % opt) + self.error(_("%s option does not take a value") % opt) else: value = None option.process(opt, value, values, self) - def _process_short_opts (self, rargs, values): + def _process_short_opts(self, rargs, values): arg = rargs.pop(0) - stop = 0 + stop = False i = 1 for ch in arg[1:]: opt = "-" + ch @@ -1253,20 +1398,20 @@ class OptionParser (OptionContainer): i += 1 # we have consumed a character if not option: - self.error("no such option: %s" % opt) + self.error(_("no such option: %s") % opt) if option.takes_value(): # Any characters left in arg? Pretend they're the # next arg, and stop consuming characters of arg. if i < len(arg): rargs.insert(0, arg[i:]) - stop = 1 + stop = True nargs = option.nargs if len(rargs) < nargs: if nargs == 1: - self.error("%s option requires a value" % opt) + self.error(_("%s option requires an argument") % opt) else: - self.error("%s option requires %s values" + self.error(_("%s option requires %d arguments") % (opt, nargs)) elif nargs == 1: value = rargs.pop(0) @@ -1285,7 +1430,19 @@ class OptionParser (OptionContainer): # -- Feedback methods ---------------------------------------------- - def error (self, msg): + def get_prog_name(self): + if self.prog is None: + return os.path.basename(sys.argv[0]) + else: + return self.prog + + def expand_prog_name(self, s): + return s.replace("%prog", self.get_prog_name()) + + def get_description(self): + return self.expand_prog_name(self.description) + + def error(self, msg): """error(msg : string) Print a usage message incorporating 'msg' to stderr and exit. @@ -1293,16 +1450,17 @@ class OptionParser (OptionContainer): should either exit or raise an exception. """ self.print_usage(sys.stderr) - sys.exit("%s: error: %s" % (self._get_prog_name(), msg)) + sys.stderr.write("%s: error: %s\n" % (self.get_prog_name(), msg)) + sys.exit(2) # command-line usage error - def get_usage (self): + def get_usage(self): if self.usage: return self.formatter.format_usage( - self.usage.replace("%prog", self._get_prog_name())) + self.expand_prog_name(self.usage)) else: return "" - def print_usage (self, file=None): + def print_usage(self, file=None): """print_usage(file : file = stdout) Print the usage message for the current program (self.usage) to @@ -1314,13 +1472,13 @@ class OptionParser (OptionContainer): if self.usage: print >>file, self.get_usage() - def get_version (self): + def get_version(self): if self.version: - return self.version.replace("%prog", self._get_prog_name()) + return self.expand_prog_name(self.version) else: return "" - def print_version (self, file=None): + def print_version(self, file=None): """print_version(file : file = stdout) Print the version message for this program (self.version) to @@ -1331,12 +1489,12 @@ class OptionParser (OptionContainer): if self.version: print >>file, self.get_version() - def format_option_help (self, formatter=None): + def format_option_help(self, formatter=None): if formatter is None: formatter = self.formatter formatter.store_option_strings(self) result = [] - result.append(formatter.format_heading("options")) + result.append(formatter.format_heading(_("options"))) formatter.indent() if self.option_list: result.append(OptionContainer.format_option_help(self, formatter)) @@ -1348,7 +1506,7 @@ class OptionParser (OptionContainer): # Drop the last "\n", or the header if no options or option groups: return "".join(result[:-1]) - def format_help (self, formatter=None): + def format_help(self, formatter=None): if formatter is None: formatter = self.formatter result = [] @@ -1359,7 +1517,7 @@ class OptionParser (OptionContainer): result.append(self.format_option_help(formatter)) return "".join(result) - def print_help (self, file=None): + def print_help(self, file=None): """print_help(file : file = stdout) Print an extended help message, listing all options and any @@ -1372,7 +1530,7 @@ class OptionParser (OptionContainer): # class OptionParser -def _match_abbrev (s, wordmap): +def _match_abbrev(s, wordmap): """_match_abbrev(s : string, wordmap : {string : Option}) -> string Return the string key in 'wordmap' for which 's' is an unambiguous @@ -1390,10 +1548,10 @@ def _match_abbrev (s, wordmap): if len(possibilities) == 1: return possibilities[0] elif not possibilities: - raise BadOptionError("no such option: %s" % s) + raise BadOptionError(_("no such option: %s") % s) else: # More than one possible completion: ambiguous prefix. - raise BadOptionError("ambiguous option: %s (%s?)" + raise BadOptionError(_("ambiguous option: %s (%s?)") % (s, ", ".join(possibilities))) diff --git a/Lib/test/test_optparse.py b/Lib/test/test_optparse.py index 37e229c..b850cec 100644 --- a/Lib/test/test_optparse.py +++ b/Lib/test/test_optparse.py @@ -20,14 +20,7 @@ from test import test_support from optparse import make_option, Option, IndentedHelpFormatter, \ TitledHelpFormatter, OptionParser, OptionContainer, OptionGroup, \ SUPPRESS_HELP, SUPPRESS_USAGE, OptionError, OptionConflictError, \ - BadOptionError, OptionValueError -from optparse import _match_abbrev - -# Do the right thing with boolean values for all known Python versions. -try: - True, False -except NameError: - (True, False) = (1, 0) + BadOptionError, OptionValueError, _match_abbrev class BaseTest(unittest.TestCase): def assertParseOK(self, args, expected_opts, expected_positional_args): @@ -60,50 +53,62 @@ Args were %(args)s.""" % locals ()) return (options, positional_args) - def assertRaises(self, func, expected_exception, expected_output, - get_output=None, - funcargs=[], funckwargs={}): + def assertRaises(self, + func, + args, + kwargs, + expected_exception, + expected_output, + get_output=None, + exact_match=False): """Assert the expected exception is raised when calling a function. Also check whether the right error message is given for a given error. - Keyword arguments: - func -- The function to be called. - expected_exception -- The exception that should be raised. - expected_output -- The output we expect to see. - get_output -- The function to call to get the output. - funcargs -- The arguments `func` should be called with. - funckwargs -- The keyword arguments `func` should be called with. + Arguments: + func -- the function to call + args -- positional arguments to `func` + kwargs -- keyword arguments to `func` + expected_exception -- exception that should be raised + expected_output -- output we expect to see + get_output -- function to call to get the output + exact_match -- whether output must exactly match expected output, + or merely contain it Returns the exception raised for further testing. """ + if args is None: + args = () + if kwargs is None: + kwargs = {} if get_output is None: get_output = self.exception try: - out = func(*funcargs, **funckwargs) + out = func(*args, **kwargs) except expected_exception, err: - output = get_output(err) - - self.failUnless(output.find(expected_output) != -1, - """ -Message was: -%(output)s -Should contain: -%(expected_output)s -Function called: -%(func)s -With args/kwargs: -%(funcargs)s/%(funckwargs)s""" % locals()) + actual_output = get_output(err) + + if exact_match: + match = actual_output == expected_exception + else: + match = actual_output.find(expected_output) != -1 + + self.assert_(match, + """mismatched output +expected output: +'''%(expected_output)s''' +actual output: +'''%(actual_output)s''' +""" % locals()) return err else: - self.fail(""" -No %(expected_exception)s raised. -Function called: -%(func)s -With args/kwargs: -%(funcargs)s/%(funckwargs)s""" % locals ()) + self.fail("""expected exception %(expected_exception)s not raised +called %(func)r +with args %(args)r +and kwargs %(kwargs)r +""" % locals ()) # -- Functions to be used as the get_output argument to assertRaises ------ @@ -113,23 +118,38 @@ With args/kwargs: def redirected_stdout(self, err): return sys.stdout.getvalue() + def redirected_stderr(self, err): + return sys.stderr.getvalue() + # -- Assertions used in more than one class -------------------- def assertParseFail(self, cmdline_args, expected_output): """Assert the parser fails with the expected message.""" - self.assertRaises(self.parser.parse_args, SystemExit, expected_output, - funcargs=[cmdline_args]) + sys.stderr = StringIO() + self.assertRaises(self.parser.parse_args, (cmdline_args,), None, + SystemExit, expected_output, + self.redirected_stderr) + sys.stderr = sys.__stderr__ def assertStdoutEquals(self, cmdline_args, expected_output): """Assert the parser prints the expected output on stdout.""" sys.stdout = StringIO() - self.assertRaises(self.parser.parse_args, SystemExit, expected_output, - self.redirected_stdout, [cmdline_args]) + self.assertRaises(self.parser.parse_args, (cmdline_args,), None, + SystemExit, expected_output, + self.redirected_stdout) sys.stdout = sys.__stdout__ def assertTypeError(self, func, expected_output, *args): """Assert a TypeError is raised when executing func.""" - self.assertRaises(func, TypeError, expected_output, funcargs=args) + self.assertRaises(func, args, None, TypeError, expected_output) + + def assertHelp(self, parser, expected_help): + actual_help = parser.format_help() + if actual_help != expected_help: + raise self.failureException( + 'help text failure; expected:\n"' + + expected_help + '"; got:\n"' + + actual_help + '"\n') # -- Test make_option() aka Option ------------------------------------- @@ -142,8 +162,8 @@ class TestOptionChecks(BaseTest): self.parser = OptionParser(usage=SUPPRESS_USAGE) def assertOptionError(self, expected_output, args=[], kwargs={}): - self.assertRaises(make_option, OptionError, expected_output, - funcargs=args, funckwargs=kwargs) + self.assertRaises(make_option, args, kwargs, + OptionError, expected_output) def test_opt_string_empty(self): self.assertTypeError(make_option, @@ -175,6 +195,8 @@ class TestOptionChecks(BaseTest): def test_type_invalid(self): self.assertOptionError("invalid option type: 'foo'", ["-b"], {'type': 'foo'}) + self.assertOptionError("invalid option type: 'tuple'", + ["-b"], {'type': tuple}) def test_no_type_for_action(self): self.assertOptionError("must not supply a type for action 'count'", @@ -304,8 +326,204 @@ class TestOptionParser(BaseTest): self.assert_removed() def test_remove_nonexistent(self): - self.assertRaises(self.parser.remove_option, ValueError, - "no such option 'foo'", funcargs=['foo']) + self.assertRaises(self.parser.remove_option, ('foo',), None, + ValueError, "no such option 'foo'") + +class TestTypeAliases(BaseTest): + def setUp(self): + self.parser = OptionParser() + + def test_type_aliases(self): + self.parser.add_option("-x", type=int) + self.parser.add_option("-s", type=str) + self.parser.add_option("-t", type="str") + self.assertEquals(self.parser.get_option("-x").type, "int") + self.assertEquals(self.parser.get_option("-s").type, "string") + self.assertEquals(self.parser.get_option("-t").type, "string") + + +# Custom type for testing processing of default values. +_time_units = { 's' : 1, 'm' : 60, 'h' : 60*60, 'd' : 60*60*24 } + +def _check_duration(option, opt, value): + try: + if value[-1].isdigit(): + return int(value) + else: + return int(value[:-1]) * _time_units[value[-1]] + except ValueError, IndexError: + raise OptionValueError( + 'option %s: invalid duration: %r' % (opt, value)) + +class DurationOption(Option): + TYPES = Option.TYPES + ('duration',) + TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER) + TYPE_CHECKER['duration'] = _check_duration + +class TestDefaultValues(BaseTest): + def setUp(self): + self.parser = OptionParser() + self.parser.add_option("-v", "--verbose", default=True) + self.parser.add_option("-q", "--quiet", dest='verbose') + self.parser.add_option("-n", type="int", default=37) + self.parser.add_option("-m", type="int") + self.parser.add_option("-s", default="foo") + self.parser.add_option("-t") + self.parser.add_option("-u", default=None) + self.expected = { 'verbose': True, + 'n': 37, + 'm': None, + 's': "foo", + 't': None, + 'u': None } + + def test_basic_defaults(self): + self.assertEqual(self.parser.get_default_values(), self.expected) + + def test_mixed_defaults_post(self): + self.parser.set_defaults(n=42, m=-100) + self.expected.update({'n': 42, 'm': -100}) + self.assertEqual(self.parser.get_default_values(), self.expected) + + def test_mixed_defaults_pre(self): + self.parser.set_defaults(x="barf", y="blah") + self.parser.add_option("-x", default="frob") + self.parser.add_option("-y") + + self.expected.update({'x': "frob", 'y': "blah"}) + self.assertEqual(self.parser.get_default_values(), self.expected) + + self.parser.remove_option("-y") + self.parser.add_option("-y", default=None) + self.expected.update({'y': None}) + self.assertEqual(self.parser.get_default_values(), self.expected) + + def test_process_default(self): + self.parser.option_class = DurationOption + self.parser.add_option("-d", type="duration", default=300) + self.parser.add_option("-e", type="duration", default="6m") + self.parser.set_defaults(n="42") + self.expected.update({'d': 300, 'e': 360, 'n': 42}) + self.assertEqual(self.parser.get_default_values(), self.expected) + + self.parser.set_process_default_values(False) + self.expected.update({'d': 300, 'e': "6m", 'n': "42"}) + self.assertEqual(self.parser.get_default_values(), self.expected) + + +class TestProgName(BaseTest): + """ + Test that %prog expands to the right thing in usage, version, + and help strings. + """ + + def assertUsage(self, parser, expected_usage): + self.assertEqual(parser.get_usage(), expected_usage) + + def assertVersion(self, parser, expected_version): + self.assertEqual(parser.get_version(), expected_version) + + + def test_default_progname(self): + # Make sure that program name taken from sys.argv[0] by default. + sys.argv[0] = "/foo/bar/baz.py" + parser = OptionParser("usage: %prog ...", version="%prog 1.2") + expected_usage = "usage: baz.py ...\n" + self.assertUsage(parser, expected_usage) + self.assertVersion(parser, "baz.py 1.2") + self.assertHelp(parser, + expected_usage + "\n" + + "options:\n" + " --version show program's version number and exit\n" + " -h, --help show this help message and exit\n") + + def test_custom_progname(self): + parser = OptionParser(prog="thingy", + version="%prog 0.1", + usage="%prog arg arg") + parser.remove_option("-h") + parser.remove_option("--version") + expected_usage = "usage: thingy arg arg\n" + self.assertUsage(parser, expected_usage) + self.assertVersion(parser, "thingy 0.1") + self.assertHelp(parser, expected_usage + "\n") + + +class TestExpandDefaults(BaseTest): + def setUp(self): + self.parser = OptionParser(prog="test") + self.help_prefix = """\ +usage: test [options] + +options: + -h, --help show this help message and exit +""" + self.file_help = "read from FILE [default: %default]" + self.expected_help_file = self.help_prefix + \ + " -f FILE, --file=FILE read from FILE [default: foo.txt]\n" + self.expected_help_none = self.help_prefix + \ + " -f FILE, --file=FILE read from FILE [default: none]\n" + + def test_option_default(self): + self.parser.add_option("-f", "--file", + default="foo.txt", + help=self.file_help) + self.assertHelp(self.parser, self.expected_help_file) + + def test_parser_default_1(self): + self.parser.add_option("-f", "--file", + help=self.file_help) + self.parser.set_default('file', "foo.txt") + self.assertHelp(self.parser, self.expected_help_file) + + def test_parser_default_2(self): + self.parser.add_option("-f", "--file", + help=self.file_help) + self.parser.set_defaults(file="foo.txt") + self.assertHelp(self.parser, self.expected_help_file) + + def test_no_default(self): + self.parser.add_option("-f", "--file", + help=self.file_help) + self.assertHelp(self.parser, self.expected_help_none) + + def test_default_none_1(self): + self.parser.add_option("-f", "--file", + default=None, + help=self.file_help) + self.assertHelp(self.parser, self.expected_help_none) + + def test_default_none_2(self): + self.parser.add_option("-f", "--file", + help=self.file_help) + self.parser.set_defaults(file=None) + self.assertHelp(self.parser, self.expected_help_none) + + def test_float_default(self): + self.parser.add_option( + "-p", "--prob", + help="blow up with probability PROB [default: %default]") + self.parser.set_defaults(prob=0.43) + expected_help = self.help_prefix + \ + " -p PROB, --prob=PROB blow up with probability PROB [default: 0.43]\n" + self.assertHelp(self.parser, expected_help) + + def test_alt_expand(self): + self.parser.add_option("-f", "--file", + default="foo.txt", + help="read from FILE [default: *DEFAULT*]") + self.parser.formatter.default_tag = "*DEFAULT*" + self.assertHelp(self.parser, self.expected_help_file) + + def test_no_expand(self): + self.parser.add_option("-f", "--file", + default="foo.txt", + help="read from %default file") + self.parser.formatter.default_tag = None + expected_help = self.help_prefix + \ + " -f FILE, --file=FILE read from %default file\n" + self.assertHelp(self.parser, expected_help) + # -- Test parser.parse_args() ------------------------------------------ @@ -318,7 +536,7 @@ class TestStandard(BaseTest): self.parser = OptionParser(usage=SUPPRESS_USAGE, option_list=options) def test_required_value(self): - self.assertParseFail(["-a"], "-a option requires a value") + self.assertParseFail(["-a"], "-a option requires an argument") def test_invalid_integer(self): self.assertParseFail(["-b", "5x"], @@ -580,7 +798,7 @@ class TestNArgs(BaseTest): def test_nargs_required_values(self): self.assertParseFail(["--point", "1.0", "3.5"], - "--point option requires 3 values") + "--point option requires 3 arguments") class TestNArgsAppend(BaseTest): def setUp(self): @@ -597,7 +815,7 @@ class TestNArgsAppend(BaseTest): def test_nargs_append_required_values(self): self.assertParseFail(["-f4,3"], - "-f option requires 2 values") + "-f option requires 2 arguments") def test_nargs_append_simple(self): self.assertParseOK(["--foo=3", "4"], @@ -612,22 +830,6 @@ class TestVersion(BaseTest): self.assertStdoutEquals(["--version"], "bar 0.1\n") sys.argv[0] = oldargv - def test_version_with_prog_keyword(self): - oldargv = sys.argv[0] - sys.argv[0] = "./foo/bar" - self.parser = OptionParser(usage=SUPPRESS_USAGE, version="%prog 0.1", - prog="splat") - self.assertStdoutEquals(["--version"], "splat 0.1\n") - sys.argv[0] = oldargv - - def test_version_with_prog_attribute(self): - oldargv = sys.argv[0] - sys.argv[0] = "./foo/bar" - self.parser = OptionParser(usage=SUPPRESS_USAGE, version="%prog 0.1") - self.parser.prog = "splat" - self.assertStdoutEquals(["--version"], "splat 0.1\n") - sys.argv[0] = oldargv - def test_no_version(self): self.parser = OptionParser(usage=SUPPRESS_USAGE) self.assertParseFail(["--version"], @@ -673,8 +875,8 @@ class TestOptionGroup(BaseTest): def test_add_group_wrong_parser(self): group = OptionGroup(self.parser, "Spam") group.parser = OptionParser() - self.assertRaises(self.parser.add_option_group, ValueError, - "invalid OptionGroup (wrong parser)", funcargs=[group]) + self.assertRaises(self.parser.add_option_group, (group,), None, + ValueError, "invalid OptionGroup (wrong parser)") def test_group_manipulate(self): group = self.parser.add_option_group("Group 2", @@ -794,7 +996,22 @@ class TestCallback(BaseTest): {'filename': "foo", 'x': 42}, []) -class TestCallBackExtraArgs(BaseTest): + def test_callback_help(self): + # This test was prompted by SF bug #960515 -- the point is + # not to inspect the help text, just to make sure that + # format_help() doesn't crash. + parser = OptionParser(usage=SUPPRESS_USAGE) + parser.remove_option("-h") + parser.add_option("-t", "--test", action="callback", + callback=lambda: None, type="string", + help="foo") + + expected_help = ("options:\n" + " -t TEST, --test=TEST foo\n") + self.assertHelp(parser, expected_help) + + +class TestCallbackExtraArgs(BaseTest): def setUp(self): options = [make_option("-p", "--point", action="callback", callback=self.process_tuple, @@ -819,7 +1036,7 @@ class TestCallBackExtraArgs(BaseTest): {'points': [(1,2,3), (4,5,6)]}, []) -class TestCallBackMeddleArgs(BaseTest): +class TestCallbackMeddleArgs(BaseTest): def setUp(self): options = [make_option(str(x), action="callback", callback=self.process_n, dest='things') @@ -848,7 +1065,7 @@ class TestCallBackMeddleArgs(BaseTest): {'things': [('foo', '--')]}, [2]) -class TestCallBackManyArgs(BaseTest): +class TestCallbackManyArgs(BaseTest): def setUp(self): options = [make_option("-a", "--apple", action="callback", nargs=2, callback=self.process_many, type="string"), @@ -870,10 +1087,10 @@ class TestCallBackManyArgs(BaseTest): self.assertParseOK(["-a", "foo", "bar", "--apple", "ding", "dong", "-b", "1", "2", "3", "--bob", "-666", "42", "0"], - {}, + {"apple": None, "bob": None}, []) -class TestCallBackCheckAbbrev(BaseTest): +class TestCallbackCheckAbbrev(BaseTest): def setUp(self): self.parser = OptionParser() self.parser.add_option("--foo-bar", action="callback", @@ -885,7 +1102,7 @@ class TestCallBackCheckAbbrev(BaseTest): def test_abbrev_callback_expansion(self): self.assertParseOK(["--foo"], {}, []) -class TestCallBackVarArgs(BaseTest): +class TestCallbackVarArgs(BaseTest): def setUp(self): options = [make_option("-a", type="int", nargs=2, dest="a"), make_option("-b", action="store_true", dest="b"), @@ -950,13 +1167,12 @@ class ConflictBase(BaseTest): class TestConflict(ConflictBase): """Use the default conflict resolution for Optik 1.2: error.""" def assert_conflict_error(self, func): - err = self.assertRaises(func, OptionConflictError, - "option -v/--version: conflicting option " - "string(s): -v", - funcargs=["-v", "--version"], - funckwargs={'action':"callback", - 'callback':self.show_version, - 'help':"show version"}) + err = self.assertRaises( + func, ("-v", "--version"), {'action' : "callback", + 'callback' : self.show_version, + 'help' : "show version"}, + OptionConflictError, + "option -v/--version: conflicting option string(s): -v") self.assertEqual(err.msg, "conflicting option string(s): -v") self.assertEqual(err.option_id, "-v/--version") @@ -969,9 +1185,9 @@ class TestConflict(ConflictBase): self.assert_conflict_error(group.add_option) def test_no_such_conflict_handler(self): - self.assertRaises(self.parser.set_conflict_handler, ValueError, - "invalid conflict_resolution value 'foo'", - funcargs=['foo']) + self.assertRaises( + self.parser.set_conflict_handler, ('foo',), None, + ValueError, "invalid conflict_resolution value 'foo'") class TestConflictIgnore(ConflictBase): @@ -1082,8 +1298,60 @@ options: # -- Other testing. ---------------------------------------------------- +_expected_help_basic = """\ +usage: bar.py [options] + +options: + -a APPLE throw APPLEs at basket + -b NUM, --boo=NUM shout "boo!" NUM times (in order to frighten away all the + evil spirits that cause trouble and mayhem) + --foo=FOO store FOO in the foo list for later fooing + -h, --help show this help message and exit +""" + +_expected_help_long_opts_first = """\ +usage: bar.py [options] + +options: + -a APPLE throw APPLEs at basket + --boo=NUM, -b NUM shout "boo!" NUM times (in order to frighten away all the + evil spirits that cause trouble and mayhem) + --foo=FOO store FOO in the foo list for later fooing + --help, -h show this help message and exit +""" + +_expected_help_title_formatter = """\ +Usage +===== + bar.py [options] + +options +======= +-a APPLE throw APPLEs at basket +--boo=NUM, -b NUM shout "boo!" NUM times (in order to frighten away all the + evil spirits that cause trouble and mayhem) +--foo=FOO store FOO in the foo list for later fooing +--help, -h show this help message and exit +""" + +_expected_help_short_lines = """\ +usage: bar.py [options] + +options: + -a APPLE throw APPLEs at basket + -b NUM, --boo=NUM shout "boo!" NUM times (in order to + frighten away all the evil spirits + that cause trouble and mayhem) + --foo=FOO store FOO in the foo list for later + fooing + -h, --help show this help message and exit +""" + class TestHelp(BaseTest): def setUp(self): + self.parser = self.make_parser(80) + + def make_parser(self, columns): options = [ make_option("-a", type="string", dest='a', metavar="APPLE", help="throw APPLEs at basket"), @@ -1095,9 +1363,8 @@ class TestHelp(BaseTest): make_option("--foo", action="append", type="string", dest='foo', help="store FOO in the foo list for later fooing"), ] - - usage = "%prog [options]" - self.parser = OptionParser(usage=usage, option_list=options) + os.environ['COLUMNS'] = str(columns) + return OptionParser(option_list=options) def assertHelpEquals(self, expected_output): # This trick is used to make optparse believe bar.py is being executed. @@ -1109,62 +1376,30 @@ class TestHelp(BaseTest): sys.argv[0] = oldargv def test_help(self): - self.assertHelpEquals("""\ -usage: bar.py [options] - -options: - -aAPPLE throw APPLEs at basket - -bNUM, --boo=NUM shout "boo!" NUM times (in order to frighten away all - the evil spirits that cause trouble and mayhem) - --foo=FOO store FOO in the foo list for later fooing - -h, --help show this help message and exit -""") + self.assertHelpEquals(_expected_help_basic) def test_help_old_usage(self): self.parser.set_usage("usage: %prog [options]") - self.assertHelpEquals("""\ -usage: bar.py [options] - -options: - -aAPPLE throw APPLEs at basket - -bNUM, --boo=NUM shout "boo!" NUM times (in order to frighten away all - the evil spirits that cause trouble and mayhem) - --foo=FOO store FOO in the foo list for later fooing - -h, --help show this help message and exit -""") + self.assertHelpEquals(_expected_help_basic) def test_help_long_opts_first(self): self.parser.formatter.short_first = 0 - self.assertHelpEquals("""\ -usage: bar.py [options] - -options: - -aAPPLE throw APPLEs at basket - --boo=NUM, -bNUM shout "boo!" NUM times (in order to frighten away all - the evil spirits that cause trouble and mayhem) - --foo=FOO store FOO in the foo list for later fooing - --help, -h show this help message and exit -""") + self.assertHelpEquals(_expected_help_long_opts_first) def test_help_title_formatter(self): self.parser.formatter = TitledHelpFormatter() - self.assertHelpEquals("""\ -Usage -===== - bar.py [options] + self.assertHelpEquals(_expected_help_title_formatter) -options -======= --aAPPLE throw APPLEs at basket ---boo=NUM, -bNUM shout "boo!" NUM times (in order to frighten away all - the evil spirits that cause trouble and mayhem) ---foo=FOO store FOO in the foo list for later fooing ---help, -h show this help message and exit -""") + def test_wrap_columns(self): + # Ensure that wrapping respects $COLUMNS environment variable. + # Need to reconstruct the parser, since that's the only time + # we look at $COLUMNS. + self.parser = self.make_parser(60) + self.assertHelpEquals(_expected_help_short_lines) def test_help_description_groups(self): self.parser.set_description( - "This is the program description. This program has " + "This is the program description for %prog. %prog has " "an option group as well as single options.") group = OptionGroup( @@ -1177,21 +1412,26 @@ options self.assertHelpEquals("""\ usage: bar.py [options] -This is the program description. This program has an option group as well as -single options. +This is the program description for bar.py. bar.py has an option group as +well as single options. + options: - -aAPPLE throw APPLEs at basket - -bNUM, --boo=NUM shout "boo!" NUM times (in order to frighten away all - the evil spirits that cause trouble and mayhem) - --foo=FOO store FOO in the foo list for later fooing - -h, --help show this help message and exit + -a APPLE throw APPLEs at basket + -b NUM, --boo=NUM shout "boo!" NUM times (in order to frighten away all the + evil spirits that cause trouble and mayhem) + --foo=FOO store FOO in the foo list for later fooing + -h, --help show this help message and exit Dangerous Options: - Caution: use of these options is at your own risk. It is believed that - some of them bite. - -g Group option. + Caution: use of these options is at your own risk. It is believed + that some of them bite. + + -g Group option. """) + + + class TestMatchAbbrev(BaseTest): def test_match_abbrev(self): self.assertEqual(_match_abbrev("--f", @@ -1205,15 +1445,23 @@ class TestMatchAbbrev(BaseTest): s = "--f" wordmap = {"--foz": None, "--foo": None, "--fie": None} possibilities = ", ".join(wordmap.keys()) - self.assertRaises(_match_abbrev, BadOptionError, - "ambiguous option: --f (%s?)" % possibilities, - funcargs=[s, wordmap]) + self.assertRaises( + _match_abbrev, (s, wordmap), None, + BadOptionError, "ambiguous option: --f (%s?)" % possibilities) -def test_main(): + +def _testclasses(): mod = sys.modules[__name__] - test_support.run_unittest( - *[getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - ) + return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] + +def suite(): + suite = unittest.TestSuite() + for testclass in _testclasses(): + suite.addTest(unittest.makeSuite(testclass)) + return suite + +def test_main(): + test_support.run_suite(suite()) if __name__ == '__main__': unittest.main() -- cgit v0.12