Python Argparse Mutual Exclusive Group

Python argparse mutual exclusive group

add_mutually_exclusive_group doesn't make an entire group mutually exclusive. It makes options within the group mutually exclusive.

What you're looking for is subcommands. Instead of prog [ -a xxxx | [-b yyy -c zzz]], you'd have:

prog 
command 1
-a: ...
command 2
-b: ...
-c: ...

To invoke with the first set of arguments:

prog command_1 -a xxxx

To invoke with the second set of arguments:

prog command_2 -b yyyy -c zzzz

You can also set the sub command arguments as positional.

prog command_1 xxxx

Kind of like git or svn:

git commit -am
git merge develop

Working Example

# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='help for foo arg.')
subparsers = parser.add_subparsers(help='help for subcommand', dest="subcommand")

# create the parser for the "command_1" command
parser_a = subparsers.add_parser('command_1', help='command_1 help')
parser_a.add_argument('a', type=str, help='help for bar, positional')

# create the parser for the "command_2" command
parser_b = subparsers.add_parser('command_2', help='help for command_2')
parser_b.add_argument('-b', type=str, help='help for b')
parser_b.add_argument('-c', type=str, action='store', default='', help='test')

Test it

>>> parser.print_help()
usage: PROG [-h] [--foo] {command_1,command_2} ...

positional arguments:
{command_1,command_2}
help for subcommand
command_1 command_1 help
command_2 help for command_2

optional arguments:
-h, --help show this help message and exit
--foo help for foo arg.
>>>

>>> parser.parse_args(['command_1', 'working'])
Namespace(subcommand='command_1', a='working', foo=False)
>>> parser.parse_args(['command_1', 'wellness', '-b x'])
usage: PROG [-h] [--foo] {command_1,command_2} ...
PROG: error: unrecognized arguments: -b x

Good luck.

Python Argparse: Mutually exclusive group where one or the other are required

The add_mutually_exclusive_group() function according to docs has required option that does exactly what you want:

Create a mutually exclusive group. argparse will make sure that only
one of the arguments in the mutually exclusive group was present on
the command line:

argparse and mutually exclusive groups, each with their own required setting

As suggested in the comments, the way to go, if you wish to have mutually exclusive test and run logics, would be to use subparsers. The following is an illustration of the idea:

#!/usr/bin/env python3
"""
Script to test or run commands on given servers.
./the_script.py test # To test all servers
./the_script.py run --id 127.0.0.1 --command "echo hello world"
"""
from argparse import ArgumentParser, RawDescriptionHelpFormatter as RDHF

def test_servers(servers):
"""
Given a list of servers, let's test them!
"""
for server in servers:
print('Just tested server {s}'.format(s=server))

def do_actual_work(server_id, command):
"""
Given a server ID and a command, let's run the command on that server!
"""
print('Connected to server {s}'.format(s=server_id))
print('Ran command {c} successfully'.format(c=command))

if __name__ == '__main__':
parser = ArgumentParser(description=__doc__, formatter_class=RDHF)
subs = parser.add_subparsers()
subs.required = True
subs.dest = 'run or test'
test_parser = subs.add_parser('test', help='Test all servers')
test_parser.set_defaults(func=test_servers)
run_parser = subs.add_parser('run', help='Run a command on the given server')
run_parser.add_argument('-i', '--id',
help='The ID of the server to connect to and run commands',
required=True)
run_parser.add_argument('-c', '--command',
help='The command to run',
required=True)
run_parser.set_defaults(func=do_actual_work)
args = parser.parse_args()

if args.func.__name__ == 'test_servers':
all_servers = ['127.0.0.1', '127.0.0.2']
test_servers(all_servers)
else:
do_actual_work(args.id, args.command)

The script sets up both mutually exclusive and required subparsers test and run. For the test subparser, nothing else is required. However, for the run subparser, both --id and --command would be required. Each of these subparsers is associated with its designated target function. For simplicity I had the test_parser tied to test_servers; while run_parser is associated with do_actual_work.

Further, you should be able to call the script as follows to run all tests:

./the_script.py test

To run a specific command on a specific server, you call the script as follows:

./the_script.py run --id 127 --command "echo hello world"

I hope this proves useful.

How to make python argparse mutually exclusive group arguments without prefix?

For all the abilities and options in argparse I don't think you'll ever get a "canned" usage string that looks like what you want.

That said, have you looked at sub-parsers since your original post?

Here's a barebones implementation:

import argparse

parser = argparse.ArgumentParser(prog='mydaemon')
sp = parser.add_subparsers()
sp_start = sp.add_parser('start', help='Starts %(prog)s daemon')
sp_stop = sp.add_parser('stop', help='Stops %(prog)s daemon')
sp_restart = sp.add_parser('restart', help='Restarts %(prog)s daemon')

parser.parse_args()

Running this with the -h option yields:

usage: mydaemon [-h] {start,stop,restart} ...

positional arguments:
{start,stop,restart}
start Starts mydaemon daemon
stop Stops mydaemon daemon
restart Restarts mydaemon daemon

One of the benefits of this approach is being able to use set_defaults for each sub-parser to hook up a function directly to the argument. I've also added a "graceful" option for stop and restart:

import argparse

def my_stop(args):
if args.gracefully:
print "Let's try to stop..."
else:
print 'Stop, now!'

parser = argparse.ArgumentParser(prog='mydaemon')

graceful = argparse.ArgumentParser(add_help=False)
graceful.add_argument('-g', '--gracefully', action='store_true', help='tries to terminate the process gracefully')
sp = parser.add_subparsers()
sp_start = sp.add_parser('start', help='Starts %(prog)s daemon')
sp_stop = sp.add_parser('stop', parents=[graceful],
description='Stops the daemon if it is currently running.',
help='Stops %(prog)s daemon')
sp_restart = sp.add_parser('restart', parents=[graceful], help='Restarts %(prog)s daemon')

# Hook subparsers up to functions
sp_stop.set_defaults(func=my_stop)

# Uncomment when my_start() and
# my_restart() are implemented
#
# sp_start.set_defaults(func=my_start)
# sp_restart.set_defaults(func=my_restart)

args = parser.parse_args()
args.func(args)

Showing the "help" message for stop:

$ python mydaemon.py stop -h
usage: mydaemon stop [-h] [-g]

Stops the daemon if it is currently running.

optional arguments:
-h, --help show this help message and exit
-g, --gracefully tries to terminate the process gracefully

Stopping "gracefully":

$ python mydaemon.py stop -g
Let's try to stop...

argparse mutually exclusive group title and description in help message

Why? Because that's how it's coded!

Mutually exclusive groups are a subclass of ArgumentGroups, but the interface is different. Purpose is also quite different. An argument group controls the display of the help lines. It does nothing to parsing. A mutually exclusive group checks arguments during parsing, and is used when formatting the usage line. But it has no effect on the help lines.

But it is possible to embed a mutually exclusive group in an argument group (but not the other way around). That should produce what you want.

In [2]: parser = argparse.ArgumentParser()
In [3]: group = parser.add_argument_group(
...: 'foo options', 'various (mutually exclusive) ways to do foo')
In [4]: mxg = group.add_mutually_exclusive_group()
In [5]: mxg.add_argument('--option_a', action='store_true', help='option a');
In [6]: mxg.add_argument('--option_b', action='store_true', help='option b');

In [7]: parser.print_help()
usage: ipython3 [-h] [--option_a | --option_b]

optional arguments:
-h, --help show this help message and exit

foo options:
various (mutually exclusive) ways to do foo

--option_a option a
--option_b option b

There are more details in the code itself, and in a bug/issue or two, but this should get you going.

Python argparse AssertionError when using mutually exclusive group

This is a known issue when suppressing args. It is only reached when the usage line is long enough that it needs to be wrapped. See 22363 and 17890

You can avoid this by moving the mutually exclusive group to the end of the arguments:

import argparse

parser = argparse.ArgumentParser('A long string that goes on and on and on'
'and on and on and on and on and on and on '
'and on and on and on and on and on and on '
'and on and on and on and on and on and on ')
parser.add_argument('-t', help='c')
me_group = parser.add_mutually_exclusive_group()
me_group.add_argument('-f', help=argparse.SUPPRESS)
me_group.add_argument('-o', help=argparse.SUPPRESS)
parser.parse_args()

results in:

python test.py -h
usage: A long string that goes on and on and onand on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on
[-h] [-t T]

optional arguments:
-h, --help show this help message and exit
-t T c


Related Topics



Leave a reply



Submit