0

Is there a more elegant way to pass an optional list of integers to argparse than to pass a delimited string and parse it later? I also have a positional argument.

parser.add_argument('--ids', type=int, nargs='+')
parser.add_argument('cmd')

doesn't work, because argparse attempts to grab cmd and complains that it isn't an integer.

Ideally, I'd like to execute with one of

program.py --ids 6,32,12 refresh
program.py --ids 6 32 12 refresh

or something similar, but also be able to

program.py refresh
Community
  • 1
  • 1
Michael
  • 8,362
  • 6
  • 61
  • 88
  • 1
    You could parse the string immediately instead of later, by passing an arbitrary callable as the `type`. – Kevin Mar 13 '15 at 18:07
  • @Kevin a la the [`perfect_square`](https://docs.python.org/3/library/argparse.html#type) example? – Michael Mar 13 '15 at 18:11
  • 1
    Yes, precisely. You could use something like [`operator.methodcaller('split', ',')`](https://docs.python.org/3/library/operator.html#operator.methodcaller) as your `type`, to split on commas automatically. – Kevin Mar 13 '15 at 18:13

2 Answers2

3

-- is a handy way of saying 'positional arguments start here'.

With your parser, these work:

program.py refresh   # sets ids=None
program.py refresh --ids 1 2 3
program.py --ids 1 2 3 -- refresh

You could give the --ids argument a default (e.g. []) if you don't like the None.

Any problems with program.py refesh --ids 1,2,3 are due to how the shell splits your command line. Look at the sys.argv list.

Problems with program.py --ids 1 2 3 refresh arise because when handling --ids, the parser tries to use all strings that follow that aren't obviously flags (e.g. with '-'). It does not use the 'int' type to test which ones to use and which to leave.

Now if the ids were positional, it would handle the 1 2 3 refresh:

parser.add_argument('ids',type=int,nargs='+')
parser.add_argument('cmd')
parser.parse_args('1 2 3 refresh'.split())

But that's because the parser uses a different strategy to allocate strings to several positional arguments. It uses a re matcher that looks like A+A.


Kevin's type approach might be better implemented with a simple function:

def mytype(astring):
    ll = astring.split(',')
    return [int(l) for l in ll]
parser.add_argument('--ids', type=mytype)

It could be generalized to handle quoted strings like "1 2 3". type can be any function that takes a string, and returns the desired value(s), and raises an error if it can't do the conversion.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
2

If you just want to parse arguments of the form --ids 1,2,3 (no whitespace), you can use something like this:

def convert(argument):
    return map(int, argument.split(','))  # 3.x: consider wrapping in list()

parser.add_argument('--ids', type=convert)

This will not handle arguments separated by whitespace, though you could probably mitigate that somewhat with a smarter convert() function. You would then need to quote them, however, or else the shell would pass them as separate arguments.

Kevin
  • 28,963
  • 9
  • 62
  • 81