diff -r c9e9142d82d6 Lib/argparse.py --- a/Lib/argparse.py Mon Apr 11 08:43:52 2011 +0100 +++ b/Lib/argparse.py Mon Apr 11 19:32:42 2011 +0200 @@ -585,6 +585,12 @@ result = '...' elif action.nargs == PARSER: result = '%s ...' % get_metavar(1) + elif type(action.nargs) is tuple: + lo, hi = action.nargs + if hi is None: + result = '%d...' % lo + else: + result = '%d..%d' % action.nargs else: formats = ['%s' for _ in range(action.nargs)] result = ' '.join(formats) % get_metavar(action.nargs) @@ -1452,8 +1458,67 @@ raise ValueError(msg % option_string) dest = dest.replace('-', '_') + # preprocess nargs + nargs = kwargs.pop('nargs', None) + if nargs is not None: + if type(nargs) in [tuple, list]: + # allowed inputs: + # - (lo, hi, ...) + # - (None, hi, ...) + # - (lo, None, ...) + # - (None, None, ...) + # + # ouput: + # - (lo, hi) + # - (lo, None) + # - ?, *, + or int + try: + lo, hi, *tail = nargs + + if lo is not None: + lo = int(lo) + assert lo >= 0 + else: + lo = 0 + + if hi is not None: + hi = int(hi) + assert hi >= 0 + + except (ValueError, TypeError, AssertionError): + msg = _('if nargs= is sequence, then [number, number] ' + 'or [number, None] is required') + raise ValueError(msg) + + if hi is None: + # cases (lo, None) and (0, None) + if lo == 0: + nargs = ZERO_OR_MORE + elif lo == 1: + nargs = ONE_OR_MORE + else: + nargs = (lo, None) + else: + # cases (lo, hi) and (0, hi) + if lo == hi: + nargs = lo + elif lo < hi: + if lo == 0 and hi == 1: + nargs = OPTIONAL + else: + nargs = (lo, hi) + else: + msg = _("nargs= lower bound %d must not be greater " + "then upper bound %d" % (lo, hi)) + raise ValueError(msg) + else: + pass # do not touch other values + # return the updated keyword arguments - return dict(kwargs, dest=dest, option_strings=option_strings) + if nargs is not None: + return dict(kwargs, dest=dest, nargs=nargs, option_strings=option_strings) + else: + return dict(kwargs, dest=dest, option_strings=option_strings) def _pop_action_class(self, kwargs, default=None): action = kwargs.pop('action', default) @@ -2043,10 +2108,18 @@ OPTIONAL: _('expected at most one argument'), ONE_OR_MORE: _('expected at least one argument'), } - default = ngettext('expected %s argument', - 'expected %s arguments', - action.nargs) % action.nargs - msg = nargs_errors.get(action.nargs, default) + if type(action.nargs) is tuple: + lo, hi = action.nargs + if hi is None: + msg = _('expected at least %d argument(s)' % lo) + else: + msg = _('expected from %d to %d argument(s)' % action.nargs) + else: + default = ngettext('expected %s argument', + 'expected %s arguments', + action.nargs) % action.nargs + msg = nargs_errors.get(action.nargs, default) + raise ArgumentError(action, msg) # return the number of arguments matched @@ -2199,6 +2272,14 @@ elif nargs == PARSER: nargs_pattern = '(-*A[-AO]*)' + # allow lo .. hi options + elif type(nargs) is tuple: + lo, hi = nargs + if hi is None: + nargs_pattern = '(-*A{%d,})' % (lo) + else: + nargs_pattern = '(-*A{%d,%d})' % nargs + # all others should be integers else: nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs) diff -r c9e9142d82d6 Lib/test/test_argparse.py --- a/Lib/test/test_argparse.py Mon Apr 11 08:43:52 2011 +0100 +++ b/Lib/test/test_argparse.py Mon Apr 11 19:32:42 2011 +0200 @@ -4675,6 +4675,147 @@ def test_nargs_3_metavar_length3(self): self.do_test_no_exception(nargs=3, metavar=("1", "2", "3")) + +# ============================ +# test when nargs is tuple +# ============================ + +class TestNargsRangeBase(unittest.TestCase): + + def setUp(self): + self.parser = p = argparse.ArgumentParser() + p.add_argument('positional', nargs='*') + p.add_argument('-o', '--optional') + + def add_argument(self, lo, hi, name='--foo'): + return self.parser.add_argument(name, nargs=(lo, hi)) + + +class TestNargsRangeAddArgument(TestNargsRangeBase): + """Test processing nargs when tuple is set""" + + def test_add_argument1(self): + "len(nargs) = 0" + with self.assertRaises(ValueError): + self.parser.add_argument('-a', nargs=() ) + + def test_add_argument2(self): + "len(nargs) = 1" + with self.assertRaises(ValueError): + self.parser.add_argument('-a', nargs=(1,) ) + + def test_add_argument3(self): + "len(nargs) = 2, first argument is not a valid num" + with self.assertRaises(ValueError): + self.add_argument('xxx', None) + + def test_add_argument4(self): + "len(nargs) = 2, second argument is not valid num" + with self.assertRaises(ValueError): + self.add_argument(None, 'xxx') + + def test_add_argument5(self): + "nargs = (lo, hi), where lo < hi" + arg = (3, 5) + res = arg + + opt = self.add_argument(*arg) + self.assertEqual(opt.nargs, res) + + def test_add_argument6(self): + "nargs = (lo, hi), where lo == hi, optimized to single number" + arg = (12, 12) + res = 12 + + opt = self.add_argument(*arg) + self.assertEqual(opt.nargs, res) + + def test_add_argument7(self): + "nargs = (lo, hi), where lo > hi (error)" + + with self.assertRaises(ValueError): + opt = self.add_argument(8, 3) + + def test_add_argument8(self): + "nargs = (None, None) => (0, None) -- optimized to '*'" + opt = self.add_argument(0, None) + self.assertEqual(opt.nargs, argparse.ZERO_OR_MORE) + + def test_add_argument9(self): + "nargs = (1, None) => optimized to '+'" + opt = self.add_argument(1, None) + self.assertEqual(opt.nargs, argparse.ONE_OR_MORE) + + def test_add_argument10(self): + "nargs = (0, 1) => optimized to '?'" + opt = self.add_argument(0, 1) + self.assertEqual(opt.nargs, argparse.OPTIONAL) + + def test_add_argument11(self): + "nargs = (None, number) => (0, number)" + arg = (None, 87) + res = (0, 87) + + opt = self.add_argument(*arg) + self.assertEqual(opt.nargs, res) + + +class TestNargumentRangeArgumentParsing(TestNargsRangeBase): + "Test parsing arguments when nargs sets range of options count" + + def test_parser_args_a1(self): + "nargs=(3, 7) - consume all possible options" + args = "aa bb cc dd ee ff gg hh".split() + + self.add_argument(3, 7) + n = self.parser.parse_args(["--foo"] + args) + + self.assertEqual(n.foo, args[:7]) + self.assertEqual(n.positional, args[7:]) + + def test_parser_args_a2(self): + "nargs=(3, 7) - consume 4 options" + args = "aa bb cc dd -o ee ff gg hh".split() + + self.add_argument(3, 7) + n = self.parser.parse_args(["--foo"] + args) + + self.assertEqual(n.foo, args[:4]) + self.assertEqual(n.positional, args[6:]) + + def test_parser_args_a3(self): + "nargs=(3, 7) - can't consume at least 3 options" + args = "aa bb -o cc dd ee ff gg hh".split() + + self.add_argument(3, 7) + with self.assertRaises(SystemExit): + self.parser.parse_args(["--foo"] + args) + + def test_parser_args_a4(self): + "nargs=(3, 7) - can't consume at least 3 options (another case)" + args = "-o aa bb cc dd ee ff gg hh".split() + + self.add_argument(3, 5) + with self.assertRaises(SystemExit): + self.parser.parse_args(["--foo"] + args) + + def test_parser_args_a5(self): + "nargs=(3, None) - consume as many as possible " + args = "aa bb cc dd ee ff gg hh ii -o jj kk".split() + + self.add_argument(3, None) + n = self.parser.parse_args(["--foo"] + args) + self.assertEqual(n.foo, args[:9]) + + def test_parser_args_a6(self): + "nargs=(3, None) - consume as many as possible (error)" + args = "aa bb -o cc dd ee ff gg hh ii -o jj kk".split() + + self.add_argument(3, None) + with self.assertRaises(SystemExit): + self.parser.parse_args(["--foo"] + args) + + # ============================ # from argparse import * tests # ============================