diff -r eb1025b107c5 Doc/library/argparse.rst
--- a/Doc/library/argparse.rst Mon May 27 23:53:02 2013 -0400
+++ b/Doc/library/argparse.rst Wed May 29 22:27:18 2013 -0700
@@ -1865,6 +1865,46 @@
This method prints a usage message including the *message* to the
standard error and terminates the program with a status code of 2.
+
+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:
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 eb1025b107c5 Lib/argparse.py
--- a/Lib/argparse.py Mon May 27 23:53:02 2013 -0400
+++ b/Lib/argparse.py Wed May 29 22:27:18 2013 -0700
@@ -581,6 +581,8 @@
result = '...'
elif action.nargs == PARSER:
result = '%s ...' % get_metavar(1)
+ elif action.nargs == SUPPRESS:
+ result = ''
else:
formats = ['%s' for _ in range(action.nargs)]
result = ' '.join(formats) % get_metavar(action.nargs)
@@ -2195,6 +2197,10 @@
elif nargs == PARSER:
nargs_pattern = '(-*A[-AO]*)'
+ # suppress action, like nargs=0
+ elif nargs == SUPPRESS:
+ nargs_pattern = '(-*-*)'
+
# all others should be integers
else:
nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs)
@@ -2208,6 +2214,86 @@
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):
+ # self - argparse parser
+ # args, namespace - as used by parse_known_args
+ # 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
+ # if parser definition is incompatible with the intermixed assumptions
+ # returns a TypeError (e.g. use of REMAINDER, subparsers)
+
+ # positionals are 'deactivated' by setting nargs and default to SUPPRESS.
+ # This blocks the addition of that positional to the namespace
+
+ positionals = self._get_positional_actions()
+ a = [action for action in positionals if action.nargs in [PARSER, REMAINDER]]
+ if a:
+ raise TypeError('parse_intermixed_args: positional arg with nargs=%s'%a[0].nargs)
+
+ if [action.dest for group in self._mutually_exclusive_groups
+ for action in group._group_actions if action in positionals]:
+ raise TypeError('parse_intermixed_args: positional in mutuallyExclusiveGroup')
+
+ save_usage = self.usage
+ try:
+ 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
+ action.nargs = SUPPRESS
+ action.save_default = action.default
+ action.default = SUPPRESS
+ 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) and getattr(namespace, action.dest)==[]:
+ from warnings import warn
+ warn('Do not expect %s in %s'%(action.dest,namespace))
+ delattr(namespace, action.dest)
+ finally:
+ # restore nargs and usage before exiting
+ for action in positionals:
+ action.nargs = action.save_nargs
+ action.default = action.save_default
+ # 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)
+ 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
+ finally:
+ self.usage = save_usage
+ return namespace, extras
+
+ # ========================
# Value conversion methods
# ========================
def _get_values(self, action, arg_strings):
@@ -2253,6 +2339,10 @@
value = [self._get_value(action, v) for v in arg_strings]
self._check_value(action, value[0])
+ # SUPPRESS argument does not put anything in the namespace
+ elif action.nargs == SUPPRESS:
+ value = SUPPRESS
+
# all other types of nargs produce a list
else:
value = [self._get_value(action, v) for v in arg_strings]
diff -r eb1025b107c5 Lib/test/test_argparse.py
--- a/Lib/test/test_argparse.py Mon May 27 23:53:02 2013 -0400
+++ b/Lib/test/test_argparse.py Wed May 29 22:27:18 2013 -0700
@@ -4714,6 +4714,93 @@
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...')
+ # self.assertRaises(TypeError, parser.parse_intermixed_args, argv)
+ with self.assertRaises(TypeError) as cm:
+ parser.parse_intermixed_args(argv)
+ self.assertRegex(str(cm.exception), '\.\.\.')
+
+ 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(TypeError, parser.parse_intermixed_args, [])
+ 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
# ==========================