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
# ==========================