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 Mon May 06 15:23:15 2013 -0700 @@ -1867,6 +1867,46 @@ .. _upgrading-optparse-code: + +Intermixed parsing +^^^^^^^^^^^^^^^^^^ + +.. method:: ArgumentParser.parse_intermixed_args(args=None, namespace=None) +.. method:: ArgumentParser.parse_known_intermixed_args(args=None, namespace=None) + +Some users expect to freely intermix optional and positional argument strings. For +example, :mod:`optparse`, by default, allows interspersed argument strings. +GNU :c:func:`getopt` +permutes the argument strings so non-options are at the end. +The :meth:`~ArgumentParser.parse_intermixed_args` method emulates this behavior +by first calling :meth:`~ArgumentParser.parse_known_args` with just the +optional arguments being active. It is then called a second time to parse the list +of remaining argument strings using the positional arguments. + +:meth:`~ArgumentParser.parse_intermixed_args` raises an error if the +parser uses features that are incompatible with this two step parsing. +These include subparsers, ``argparse.REMAINDER``, and mutually exclusive +groups that include both optionals and positionals. + +In this example, :meth:`~ArgumentParser.parse_known_args` returns an unparsed +list of arguments `['2', '3']`, while :meth:`~ArgumentParser.parse_intermixed_args` +returns `rest=[1, 2, 3]`. +:: + + >>> parser = argparse.ArgumentParser() + >>> parser.add_argument('--foo') + >>> parser.add_argument('cmd') + >>> parser.add_argument('rest', nargs='*', type=int) + >>> parser.parse_known_args('cmd1 1 --foo bar 2 3'.split()) + (Namespace(cmd='cmd1', foo='bar', rest=[1]), ['2', '3']) + >>> parser.parse_intermixed_args('cmd1 1 --foo bar 2 3'.split()) + Namespace(cmd='cmd1', foo='bar', rest=[1, 2, 3]) + +:meth:`~ArgumentParser.parse_known_intermixed_args` method, returns a +two item tuple containing the populated namespace and the list of +remaining argument strings. :meth:`~ArgumentParser.parse_intermixed_args` +raises an error if there are any remaining unparsed argument strings. + Upgrading optparse code ----------------------- @@ -1903,3 +1943,6 @@ * Replace the OptionParser constructor ``version`` argument with a call to ``parser.add_argument('--version', action='version', version='')`` + +* Use :meth:`~ArgumentParser.parse_intermixed_args` if + interspersing switches with command arguments is important. \ No newline at end of file diff -r 8a364deb0225 Lib/argparse.py --- a/Lib/argparse.py Thu May 02 10:44:04 2013 -0700 +++ b/Lib/argparse.py Mon May 06 15:23:15 2013 -0700 @@ -2208,6 +2208,92 @@ return nargs_pattern # ======================== + # Alt command line argument parsing, allowing free intermix + # ======================== + + def parse_intermixed_args(self, args=None, namespace=None): + args, argv = self.parse_known_intermixed_args(args, namespace) + if argv: + msg = _('unrecognized arguments: %s') + self.error(msg % ' '.join(argv)) + return args + + def parse_known_intermixed_args(self, args=None, namespace=None, _fallback=None): + # self - argparse parser + # args, namespace - as used by parse_known_args + # _fallback - action to take if it can't handle this parser's arguments + # (default raises an error) + # returns a namespace and list of extras + + # positional can be freely intermixed with optionals + # optionals are first parsed with all positional arguments deactivated + # the 'extras' are then parsed + # positionals 'deactivated' by setting nargs=0 + + positionals = self._get_positional_actions() + a = [action for action in positionals if action.nargs in [PARSER, REMAINDER]] + if a: + if _fallback is None: + a = a[0] + err = ArgumentError(a, 'parse_intermixed_args: positional arg with nargs=%s'%a.nargs) + self.error(str(err)) + else: + return _fallback(args, namespace) + + if [action.dest for group in self._mutually_exclusive_groups + for action in group._group_actions if action in positionals]: + if _fallback is None: + self.error('parse_intermixed_args: positional in mutuallyExclusiveGroup') + else: + return _fallback(args, namespace) + + save_usage = self.usage + if self.usage is None: + # capture the full usage for use in error messages + self.usage = self.format_usage()[7:] + for action in positionals: + # deactivate positionals + action.save_nargs = action.nargs + action.nargs = 0 + try: + namespace, remaining_args = self.parse_known_args(args, namespace) + for action in positionals: + # remove the empty positional values from namespace + if hasattr(namespace, action.dest): + delattr(namespace, action.dest) + except SystemExit: + # warn('error from 1st parse_known_args') + raise + finally: + # restore nargs and usage before exiting + for action in positionals: + action.nargs = action.save_nargs + self.usage = save_usage + # logging.info('1st: %s,%s'%(namespace, remaining_args)) + # parse positionals + # optionals aren't normally required, but just in case, turn that off + optionals = self._get_optional_actions() + for action in optionals: + action.save_required = action.required + action.required = False + for group in self._mutually_exclusive_groups: + group.save_required = group.required + group.required = False + try: + namespace, extras = self.parse_known_args(remaining_args, namespace) + except SystemExit: + # warn('error from 2nd parse_known_args') + raise + finally: + # restore parser values before exiting + for action in optionals: + action.required = action.save_required + for group in self._mutually_exclusive_groups: + group.required = group.save_required + self.usage = save_usage + return namespace, extras + + # ======================== # Value conversion methods # ======================== def _get_values(self, action, arg_strings): 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 Mon May 06 15:23:15 2013 -0700 @@ -4714,6 +4714,94 @@ self.assertEqual(NS(v=3, spam=True, badger="B"), args) self.assertEqual(["C", "--foo", "4"], extras) +# =========================== +# parse_intermixed_args tests +# =========================== + +class TestIntermixedArgs(TestCase): + def test_basic(self): + # test parsing intermixed optionals and positionals + parser = argparse.ArgumentParser(prog='PROG') + parser.add_argument('--foo', dest='foo') + bar = parser.add_argument('--bar', dest='bar', required=True) + parser.add_argument('cmd') + parser.add_argument('rest', nargs='*', type=int) + argv = 'cmd --foo x 1 --bar y 2 3'.split() + args = parser.parse_intermixed_args(argv) + # rest gets [1,2,3] despite the foo and bar strings + self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1, 2, 3]), args) + + args, extras = parser.parse_known_args(argv) + # cannot parse the '1,2,3' + self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[]), args) + self.assertEqual(["1", "2", "3"], extras) + + argv = 'cmd --foo x 1 --error 2 --bar y 3'.split() + args, extras = parser.parse_known_intermixed_args(argv) + # unknown optionals go into extras + self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1]), args) + self.assertEqual(['--error', '2', '3'], extras) + + # restores attributes that were temporarily changed + self.assertIsNone(parser.usage) + self.assertEqual(bar.required, True) + + def test_remainder(self): + # Intermixed and remainder are incompatible + parser = ErrorRaisingArgumentParser(prog='PROG') + parser.add_argument('-z') + parser.add_argument('x') + parser.add_argument('y', nargs='...') + argv = 'X A B -z Z'.split() + # intermixed fails with '...' (also 'A...') + # but there is a fallback mechanism + self.assertRaises(ArgumentParserError, parser.parse_intermixed_args, argv) + args, extras = parser.parse_known_intermixed_args(argv, _fallback=parser.parse_known_args) + self.assertEqual(NS(x='X', y=['A', 'B', '-z', 'Z'], z=None), args) + + def test_exclusive(self): + # mutually exclusive group; intermixed works fine + parser = ErrorRaisingArgumentParser(prog='PROG') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--foo', action='store_true', help='FOO') + group.add_argument('--spam', help='SPAM') + parser.add_argument('badger', nargs='*', default='X', help='BADGER') + args = parser.parse_intermixed_args('1 --foo 2'.split()) + self.assertEqual(NS(badger=['1', '2'], foo=True, spam=None), args) + self.assertRaises(ArgumentParserError, parser.parse_intermixed_args, '1 2'.split()) + self.assertEqual(group.required, True) + + def test_exclusive_incompatible(self): + # mutually exclusive group including positional - fail + parser = ErrorRaisingArgumentParser(prog='PROG') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--foo', action='store_true', help='FOO') + group.add_argument('--spam', help='SPAM') + group.add_argument('badger', nargs='*', default='X', help='BADGER') + self.assertRaises(ArgumentParserError, parser.parse_intermixed_args, []) + # could include a fallback example + self.assertEqual(group.required, True) + +class TestIntermixedMessageContentError(TestCase): + # case where Intermixed gives different error message + # error is raised by 1st parsing step + def test_missing_argument_name_in_message(self): + parser = ErrorRaisingArgumentParser(prog='PROG', usage='') + parser.add_argument('req_pos', type=str) + parser.add_argument('-req_opt', type=int, required=True) + + with self.assertRaises(ArgumentParserError) as cm: + parser.parse_args([]) + msg = str(cm.exception) + self.assertRegex(msg, 'req_pos') + self.assertRegex(msg, 'req_opt') + + with self.assertRaises(ArgumentParserError) as cm: + parser.parse_intermixed_args([]) + msg = str(cm.exception) + self.assertNotRegex(msg, 'req_pos') + self.assertRegex(msg, 'req_opt') + # ========================== # add_argument metavar tests # ==========================