classification
Title: argparse: nargs could accept range of options count
Type: enhancement Stage: patch review
Components: Library (Lib) Versions: Python 3.4
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: Danh, atfrase, bethard, paul.j3, wm
Priority: normal Keywords: patch

Created on 2011-02-28 16:51 by wm, last changed 2013-05-09 23:29 by paul.j3.

Files
File name Uploaded Description Edit
argparse-nargs.patch wm, 2011-02-28 16:51 patch
test.py wm, 2011-03-02 20:24
issue11354.patch wm, 2011-04-11 17:40 patch (lib & tests) review
prelimary.patch paul.j3, 2013-05-08 22:32 review
nargsrange.patch paul.j3, 2013-05-09 23:29 review
Messages (11)
msg129714 - (view) Author: Wojciech Muła (wm) Date: 2011-02-28 16:51
Hi, sometimes it is needed to grab variable, but limited, number of options.
For example --point could accept 2, 3 or 4 values: (x,y) or (x,y,z) or
(x,y,z,w). Current version of argparse requires postprocessing:

	parser.add_argument('--point', action='append', default=[])
	nmps = parser.parse_args()
	if not (2 <= len(nmsp.point) <= 4):
		raise argparse.ArgumentTypeError("--point expects 2, 3 or 4 values")

I propose to allow pass range of options count to nargs, including
lower/upper bound. For example:
	
	parser.add_argument('--point', nargs=(2,4) )	# from 2 to 4
	parser.add_argument('--foo', nargs=(9, None) )	# at least 9
	parser.add_argument('--bar', nargs=(None, 7) )	# at most 7
	nmsp = parser.parse_args()

I've attached tests and patch made against Python3.2 lib from Debian.

w.
msg129715 - (view) Author: Wojciech Muła (wm) Date: 2011-02-28 16:52
tests
msg129755 - (view) Author: Daniel Haertle (Danh) Date: 2011-03-01 12:57
Hi Wojciech,

in your tests, at

    def test_add_argument10(self):
        "nargs = (0, 1) => optimized to '?'"
        opt = self.add_argument(1, None)
        self.assertEqual(opt.nargs, argparse.ONE_OR_MORE)

you should change "argparse.ONE_OR_MORE" to "argparse.OPTIONAL".
msg129917 - (view) Author: Wojciech Muła (wm) Date: 2011-03-02 20:24
Daniel, thanks for note, fixed.
msg132240 - (view) Author: Steven Bethard (bethard) * (Python committer) Date: 2011-03-26 14:07
Thanks for the patch. The idea and the approach of the patch look fine. But the patch needs to be against the Python repository:

http://docs.python.org/devguide/patch.html#creating

For the tests, you should integrate your test.py into Lib/test/test_argparse.py.
msg133537 - (view) Author: Wojciech Muła (wm) Date: 2011-04-11 17:40
Steven, thank you for links, I prepared patch against trunk.
All tests passed.
msg162475 - (view) Author: Alex Frase (atfrase) Date: 2012-06-07 16:01
I'm new here so I apologize if this is considered poor etiquette, but I'm just commenting to 'bump' this issue.  My search for a way to accomplish exactly this functionality led me here, and it seems a patch was offered but no action has been taken on it for over a year now.  Alas, it seems I must write a custom Action handler instead.
msg166171 - (view) Author: Steven Bethard (bethard) * (Python committer) Date: 2012-07-22 21:17
The tests look like they're testing the right things, but the tests should instead be written like the rest of the argparse tests. For example, look at TestOptionalsNargs3 and TestPositionalsNargs2. You could write your tests to look something like those, e.g.

class TestOptionalsNargs1_3(ParserTestCase):

    argument_signatures = [Sig('-x', nargs=(1, 3))]
    failures = ['a', '-x', '-x a b c d']
    successes = [
        ('', NS(x=None)),
        ('-x a', NS(x=['a'])),
        ('-x a b', NS(x=['a', 'b'])),
        ('-x a b c', NS(x=['a', 'b', 'c'])),
    ]

Also, a complete patch will need to document the new feature in the Python documentation, among other things. See the details described here:

http://docs.python.org/devguide/patch.html#preparation
msg188743 - (view) Author: paul j3 (paul.j3) * Date: 2013-05-08 22:32
Wouldn't it be simpler to use the re {m,n} notation to grab the appropriate number of arguments?  

In ArgumentParser _get_nargs_pattern we could add:

+        # n to m arguments, nargs is re like {n,m}
+        elif is_mnrep(nargs):
+            nargs_pattern = '([-A]%s)'%nargs

        # all others should be integers

where is_mnrep() tests that the nargs string looks like '{m,n}' (or '{,n}' or '{m,}').  The resulting nargs_pattern will look like

    '([-A]{m,n})'

In _format_args() a similar addition would be:

+        elif is_mnrep(action.nargs):
+            result = '%s%s' % (get_metavar(1)[0], action.nargs)

   'FOO{2,5}'

It would take more code to generate

   FOO FOO [FOO [FOO [FOO]]]

A matching programmer input would be:

   parser.add_argument('Foo', nargs='{2,5}')

though it wouldn't be much work to also translate a tuple.

   parser.add_argument('Foo', nargs=(2,5))

I'll try to test this implementation against wm's tests.
msg188757 - (view) Author: paul j3 (paul.j3) * Date: 2013-05-09 08:01
I think this patch should build on http://bugs.python.org/issue9849, which seeks to improve the error checking for nargs.  There, add_argument returns an ArgumentError if the nargs value is not a valid string, interger, or it there is mismatch between a tuple metavar and nargs.  

This range nargs should be tested at the same time, and return a ArgumentError if possible.
msg188795 - (view) Author: paul j3 (paul.j3) * Date: 2013-05-09 23:29
This patch adds this `nargs='{m,n}'` or `nargs=(m,n)` feature.

It builds on the `better nargs error message` patch in http://bugs.python.org/msg187754

It includes an argparse.rst paragraph, changes to argparse.py, and additions to test_argparse.py.

The tests include those proposed by wm, rewritten to use the ParserTestCase framework where possible.  I did not give a lot of thought to test names.  The coverage could also use further thought.

As WM noted some range cases are the equivalent to existing nargs options ('{1,}'=='+').  I did not attempt to code or test such equivalences.  Since the '{0,}' form uses regex matching just like '*',
I don't think there is any advantage to making such a translation.  

I convert the tuple version (m,n) to the re string '{m,n}' during the add_argument() testing.  So it is the string form that is added to the parser action.  This is also the format that appears in usage.

The documentation paragraph is:

'{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
History
Date User Action Args
2013-05-09 23:29:08paul.j3setfiles: + nargsrange.patch

messages: + msg188795
2013-05-09 08:01:33paul.j3setmessages: + msg188757
2013-05-08 22:32:57paul.j3setfiles: + prelimary.patch
nosy: + paul.j3
messages: + msg188743

2012-07-22 21:17:50bethardsetmessages: + msg166171
versions: + Python 3.4, - Python 3.3
2012-06-07 16:01:57atfrasesetnosy: + atfrase
messages: + msg162475
2011-04-11 17:40:23wmsetfiles: + issue11354.patch

messages: + msg133537
2011-03-26 14:07:16bethardsetstage: patch review
messages: + msg132240
versions: - Python 3.2
2011-03-02 21:46:34SilentGhostsetnosy: + bethard
2011-03-02 20:24:25wmsetfiles: + test.py

messages: + msg129917
2011-03-02 20:22:32wmsetfiles: - test.py
2011-03-01 12:57:50Danhsetnosy: + Danh
messages: + msg129755
2011-02-28 16:52:26wmsetfiles: + test.py

messages: + msg129715
2011-02-28 16:51:33wmcreate