diff -r 8a364deb0225 Doc/library/argparse.rst --- a/Doc/library/argparse.rst Thu May 02 10:44:04 2013 -0700 +++ b/Doc/library/argparse.rst Thu May 09 15:59:26 2013 -0700 @@ -880,6 +880,21 @@ >>> print(parser.parse_args('--foo B cmd --arg1 XX ZZ'.split())) Namespace(args=['--arg1', 'XX', 'ZZ'], command='cmd', foo='B') + +* ``'{m,n}'``. `m` to `n` command-line arguments are gathered into a list. + This is modeled on the Regular Expression use. ``'{,n}'`` gathers up to + `n` arguments. ``'{m,}'`` gathers `m` or more. Thus ``'{1,}'`` is the + equivalent to ``'+'``, and ``'{,1}'`` to ``'?'``. A tuple notation is + also accepted, ``'(m,n)'``, ``'(None,n)'``, ``'(m,None)'``. For example:: + + >>> parser = argparse.ArgumentParser(prog='PROG') + >>> parser.add_argument('--foo', nargs='{2,4}') + >>> parser.parse_args('--foo a b c'.split()) + Namespace(foo=['a', 'b', 'c']) + >>> parser.parse_args('--foo a'.split()) + usage: PROG [-h] [--foo FOO{2,4}] + PROG: error: argument --foo: expected {2,4} arguments + If the ``nargs`` keyword argument is not provided, the number of arguments consumed is determined by the action_. Generally this means a single command-line argument will be consumed and a single item (not a list) will be produced. diff -r 8a364deb0225 Lib/argparse.py --- a/Lib/argparse.py Thu May 02 10:44:04 2013 -0700 +++ b/Lib/argparse.py Thu May 09 15:59:26 2013 -0700 @@ -136,6 +136,29 @@ setattr(namespace, name, value) return getattr(namespace, name) +def _is_mnrep(nargs): + # test for are like string, {n,m} + # return valid nargs, or False if not valid + # it also converts a (m,n) tuple to equivalent {m,n} string + if nargs is None: + return False + if isinstance(nargs, int): + return False + if isinstance(nargs, tuple): + if len(nargs)==2: + nargs = '{%s,%s}'%nargs + nargs = nargs.replace('None','') + else: + raise ValueError('nargs tuple requires 2 integers') + m = _re.match('{(\d*),(\d*)}',nargs) + if m: + try: + x = _re.compile('[-A]%s'%nargs) + return nargs + except _re.error as e: + raise ValueError(str(e)) + else: + return False # =============== # Formatting Help @@ -581,7 +604,12 @@ result = '...' elif action.nargs == PARSER: result = '%s ...' % get_metavar(1) + elif _is_mnrep(action.nargs): + result = '%s%s' % (get_metavar(1)[0], action.nargs) else: + if not isinstance(action.nargs, int): + valid_nargs = [None,OPTIONAL,ZERO_OR_MORE,ONE_OR_MORE,REMAINDER,PARSER] + raise ValueError('nargs %r not integer or %s'%(action.nargs, valid_nargs)) formats = ['%s' for _ in range(action.nargs)] result = ' '.join(formats) % get_metavar(action.nargs) return result @@ -1325,6 +1353,9 @@ if not callable(type_func): raise ValueError('%r is not callable' % (type_func,)) + if hasattr(self, "_check_argument"): + self._check_argument(action) + # raise an error if the metavar does not match the type if hasattr(self, "_get_formatter"): try: @@ -1534,6 +1565,7 @@ self._has_negative_number_optionals = \ container._has_negative_number_optionals self._mutually_exclusive_groups = container._mutually_exclusive_groups + self._check_argument = container._check_argument def _add_action(self, action): action = super(_ArgumentGroup, self)._add_action(action) @@ -1707,6 +1739,27 @@ for action in self._actions if not action.option_strings] + def _check_argument(self, action): + # check action arguments + # focus on the arguments that the parent container does not know about + # check nargs and metavar tuple + + # test for {m,n} rep; convert a (m,n) tuple if needed + try: + nargs = _is_mnrep(action.nargs) + if nargs: + action.nargs = nargs + except ValueError as e: + raise ArgumentError(action, str(e)) + + try: + self._get_formatter()._format_args(action, None) + except ValueError as e: + raise ArgumentError(action, str(e)) + except TypeError: + #raise ValueError("length of metavar tuple does not match nargs") + raise ArgumentError(action, "length of metavar tuple does not match nargs") + # ===================================== # Command line argument parsing methods # ===================================== @@ -2195,8 +2248,14 @@ elif nargs == PARSER: nargs_pattern = '(-*A[-AO]*)' + # n to m arguments, nargs is re like {n,m} + elif _is_mnrep(nargs): + nargs_pattern = '([-A]%s)'%nargs + # all others should be integers else: + if not isinstance(action.nargs, int): + raise ValueError('nargs %r not integer or valid string'%(action.nargs)) nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs) # if this is an optional action, -- is not allowed diff -r 8a364deb0225 Lib/test/test_argparse.py --- a/Lib/test/test_argparse.py Thu May 02 10:44:04 2013 -0700 +++ b/Lib/test/test_argparse.py Thu May 09 15:59:26 2013 -0700 @@ -4251,6 +4251,40 @@ self.assertRaises(Success, parser.add_argument, 'spam', action=Action, default=Success, const=Success) + def test_nargs_value(self): + """test for invalid values of nargs, not integer or accepted string + tests parser and groups""" + error_msg = r"nargs (.*) not integer or" + error_msg = r"argument --foo: nargs (.*) not integer or \[(.*)\]" + error_type = argparse.ArgumentError + parser = ErrorRaisingArgumentParser() + group = parser.add_argument_group('g') + m = parser.add_mutually_exclusive_group() + + with self.assertRaisesRegex(error_type, error_msg): + parser.add_argument('--foo', nargs='1') + with self.assertRaisesRegex(error_type, error_msg): + group.add_argument('--foo', nargs='**') + with self.assertRaisesRegex(error_type, error_msg): + m.add_argument('--foo', nargs='1') + + def test_nargs_metavar_tuple(self): + "test that metavar tuple matches with nargs; test parser and groups" + error_msg = r'length of metavar tuple does not match nargs' + error_type = ValueError + error_msg = r'argument (.*): length of metavar tuple does not match nargs' + error_type = argparse.ArgumentError + parser = ErrorRaisingArgumentParser() + group = parser.add_argument_group('g') + m = parser.add_mutually_exclusive_group() + + with self.assertRaisesRegex(error_type, error_msg): + parser.add_argument('-w', help='w', nargs='+', metavar=('W1',)) + with self.assertRaisesRegex(error_type, error_msg): + group.add_argument('-x', help='x', nargs='*', metavar=('X1', 'X2', 'x3')) + with self.assertRaisesRegex(error_type, error_msg): + m.add_argument('-y', help='y', nargs=3, metavar=('Y1', 'Y2')) + # ================================ # Actions returned by add_argument # ================================ @@ -4720,19 +4754,23 @@ class TestAddArgumentMetavar(TestCase): - EXPECTED_MESSAGE = "length of metavar tuple does not match nargs" + EXPECTED_MESSAGE = "argument --foo: length of metavar tuple does not match nargs" + EXPECTED_ERROR = argparse.ArgumentError def do_test_no_exception(self, nargs, metavar): parser = argparse.ArgumentParser() parser.add_argument("--foo", nargs=nargs, metavar=metavar) + #def do_test_exception(self, nargs, metavar): + # parser = argparse.ArgumentParser() + # with self.assertRaises(ValueError) as cm: + # parser.add_argument("--foo", nargs=nargs, metavar=metavar) + # self.assertEqual(cm.exception.args[0], self.EXPECTED_MESSAGE) + def do_test_exception(self, nargs, metavar): parser = argparse.ArgumentParser() - with self.assertRaises(ValueError) as cm: + with self.assertRaisesRegex(self.EXPECTED_ERROR, self.EXPECTED_MESSAGE): parser.add_argument("--foo", nargs=nargs, metavar=metavar) - self.assertEqual(cm.exception.args[0], self.EXPECTED_MESSAGE) - - # Unit tests for different values of metavar when nargs=None def test_nargs_None_metavar_string(self): self.do_test_no_exception(nargs=None, metavar="1") @@ -4886,6 +4924,117 @@ self.do_test_no_exception(nargs=3, metavar=("1", "2", "3")) # ============================ +# test when nargs is range +# ============================ + +class TestNargsRangeAddArgument(TestCase): + """Test processing nargs when range ((m,n) or {m,n}) is set""" + def test1(self): + parser = ErrorRaisingArgumentParser(prog='PROG') + with self.assertRaises(argparse.ArgumentError): + parser.add_argument('-a', nargs=() ) + with self.assertRaises(argparse.ArgumentError): + parser.add_argument('-a', nargs=(1,) ) + with self.assertRaises(argparse.ArgumentError): + parser.add_argument('-a', nargs=('xxx', None)) + with self.assertRaises(argparse.ArgumentError): + parser.add_argument('-a', nargs=(None, 'xxx')) + with self.assertRaises(argparse.ArgumentError): + parser.add_argument('-a', nargs=(8, 3)) + + def test_add_argument5(self): + "nargs = (lo, hi), where lo < hi" + parser = ErrorRaisingArgumentParser(prog='PROG') + arg = parser.add_argument('-a', nargs=(3,5)) + self.assertEqual(arg.nargs, '{3,5}') + + + def test_add_argument11(self): + "nargs = (None, number) => (0, number)" + parser = ErrorRaisingArgumentParser(prog='PROG') + arg = parser.add_argument('-a', nargs=(None, 87)) + self.assertEqual(arg.nargs, '{,87}') + +class TestNargumentRangeArgumentParsing(ParserTestCase): + "Test parsing arguments when nargs sets range of options count" + + argument_signatures = [Sig('positional', nargs='*'), + Sig('-o','--optional'), + Sig('--foo', nargs=(3,7))] + failures = ["--foo aa bb -o cc dd ee ff gg hh", + "--foo -o aa bb cc dd ee ff gg hh"] + successes = [ + ('--foo aa bb cc dd ee ff gg hh', + NS(foo='aa bb cc dd ee ff gg'.split(), optional=None, positional=['hh'])), + ('--foo aa bb cc dd -o ee ff gg hh', + NS(foo='aa bb cc dd'.split(), + positional='ff gg hh'.split(), + optional='ee')), + ] + +class TestNargumentRangeArgumentParsing5(ParserTestCase): + "Test parsing arguments when nargs sets range of options count" + + argument_signatures = [Sig('positional', nargs='*'), + Sig('-o','--optional'), + Sig('--foo', nargs=(3,None))] + failures = ["--foo aa bb -o cc dd ee ff gg hh ii -o jj kk"] + successes = [ + ('--foo aa bb cc dd ee ff gg hh ii -o jj kk', + NS(foo='aa bb cc dd ee ff gg hh ii'.split(), optional='jj', positional=['kk'])), + ] + +class TestNargsRange1(ParserTestCase): + argument_signatures = [Sig('pos', nargs='{2,4}', type=int), + Sig('rest', nargs='*')] + failures = ['1'] + successes = [ + ('1 2', NS(pos=[1,2], rest=[])), + ('1 2 3', NS(pos=[1,2,3], rest=[])), + ('1 2 3 4', NS(pos=[1,2,3,4], rest=[])), + ('1 2 3 4 bar', NS(pos=[1,2,3,4], rest=['bar'])), + ] + +class TestNargsRange2(ParserTestCase): + argument_signatures = [Sig('pos', nargs='{2,4}', type=int), + Sig('rest')] + failures = ['1', '1 2', ] + successes = [ + ('1 2 bar', NS(pos=[1,2], rest='bar')), + ('1 2 3', NS(pos=[1,2], rest='3')), + ('1 2 3 4', NS(pos=[1,2,3], rest='4')), + ('1 2 3 4 bar', NS(pos=[1,2,3,4], rest='bar')), + ] + # # "usage: [-h] 'pos',{2,4}\n\npositional arguments:\n pos\n\noptional arguments:\n -h, --help show this help message and exit\n" + +class TestNargsRange3(ParserTestCase): + argument_signatures = [Sig('pos', nargs='{2,}', type=int)] + failures = ['', '1'] + successes = [ + ('1 2', NS(pos=[1,2])), + ('1 2 3 4 5 6', NS(pos=[1,2,3,4,5,6])), + ] + +class TestNargsRange4(ParserTestCase): + argument_signatures = [Sig('pos', nargs='{,2}', type=int)] + failures = ['1 2 3'] + successes = [ + ('', NS(pos=[])), + ('1', NS(pos=[1])), + ('1 2', NS(pos=[1,2])), + ] + +class TestNargsRange5(ParserTestCase): + argument_signatures = [Sig('pos', nargs='{2,2}', type=int)] + failures = ['','1','1 2 3'] + successes = [ + ('1 2', NS(pos=[1,2])), + ] + + + + +# ============================ # from argparse import * tests # ============================