40

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?

Dharman
  • 30,962
  • 25
  • 85
  • 135
Omnifarious
  • 54,333
  • 19
  • 131
  • 194
  • No. The "no-" prefix is highly localized. It's not consistent in English ("un-" is also quite common.) – S.Lott Feb 10 '12 at 20:17
  • I think you have to write it yourself. I wish it had it built-in. – jterrace Feb 10 '12 at 20:18
  • @S.Lott: That's true. This program will not have a global audience though. :-) And if such a possibility were available, I'd expect the prefix to be able to be customized in some way. – Omnifarious Feb 10 '12 at 20:29
  • Global isn't the issue. Language is the issue. For the one language I know well, there are innumerable irregularities. That's why there's not "automatic" feature. – S.Lott Feb 10 '12 at 21:04
  • @jterrace: I wish the `_add_action` API were documented and that `Action` was more than a simple container of attributes. – Omnifarious Feb 11 '12 at 16:30

9 Answers9

23

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.

Omnifarious
  • 54,333
  • 19
  • 131
  • 194
14

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.

Dharman
  • 30,962
  • 25
  • 85
  • 135
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • 1
    Unfortunately it doesn't seem to make them mutually exclusive. – wim Jun 09 '22 at 22:42
  • 1
    @wim, I didn't pay much attention to this addition to `argparse`. (and with the move to `github` I'm more out of the loop.) It creates just one `Action` object, with two flags. To be mutually_exclusive, it would have had to create two Actions, one for the True side, and the other for the False side. – hpaulj Jun 09 '22 at 23:41
10

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)  
btel
  • 5,563
  • 6
  • 37
  • 47
10

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
Community
  • 1
  • 1
Zach Young
  • 10,137
  • 4
  • 32
  • 53
  • 1
    Could you set ``action='store_false'`` for ``--no-foo`` and set ``dest='foo'`` for both so that it shows up in a single variable? – jterrace Feb 10 '12 at 21:59
  • @jterrace Yes. Interesting suggestion. I've added an updated solution. – Zach Young Feb 10 '12 at 22:17
  • nice. you could wrap it in a function like in @s-lott's answer and would be really nice – jterrace Feb 10 '12 at 22:37
  • This is good, except the help is a little unnecessarily verbose. But at least the argument group thing keeps the related arguments stuck together. – Omnifarious Feb 10 '12 at 22:41
  • Also, it does not allow '--foo' and '--no-foo' to be specified at the same time and have the last specified take precedence. – Omnifarious Feb 10 '12 at 23:45
  • @Omnifarious True. Why do you want let the user enter both and have the interpreter (deterministically) choose which one to use? – Zach Young Feb 10 '12 at 23:52
  • 1
    This is intended for environments where the arguments may be passed through multiple layers, each one prepending its own defaults for the values. Anyway, I came up with a good solution after pulling apart the module myself. – Omnifarious Feb 11 '12 at 00:00
3

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 )
S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • Hmm... that's an interesting idea. Is there an idea of an 'argument object' that can parse things itself and maybe generate it's own help message? That would really do the trick. – Omnifarious Feb 10 '12 at 22:40
  • @Omnifarious: "generate it's own help message"? What can that possibly mean? What's wrong with adding more code as shown above? If you want even more magical things to occur, you might find it easier to simply read the source to `argparse` yourself and see how it works internally. – S.Lott Feb 10 '12 at 22:45
  • Well, that's one of the big advantages of argparse. It generates the help messages and stuff for you. `add_argument` could be thought of as a function that constructs some sort of argument object that represents all the features of an argument... how to parse it, which variable to stuff it in, default values, how to generate help, all that stuff, and puts it into a nice list inside the parser. But you're right, I should just delve into the internals myself and see if I can fiddle it the way I want. If it doesn't work the way I imagine, it should. It's lots more flexible. – Omnifarious Feb 10 '12 at 22:53
  • "add_argument could be thought of as a function"? It **is** a method which constructs an argument object. That's what it actually does. I don't get the comment. What are you saying? – S.Lott Feb 10 '12 at 22:54
  • So it does work the way I imagine. There is an 'argument object' that's constructed. Which means you can instantiate a different argument object that implements the methods differently. It could, for example, just add a dictionary with all the values given to the `add_argument` method to a list of those dictionaries. – Omnifarious Feb 10 '12 at 22:55
  • I'll repeat this, since you're ignoring it. "you might find it easier to simply read the source to argparse yourself and see how it works internally" – S.Lott Feb 10 '12 at 22:57
  • I fully understood exactly what you were asking me to do. So I did it and found my answer. I've added it as an answer to this question. I was hoping that someone knew without me having to pull the module apart myself, but it seems that nobody did. – Omnifarious Feb 10 '12 at 23:49
2

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

Omnifarious
  • 54,333
  • 19
  • 131
  • 194
ThorSummoner
  • 16,657
  • 15
  • 135
  • 147
2

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()
Dharman
  • 30,962
  • 25
  • 85
  • 135
Duarte
  • 83
  • 5
  • Does this work with Python pre-3.9 and with Python 2.7? Because for 3.9 and later, there is a better answer. – Omnifarious Jan 21 '23 at 01:29
  • I like this solution! Putting "--foo" and "--no-foo" into a mutually exclusive group might produce a better user experience. foo_group = parser.add_mutually_exclusive_group() foo_group.add_argument(... – Terry Spotts Jan 21 '23 at 02:32
2

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)
Community
  • 1
  • 1
Zach Young
  • 10,137
  • 4
  • 32
  • 53
  • This is very nice, but has a couple of disadvantages. First it does allow specifying both `--foo` and `--no-foo` on the command line and having the last one take precedence. Secondly, the help is unnecessarily verbose, even though the mutually exclusive group thing does put them together. I went my own way and detailed my approach in an answer to this question. – Omnifarious Feb 10 '12 at 23:51
0

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.

cfi
  • 10,915
  • 8
  • 57
  • 103