Related
I want to create a command line flag that can be used as
./prog.py --myarg=abcd,e,fg
and inside the parser have this be turned into ['abcd', 'e', 'fg'] (a tuple would be fine too).
I have done this successfully using action and type, but I feel like one is likely an abuse of the system or missing corner cases, while the other is right. However, I don't know which is which.
With action:
import argparse
class SplitArgs(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values.split(','))
parser = argparse.ArgumentParser()
parser.add_argument('--myarg', action=SplitArgs)
args = parser.parse_args()
print(args.myarg)
Instead with type:
import argparse
def list_str(values):
return values.split(',')
parser = argparse.ArgumentParser()
parser.add_argument('--myarg', type=list_str)
args = parser.parse_args()
print(args.myarg)
The simplest solution is to consider your argument as a string and split.
#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--myarg", type=str)
d = vars(parser.parse_args())
if "myarg" in d.keys():
d["myarg"] = [s.strip() for s in d["myarg"].split(",")]
print(d)
Result:
$ ./toto.py --myarg=abcd,e,fg
{'myarg': ['abcd', 'e', 'fg']}
$ ./toto.py --myarg="abcd, e, fg"
{'myarg': ['abcd', 'e', 'fg']}
I find your first solution to be the right one. The reason is that it allows you to better handle defaults:
names: List[str] = ['Jane', 'Dave', 'John']
parser = argparse.ArumentParser()
parser.add_argument('--names', default=names, action=SplitArgs)
args = parser.parse_args()
names = args.names
This doesn't work with list_str because the default would have to be a string.
Your custom action is the closest way to how it is done internally for other argument types. IMHO there should be a _StoreCommaSeperatedAction added to argparse in the stdlib since it is a somewhat common and useful argument type,
It can be used with an added default as well.
Here is an example without using an action (no SplitArgs class):
class Test:
def __init__(self):
self._names: List[str] = ["Jane", "Dave", "John"]
#property
def names(self):
return self._names
#names.setter
def names(self, value):
self._names = [name.strip() for name in value.split(",")]
test_object = Test()
parser = ArgumentParser()
parser.add_argument(
"-n",
"--names",
dest="names",
default=",".join(test_object.names), # Joining the default here is important.
help="a comma separated list of names as an argument",
)
print(test_object.names)
parser.parse_args(namespace=test_object)
print(test_object.names)
Here is another example using SplitArgs class inside a class completely
"""MyClass
Demonstrates how to split and use a comma separated argument in a class with defaults
"""
import sys
from typing import List
from argparse import ArgumentParser, Action
class SplitArgs(Action):
def __call__(self, parser, namespace, values, option_string=None):
# Be sure to strip, maybe they have spaces where they don't belong and wrapped the arg value in quotes
setattr(namespace, self.dest, [value.strip() for value in values.split(",")])
class MyClass:
def __init__(self):
self.names: List[str] = ["Jane", "Dave", "John"]
self.parser = ArgumentParser(description=__doc__)
self.parser.add_argument(
"-n",
"--names",
dest="names",
default=",".join(self.names), # Joining the default here is important.
action=SplitArgs,
help="a comma separated list of names as an argument",
)
self.parser.parse_args(namespace=self)
if __name__ == "__main__":
print(sys.argv)
my_class = MyClass()
print(my_class.names)
sys.argv = [sys.argv[0], "--names", "miigotu, sickchill,github"]
my_class = MyClass()
print(my_class.names)
And here is how to do it in a function based situation, with a default included
class SplitArgs(Action):
def __call__(self, parser, namespace, values, option_string=None):
# Be sure to strip, maybe they have spaces where they don't belong and wrapped the arg value in quotes
setattr(namespace, self.dest, [value.strip() for value in values.split(",")])
names: List[str] = ["Jane", "Dave", "John"]
parser = ArgumentParser(description=__doc__)
parser.add_argument(
"-n",
"--names",
dest="names",
default=",".join(names), # Joining the default here is important.
action=SplitArgs,
help="a comma separated list of names as an argument",
)
parser.parse_args()
I know this post is old but I recently found myself solving this exact problem. I used functools.partial for a lightweight solution:
import argparse
from functools import partial
csv_ = partial(str.split, sep=',')
p = argparse.ArgumentParser()
p.add_argument('--stuff', type=csv_)
p.parse_args(['--stuff', 'a,b,c'])
# Namespace(stuff=['a', 'b', 'c'])
If you're not familiar with functools.partial, it allows you to create a partially "frozen" function/method. In the above example, I created a new function (csv_) that is essentially a copy of str.split() except that the sep argument has been "frozen" to the comma character.
I am playing around with Python's argparse module in order to get a rather complicated and large sub-commands structure. So far, the arguments parse goes pretty well and everything works fine but I am looking for a better way to manage how sub-commands are executed.
Here is an example of my dummy/playaround application:
def a_func(cmd_args):
print(cmd_args)
def b_func(cmd_args):
print(cmd_args)
CMD_DISPATCHER = {
'a': a_func,
'b': b_func
}
def parse_commands():
# Create top-level parser
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcmd_name')
# Create parser for the "a" sub-command
parser_a = subparsers.add_parser('a')
parser_a.add_argument('-bar', type=int)
# Create parser for the "b" sub-command
parser_b = subparsers.add_parser('b')
parser_b.add_argument('-foo', type=int)
args = parser.parse_args()
CMD_DISPATCHER[args.subcmd_name](args)
def main():
parse_commands()
if __name__ == '__main__':
main()
As you can see, here I use a simple dict (CMD_DISPATCHER) as a map relating the sub-command name to its function.
As I said, this is just a simple example of what I want to achieve but the real project will have many nested sub-commands so I would like a better way to manage those several sub-commands.
Do you know a better/more-professional way to do this?
OK, after some more research I have found a good way that fits my needs: set_defaults. I have used this function earlier but always to set default existing default argument values. I did not notice the clear example that argparse documentation provides:
>>> # sub-command functions
>>> def foo(args):
... print(args.x * args.y)
...
>>> def bar(args):
... print('((%s))' % args.z)
...
>>> # create the top-level parser
>>> parser = argparse.ArgumentParser()
>>> subparsers = parser.add_subparsers()
>>>
>>> # create the parser for the "foo" command
>>> parser_foo = subparsers.add_parser('foo')
>>> parser_foo.add_argument('-x', type=int, default=1)
>>> parser_foo.add_argument('y', type=float)
>>> parser_foo.set_defaults(func=foo)
>>>
>>> # create the parser for the "bar" command
>>> parser_bar = subparsers.add_parser('bar')
>>> parser_bar.add_argument('z')
>>> parser_bar.set_defaults(func=bar)
>>>
>>> # parse the args and call whatever function was selected
>>> args = parser.parse_args('foo 1 -x 2'.split())
>>> args.func(args)
2.0
>>>
>>> # parse the args and call whatever function was selected
>>> args = parser.parse_args('bar XYZYX'.split())
>>> args.func(args)
((XYZYX))
As you can see the line parser_bar.set_defaults(func=bar) sets a new variable to parser_bar arguments. In this case bar is a function that is eventually used as the sub-command executor in line args.func(args).
I hope this helps someone in the future.
I have a script where I ask the user for a list of pre-defined actions to perform. I also want the ability to assume a particular list of actions when the user doesn't define anything. however, it seems like trying to do both of these together is impossible.
when the user gives no arguments, they receive an error that the default choice is invalid
acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', action='append', choices=acts, default=[['dump', 'clear']])
args = p.parse_args([])
>>> usage: [-h] [{clear,copy,dump,lock} [{clear,copy,dump,lock} ...]]
: error: argument action: invalid choice: [['dump', 'clear']] (choose from 'clear', 'copy', 'dump', 'lock')
and when they do define a set of actions, the resultant namespace has the user's actions appended to the default, rather than replacing the default
acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', action='append', choices=acts, default=[['dump', 'clear']])
args = p.parse_args(['lock'])
args
>>> Namespace(action=[['dump', 'clear'], ['dump']])
What you need can be done using a customized argparse.Action as in the following example:
import argparse
parser = argparse.ArgumentParser()
class DefaultListAction(argparse.Action):
CHOICES = ['clear','copy','dump','lock']
def __call__(self, parser, namespace, values, option_string=None):
if values:
for value in values:
if value not in self.CHOICES:
message = ("invalid choice: {0!r} (choose from {1})"
.format(value,
', '.join([repr(action)
for action in self.CHOICES])))
raise argparse.ArgumentError(self, message)
setattr(namespace, self.dest, values)
parser.add_argument('actions', nargs='*', action=DefaultListAction,
default = ['dump', 'clear'],
metavar='ACTION')
print parser.parse_args([])
print parser.parse_args(['lock'])
The output of the script is:
$ python test.py
Namespace(actions=['dump', 'clear'])
Namespace(actions=['lock'])
In the documentation (http://docs.python.org/dev/library/argparse.html#default), it is said :
For positional arguments with nargs equal to ? or *, the default value is used when no command-line argument was present.
Then, if we do :
acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', choices=acts, default='clear')
print p.parse_args([])
We get what we expect
Namespace(action='clear')
The problem is when you put a list as a default.
But I've seen it in the doc,
parser.add_argument('bar', nargs='*', default=[1, 2, 3], help='BAR!')
So, I don't know :-(
Anyhow, here is a workaround that does the job you want :
import sys, argparse
acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', choices=acts)
args = ['dump', 'clear'] # I set the default here ...
if sys.argv[1:]:
args = p.parse_args()
print args
I ended up doing the following:
no append
add the empty list to the possible choices or else the empty input breaks
without default
check for an empty list afterwards and set the actual default in that case
Example:
parser = argparse.ArgumentParser()
parser.add_argument(
'is',
type=int,
choices=[[], 1, 2, 3],
nargs='*',
)
args = parser.parse_args(['1', '3'])
assert args.a == [1, 3]
args = parser.parse_args([])
assert args.a == []
if args.a == []:
args.a = [1, 2]
args = parser.parse_args(['1', '4'])
# Error: '4' is not valid.
You could test whether the user is supplying actions (in which case parse it as a required, position argument), or is supplying no actions (in which case parse it as an optional argument with default):
import argparse
import sys
acts = ['clear', 'copy', 'dump', 'lock']
p = argparse.ArgumentParser()
if sys.argv[1:]:
p.add_argument('action', nargs = '*', choices = acts)
else:
p.add_argument('--action', default = ['dump', 'clear'])
args = p.parse_args()
print(args)
when run, yields these results:
% test.py
Namespace(action=['dump', 'clear'])
% test.py lock
Namespace(action=['lock'])
% test.py lock dump
Namespace(action=['lock', 'dump'])
You probably have other options to parse as well. In that case, you could use parse_known_args to parse the other options, and then handle the unknown arguments in a second pass:
import argparse
acts = ['clear', 'copy', 'dump', 'lock']
p = argparse.ArgumentParser()
p.add_argument('--foo')
args, unknown = p.parse_known_args()
if unknown:
p.add_argument('action', nargs = '*', choices = acts)
else:
p.add_argument('--action', default = ['dump', 'clear'])
p.parse_args(unknown, namespace = args)
print(args)
when run, yields these results:
% test.py
Namespace(action=['dump', 'clear'], foo=None)
% test.py --foo bar
Namespace(action=['dump', 'clear'], foo='bar')
% test.py lock dump
Namespace(action=['lock', 'dump'], foo=None)
% test.py lock dump --foo bar
Namespace(action=['lock', 'dump'], foo='bar')
The action was being appended because of the "action='append'" parameter you passed to argparse.
After removing this parameter, the arguments passed by a user would be displayed on their own, but the program would throw an error when no arguments were passed.
Adding a '--' prefix to the first parameter resolves this in the laziest way.
acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('--action', nargs='*', choices=acts, default=[['dump', 'clear']])
args = p.parse_args()
The downside to this approach is that the options passed by the user must now be preceded by '--action', like:
app.py --action clear dump copy
When I use subcommands with python argparse, I can get the selected arguments.
parser = argparse.ArgumentParser()
parser.add_argument('-g', '--global')
subparsers = parser.add_subparsers()
foo_parser = subparsers.add_parser('foo')
foo_parser.add_argument('-c', '--count')
bar_parser = subparsers.add_parser('bar')
args = parser.parse_args(['-g', 'xyz', 'foo', '--count', '42'])
# args => Namespace(global='xyz', count='42')
So args doesn't contain 'foo'. Simply writing sys.argv[1] doesn't work because of the possible global args. How can I get the subcommand itself?
The very bottom of the Python docs on argparse sub-commands explains how to do this:
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('-g', '--global')
>>> subparsers = parser.add_subparsers(dest="subparser_name") # this line changed
>>> foo_parser = subparsers.add_parser('foo')
>>> foo_parser.add_argument('-c', '--count')
>>> bar_parser = subparsers.add_parser('bar')
>>> args = parser.parse_args(['-g', 'xyz', 'foo', '--count', '42'])
>>> args
Namespace(count='42', global='xyz', subparser_name='foo')
You can also use the set_defaults() method referenced just above the example I found.
ArgumentParser.add_subparsers has dest formal argument described as:
dest - name of the attribute under which sub-command name will be stored; by default None and no value is stored
In the example below of a simple task function layout using subparsers, the selected subparser is in parser.parse_args().subparser.
import argparse
def task_a(alpha):
print('task a', alpha)
def task_b(beta, gamma):
print('task b', beta, gamma)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subparser')
parser_a = subparsers.add_parser('task_a')
parser_a.add_argument(
'-a', '--alpha', dest='alpha', help='Alpha description')
parser_b = subparsers.add_parser('task_b')
parser_b.add_argument(
'-b', '--beta', dest='beta', help='Beta description')
parser_b.add_argument(
'-g', '--gamma', dest='gamma', default=42, help='Gamma description')
kwargs = vars(parser.parse_args())
globals()[kwargs.pop('subparser')](**kwargs)
Just wanted to post this answer as this came in very handy in some of my recent work. This method makes use of decorators (although not used with conventional # syntax) and comes in especially handy if the recommended set_defaults is already being used with subparsers.
import argparse
from functools import wraps
import sys
def foo(subparser):
subparser.error('err')
def bar(subparser):
subparser.error('err')
def map_subparser_to_func(func, subparser):
#wraps(func)
def wrapper(*args, **kwargs):
return func(subparser, *args, **kwargs)
return wrapper
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
foo_parser = subparsers.add_parser('foo')
foo_parser.set_defaults(func = map_subparser_to_func(foo, foo_parser))
bar_parser = subparsers.add_parser('bar')
bar_parser.set_defaults(func = map_subparser_to_func(bar, bar_parser))
args = parser.parse_args(sys.argv[1:])
args.func()
The map_subparser_to_func function can be modified to set the subparser to some class attribute or global variable inside of the wrapper function instead of passing it directly and can also be reworked to a conventional decorator for the functions, although that would require adding another layer.
This way there is a direct reference to the object.
I'm writing a python script which I would like to be able to both call from the command line and import as a library function.
Ideally the command line options and the function should use the same set of default values.
What is the best way to allow me to reuse a single set of defaults in both places?
Here's the current code with duplicate defaults.
from optparse import OptionParser
def do_stuff(opt1="a", opt2="b", opt3="c"):
print opt1, opt2, opt3
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("--opt1", default="a")
parser.add_option("--opt2", default="b")
parser.add_option("--opt3", default="c")
#parser.set_defaults(opt1="a")
options, args = parser.parse_args()
do_stuff(*args, **vars(options))
I'd handle it by introspecting the function of interest to set options and defaults appropriately. For example:
import inspect
from optparse import OptionParser
import sys
def do_stuff(opt0, opt1="a", opt2="b", opt3="c"):
print opt0, opt1, opt2, opt3
if __name__ == "__main__":
parser = OptionParser()
args, varargs, varkw, defaults = inspect.getargspec(do_stuff)
if varargs or varkw:
sys.exit("Sorry, can't make opts from a function with *a and/or **k!")
lend = len(defaults)
nodef = args[:-lend]
for a in nodef:
parser.add_option("--%s" % a)
for a, d in zip(args[-lend:], defaults):
parser.add_option("--%s" % a, default=d)
options, args = parser.parse_args()
d = vars(options)
for n, v in zip(nodef, args):
d[n] = v
do_stuff(**d)
Here's the solution - it's trivial if you only need keyword arguments - just use locals.update. Following handles both, positional and key word args (key word args overrides positional).
from optparse import OptionParser
ARGS = {'opt1': 'a',
'opt2': 'b',
'opt3': 'c'}
def do_stuff(*args, **kwargs):
locals = ARGS
keys = ARGS.keys()
keys.sort()
if args:
for key,arg in zip(keys,args):
locals.update({key: arg})
if kwargs:
locals.update(kwargs)
print locals['opt1'], locals['opt2'], locals['opt3']
if __name__ == "__main__":
parser = OptionParser()
for key,default in ARGS.items():
parser.add_option('--%s' % key, default='%s' % default)
options, args = parser.parse_args()
do_stuff(*args, **vars(options))
do_stuff()
do_stuff('d','e','f')
do_stuff('d','e','f', opt3='b')
do_stuff(opt1='c', opt2='a', opt3='b')
Output:
a b c
a b c
d e f
d e b
c a b
The inspect solution by Alex is very powerful!
For lightweight programs, you could also simply use this:
def do_stuff(opt1="a", opt2="b", opt3="c"):
print opt1, opt2, opt3
if __name__ == "__main__":
from optparse import OptionParser
opts = do_stuff.func_defaults
parser = OptionParser()
parser.add_option("--opt1", default=opts[0], help="Option 1 (%default)")
parser.add_option("--opt2", default=opts[1], help="Option 2 (%default)")
parser.add_option("--opt3", default=opts[2], help="Option 3 (%default)")
options, args = parser.parse_args()
do_stuff(*args, **vars(options))