Get selected subcommand with argparse - python

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.

Related

argparse action or type for comma-separated list

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.

Best manner of dispatching Python's argparse subcommands

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.

Python command line argument assign to a variable

I have to either store the command line argument in a variable or assign a default value to it.
What i am trying is the below
import sys
Var=sys.argv[1] or "somevalue"
I am getting the error out of index if i don't specify any argument. How to solve this?
Var=sys.argv[1] if len(sys.argv) > 1 else "somevalue"
The builtin argparse module is intended for exactly these sorts of tasks:
import argparse
# Set up argument parser
ap = argparse.ArgumentParser()
# Single positional argument, nargs makes it optional
ap.add_argument("thingy", nargs='?', default="blah")
# Do parsing
a = ap.parse_args()
# Use argument
print a.thingy
Or, if you are stuck with Python 2.6 or earlier, and don't wish to add a requirement on the backported argparse module, you can do similar things manually like so:
import optparse
opter = optparse.OptionParser()
# opter.add_option("-v", "--verbose") etc
opts, args = opter.parse_args()
if len(args) == 0:
var = "somevalue"
elif len(args) == 1:
var = args[0]
else:
opter.error("Only one argument expected, got %d" % len(args))
print var
Good question.
I think the best solution would be to do
try:
var = sys.argv[1]
except IndexError:
var = "somevalue"
Try the following with a command-line-processing template:
def process_command_line(argv):
...
# add your option here
parser.add_option('--var',
default="somevalue",
help="your help text")
def main(argv=None):
settings, args = process_command_line(argv)
...
print settings, args # <- print your settings and args
Running ./your_script.py with the template below and your modifications above prints {'var': 'somevalue'} []
For an example of a command-line-processing template see an example in Code Like a Pythonista: Idiomatic Python (http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#command-line-processing):
#!/usr/bin/env python
"""
Module docstring.
"""
import sys
import optparse
def process_command_line(argv):
"""
Return a 2-tuple: (settings object, args list).
`argv` is a list of arguments, or `None` for ``sys.argv[1:]``.
"""
if argv is None:
argv = sys.argv[1:]
# initialize the parser object:
parser = optparse.OptionParser(
formatter=optparse.TitledHelpFormatter(width=78),
add_help_option=None)
# define options here:
parser.add_option( # customized description; put --help last
'-h', '--help', action='help',
help='Show this help message and exit.')
settings, args = parser.parse_args(argv)
# check number of arguments, verify values, etc.:
if args:
parser.error('program takes no command-line arguments; '
'"%s" ignored.' % (args,))
# further process settings & args if necessary
return settings, args
def main(argv=None):
settings, args = process_command_line(argv)
# application code here, like:
# run(settings, args)
return 0 # success
if __name__ == '__main__':
status = main()
sys.exit(status)

In Python argparse, is it possible to have paired --no-something/--something arguments?

I'm writing a program in which I would like to have arguments like this:
--[no-]foo Do (or do not) foo. Default is do.
Is there a way to get argparse to do this for me?
Well, none of the answers so far are quite satisfactory for a variety of reasons. So here is my own answer:
class ActionNoYes(argparse.Action):
def __init__(self, opt_name, dest, default=True, required=False, help=None):
super(ActionNoYes, self).__init__(['--' + opt_name, '--no-' + opt_name], dest, nargs=0, const=None, default=default, required=required, help=help)
def __call__(self, parser, namespace, values, option_string=None):
if option_string.starts_with('--no-'):
setattr(namespace, self.dest, False)
else:
setattr(namespace, self.dest, True)
And an example of use:
>>> p = argparse.ArgumentParser()
>>> p._add_action(ActionNoYes('foo', 'foo', help="Do (or do not) foo. (default do)"))
ActionNoYes(option_strings=['--foo', '--no-foo'], dest='foo', nargs=0, const=None, default=True, type=None, choices=None, help='Do (or do not) foo. (default do)', metavar=None)
>>> p.parse_args(['--no-foo', '--foo', '--no-foo'])
Namespace(foo=False)
>>> p.print_help()
usage: -c [-h] [--foo]
optional arguments:
-h, --help show this help message and exit
--foo, --no-foo Do (or do not) foo. (default do)
Unfortunately, the _add_action member function isn't documented, so this isn't 'official' in terms of being supported by the API. Also, Action is mainly a holder class. It has very little behavior on its own. It would be nice if it were possible to use it to customize the help message a bit more. For example saying --[no-]foo at the beginning. But that part is auto-generated by stuff outside the Action class.
v3.9 has added an action class that does this. From the docs (near the end of the action section)
The BooleanOptionalAction is available in argparse and adds support for boolean actions such as --foo and --no-foo:
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
>>> parser.parse_args(['--no-foo'])
Namespace(foo=False)
To explore #wim's comment about not being mutually_exclusive.
In [37]: >>> parser = argparse.ArgumentParser()
...: >>> parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
Out[37]: BooleanOptionalAction(option_strings=['--foo', '--no-foo'], dest='foo', nargs=0, const=None, default=None, type=None, choices=None, help=None, metavar=None)
The last line shows that the add_argument created a BooleanOptionalAction Action class.
With various inputs:
In [38]: parser.parse_args('--foo'.split())
Out[38]: Namespace(foo=True)
In [39]: parser.parse_args('--no-foo'.split())
Out[39]: Namespace(foo=False)
In [40]: parser.parse_args([])
Out[40]: Namespace(foo=None)
In [41]: parser.parse_args('--no-foo --foo'.split())
Out[41]: Namespace(foo=True)
So you can supply both flags, with the last taking effect, over writing anything produced by the previous. It's as though we defined two Actions, with the same dest, but different True/False const.
The key is that it defined two flag strings:
option_strings=['--foo', '--no-foo']
Part of the code for this new class:
class BooleanOptionalAction(Action):
def __init__(self,
option_strings,
dest,
...):
_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)
if option_string.startswith('--'):
option_string = '--no-' + option_string[2:]
_option_strings.append(option_string)
...
def __call__(self, parser, namespace, values, option_string=None):
if option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith('--no-'))
So the action __init__ defines the two flags, and the __call__ checks for the no part.
Does the add_mutually_exclusive_group() of argparse help?
parser = argparse.ArgumentParser()
exclusive_grp = parser.add_mutually_exclusive_group()
exclusive_grp.add_argument('--foo', action='store_true', help='do foo')
exclusive_grp.add_argument('--no-foo', action='store_true', help='do not do foo')
args = parser.parse_args()
print 'Starting program', 'with' if args.foo else 'without', 'foo'
print 'Starting program', 'with' if args.no_foo else 'without', 'no_foo'
Here's how it looks when run:
./so.py --help
usage: so.py [-h] [--foo | --no-foo]
optional arguments:
-h, --help show this help message and exit
--foo do foo
--no-foo do not do foo
./so.py
Starting program without foo
Starting program without no_foo
./so.py --no-foo --foo
usage: so.py [-h] [--foo | --no-foo]
so.py: error: argument --foo: not allowed with argument --no-foo
This is different from the following in the mutually exclusive group allows neither option in your program (and I'm assuming that you want options because of the -- syntax). This implies one or the other:
parser.add_argument('--foo=', choices=('y', 'n'), default='y',
help="Do foo? (default y)")
If these are required (non-optional), maybe using add_subparsers() is what you're looking for.
Update 1
Logically different, but maybe cleaner:
...
exclusive_grp.add_argument('--foo', action='store_true', dest='foo', help='do foo')
exclusive_grp.add_argument('--no-foo', action='store_false', dest='foo', help='do not do foo')
args = parser.parse_args()
print 'Starting program', 'with' if args.foo else 'without', 'foo'
And running it:
./so.py --foo
Starting program with foo
./so.py --no-foo
Starting program without foo
./so.py
Starting program without foo
I modified the solution of #Omnifarious to make it more like the standard actions:
import argparse
class ActionNoYes(argparse.Action):
def __init__(self, option_strings, dest, default=None, required=False, help=None):
if default is None:
raise ValueError('You must provide a default with Yes/No action')
if len(option_strings)!=1:
raise ValueError('Only single argument is allowed with YesNo action')
opt = option_strings[0]
if not opt.startswith('--'):
raise ValueError('Yes/No arguments must be prefixed with --')
opt = opt[2:]
opts = ['--' + opt, '--no-' + opt]
super(ActionNoYes, self).__init__(opts, dest, nargs=0, const=None,
default=default, required=required, help=help)
def __call__(self, parser, namespace, values, option_strings=None):
if option_strings.startswith('--no-'):
setattr(namespace, self.dest, False)
else:
setattr(namespace, self.dest, True)
You can add the Yes/No argument as you would add any standard option. You just need to pass ActionNoYes class in the action argument:
parser = argparse.ArgumentParser()
parser.add_argument('--foo', action=ActionNoYes, default=False)
Now when you call it:
>> args = parser.parse_args(['--foo'])
Namespace(foo=True)
>> args = parser.parse_args(['--no-foo'])
Namespace(foo=False)
>> args = parser.parse_args([])
Namespace(foo=False)
Write your own subclass.
class MyArgParse(argparse.ArgumentParser):
def magical_add_paired_arguments( self, *args, **kw ):
self.add_argument( *args, **kw )
self.add_argument( '--no'+args[0][2:], *args[1:], **kw )
For fun, here's a full implementation of S.Lott's answer:
import argparse
class MyArgParse(argparse.ArgumentParser):
def magical_add_paired_arguments( self, *args, **kw ):
exclusive_grp = self.add_mutually_exclusive_group()
exclusive_grp.add_argument( *args, **kw )
new_action = 'store_false' if kw['action'] == 'store_true' else 'store_true'
del kw['action']
new_help = 'not({})'.format(kw['help'])
del kw['help']
exclusive_grp.add_argument( '--no-'+args[0][2:], *args[1:],
action=new_action,
help=new_help, **kw )
parser = MyArgParse()
parser.magical_add_paired_arguments('--foo', action='store_true',
dest='foo', help='do foo')
args = parser.parse_args()
print 'Starting program', 'with' if args.foo else 'without', 'foo'
Here's the output:
./so.py --help
usage: so.py [-h] [--foo | --no-foo]
optional arguments:
-h, --help show this help message and exit
--foo do foo
--no-foo not(do foo)
Extending https://stackoverflow.com/a/9236426/1695680 's answer
import argparse
class ActionFlagWithNo(argparse.Action):
"""
Allows a 'no' prefix to disable store_true actions.
For example, --debug will have an additional --no-debug to explicitly disable it.
"""
def __init__(self, opt_name, dest=None, default=True, required=False, help=None):
super(ActionFlagWithNo, self).__init__(
[
'--' + opt_name[0],
'--no-' + opt_name[0],
] + opt_name[1:],
dest=(opt_name[0].replace('-', '_') if dest is None else dest),
nargs=0, const=None, default=default, required=required, help=help,
)
def __call__(self, parser, namespace, values, option_string=None):
if option_string.startswith('--no-'):
setattr(namespace, self.dest, False)
else:
setattr(namespace, self.dest, True)
class ActionFlagWithNoFormatter(argparse.HelpFormatter):
"""
This changes the --help output, what is originally this:
--file, --no-file, -f
Will be condensed like this:
--[no-]file, -f
"""
def _format_action_invocation(self, action):
if action.option_strings[1].startswith('--no-'):
return ', '.join(
[action.option_strings[0][:2] + '[no-]' + action.option_strings[0][2:]]
+ action.option_strings[2:]
)
return super(ActionFlagWithNoFormatter, self)._format_action_invocation(action)
def main(argp=None):
if argp is None:
argp = argparse.ArgumentParser(
formatter_class=ActionFlagWithNoFormatter,
)
argp._add_action(ActionFlagWithNo(['flaga', '-a'], default=False, help='...'))
argp._add_action(ActionFlagWithNo(['flabb', '-b'], default=False, help='...'))
argp = argp.parse_args()
This yields help output like so:
usage: myscript.py [-h] [--flaga] [--flabb]
optional arguments:
-h, --help show this help message and exit
--[no-]flaga, -a ...
--[no-]flabb, -b ...
Gist version here, pull requests welcome :)
https://gist.github.com/thorsummoner/9850b5d6cd5e6bb5a3b9b7792b69b0a5
Actualy I beleive there is a better answer to this...
parser = argparse.ArgumentParser()
parser.add_argument('--foo',
action='store_true',
default=True,
help="Sets foo arg to True. If not included defaults to tru")
parser.add_argument('--no-foo',
action="store_const",
const=False,
dest="foo",
help="negates --foo so if included then foo=False")
args = parser.parse_args()
Before seeing this question and the answers I wrote my own function to deal with this:
def on_off(item):
return 'on' if item else 'off'
def argparse_add_toggle(parser, name, **kwargs):
"""Given a basename of an argument, add --name and --no-name to parser
All standard ArgumentParser.add_argument parameters are supported
and fed through to add_argument as is with the following exceptions:
name is used to generate both an on and an off
switch: --<name>/--no-<name>
help by default is a simple 'Switch on/off <name>' text for the
two options. If you provide it make sure it fits english
language wise into the template
'Switch on <help>. Default: <default>'
If you need more control, use help_on and help_off
help_on Literally used to provide the help text for --<name>
help_off Literally used to provide the help text for --no-<name>
"""
default = bool(kwargs.pop('default', 0))
dest = kwargs.pop('dest', name)
help = kwargs.pop('help', name)
help_on = kwargs.pop('help_on', 'Switch on {}. Default: {}'.format(help, on_off(defaults)))
help_off = kwargs.pop('help_off', 'Switch off {}.'.format(help))
parser.add_argument('--' + name, action='store_true', dest=dest, default=default, help=help_on)
parser.add_argument('--no-' + name, action='store_false', dest=dest, help=help_off)
It can be used like this:
defaults = {
'dry_run' : 0,
}
parser = argparse.ArgumentParser(description="Fancy Script",
formatter_class=argparse.RawDescriptionHelpFormatter)
argparse_add_toggle(parser, 'dry_run', default=defaults['dry_run'],
help_on='No modifications on the filesystem. No jobs started.',
help_off='Normal operation')
parser.set_defaults(**defaults)
args = parser.parse_args()
Help output looks like this:
--dry_run No modifications on the filesystem. No jobs started.
--no-dry_run Normal operation
I prefer the approach of subclassing argparse.Action that the other answers are suggesting over my plain function because it makes the code using it cleaner, and easier to read.
This code has the advantage of having a standard default help, but also a help_on and help_off to reconfigure the rather stupid defaults.
Maybe someone can integrate.

python argparse - optional append argument with choices

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

Categories