Reputation: 56048
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 in versions of Python earlier than 3.9 (and for versions after that as well)?
Upvotes: 44
Views: 9342
Reputation: 2460
I took bits of several answers here (and from other questions), which helped me on my way to the following solution.
Key features:
add_argument()
calls if I want to add more later.--foo
or --no-foo
is accessible as the single boolean args.foo
in script.True
by default and can be selectively turned off, unless you provide --<flag>
, in which case they all default to False
, and you have to selectively turn them on.--<flag>
/ --no-<flag>
#!/usr/bin/env python3
import argparse
import sys
def main():
print(f"Foo is {args.foo}")
print(f"Bar is {args.bar}")
print(f"Baz is {args.baz}")
if __name__ == "__main__":
feature_flags = {
"foo",
"bar",
"baz"
}
flag_default = True
for flag in feature_flags:
if any([f'--{flag}' in arg for arg in sys.argv]):
flag_default = False
parser = argparse.ArgumentParser()
for flag in feature_flags:
arg_group = parser.add_mutually_exclusive_group()
arg_group.add_argument(
f"--{flag}",
action="store_true",
default=flag_default,
dest=flag,
help=f"Turn {flag} on.",
)
arg_group.add_argument(
f"--no-{flag}",
action="store_false",
dest=flag,
help=f"Turn {flag} off.",
)
args = parser.parse_args()
main()
Results in:
$ ./demo.py
Foo is True
Bar is True
Baz is True
$ ./demo.py --foo
Foo is True
Bar is False
Baz is False
$ ./demo.py --no-baz
Foo is True
Bar is True
Baz is False
Upvotes: 0
Reputation: 56048
Well, none of the answers so far are quite satisfactory for a variety of reasons. So here is my own answer for versions of Python earlier than 3.9:
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.
Upvotes: 25
Reputation: 113
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()
Upvotes: 4
Reputation: 231385
v3.9 has added an action
class that does this. From the docs (near the end of the action
section)
The
BooleanOptionalAction
is available inargparse
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.
Upvotes: 19
Reputation: 18109
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
Upvotes: 2
Reputation: 5693
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)
Upvotes: 10
Reputation: 11290
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.
Upvotes: 0
Reputation: 11188
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)
Upvotes: 2
Reputation: 11188
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
Upvotes: 11
Reputation: 391854
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 )
Upvotes: 3