9

I started to learn Python, and now I'm learning the great benefits of argparse. Using argparse, I have created two groups of arguments: group_list and group_simulate. Each of the groups has its own arguments -- the user can specify only one argument in each group (achieved using parser.add_mutually_exclusive_group()).

And now my target is present a syntax error if the user specified arguments from both groupgs and not from only one of them -- I want to achieve this by using the capabilities of argparse and not by writing a method that asks if this and this was specified print syntax error.

import argparse
parser = argparse.ArgumentParser(
        description='this is the description',
        epilog="This is the epilog",
        argument_default=argparse.SUPPRESS  
        )

parser.add_argument('-v', '--verbose', help='verbose', action='store_true', default=False)

group_list = parser.add_mutually_exclusive_group()
group_list.add_argument('-m', help='list only modules', action='store_const', dest='list', const='modules', default='all')
group_list.add_argument('-p', help='list only ports', action='store_const', dest='list', const='ports', default='all')
group_list.add_argument('--list', help='list only module or ports', choices=['modules','ports'], metavar='<modules/ports>', default='all')

group_simulate = parser.add_mutually_exclusive_group()
group_simulate.add_argument('-M', help='simulate module down', nargs=1, metavar='module_name', dest='simulate')
group_simulate.add_argument('-P', help='simulate FC port down', nargs=1, metavar='fc_port_name', dest='simulate')
group_simulate.add_argument('-I', help='simulate iSCSI port down', nargs=1, metavar='iSCSI_port_name', dest='simulate')
group_simulate.add_argument('--simulate', help='simulate module or port down', nargs=1, dest='simulate')

args = parser.parse_args()

print args

So talking more specifically:

allowed:

test.py
output: Namespace(list='all', verbose=False)
test.py -m
output: Namespace(list='modules', verbose=False)
test.py -P asfasf
output: Namespace(P=['asfasf'], list='all', verbose=False)

not allowed:

test.py -m -P asfsaf
expected output: <the help message>
test.py -P asfasf -m
expected output: <the help message>

I have tried to achieve the wanted target with the option of add_subparsers from argparse but without any success.

So my question is how to achieve this situation?

Bart
  • 19,692
  • 7
  • 68
  • 77
Elia
  • 193
  • 1
  • 3
  • 11
  • what is then the difference to throwing all mutually exclusive args in one group? – tzelleke Feb 02 '13 at 10:49
  • 1
    @TheodrosZelleke Using multiple mutually-exclusive groups you can, for example, pass the `required` argument to only some of them. Having a single mutually exclusive group you can't do this. An other advantage is that the help message is a bit more informative if using more than a single mutually exclusive group. Also, *in the future* some more information about the mutually-exclusive group might be provided(e.g. title and description), and thus it would show more informative help messages. – Bakuriu Feb 02 '13 at 10:52
  • 1
    @Bakuriu -- but if one group is `required` to provide an arg, does't that immediately exclude all other groups? – tzelleke Feb 02 '13 at 10:57
  • I would agree the if an argument is defined as `required`, this will be in conflict with the `add_mutually_exclusive` - since you you will have to specify this argument and will not be able to specify other arguments, this is how I understand it. in the case above I didn't use any `required ` arguments. – Elia Feb 02 '13 at 11:57
  • I don't know why this answer was accepted. The OP states that you should be able to select exactly 1 of `-m, -p, --list` AND exactly 1 of `-M, -P, -I, --simulate`. This answer does not do that. – Bruno Bronosky May 19 '16 at 20:20
  • Furthermore, I don't know what the point of `--list` and `--simulate` alone could be. It seems that what they ACTUALLY want is either `--list` with one of `-m, -p` or `--simulate` with one of `-M, -P, -I`. This is why you should always start with writing a sensible usage statement first. Get it straight in your head and "on paper" first. Then you can code it, or communicate it to get help. If you can't write a usage statement for it, you either don't know POSIX, or you have a bad idea all together. – Bruno Bronosky May 19 '16 at 20:27
  • I don't know it's for learning purposes or not, but this problem specification is a lot more complicated than it needs to be. It's just seeking to set 2 attributes, `list` and `simulate`. `list` can be a choice of 3 things; `simulate` is any string, despite the fact that there are 4 possible flags. – hpaulj May 20 '16 at 00:22

3 Answers3

9

You can use a common mutually-exclusive-group as "root" of the two subgroups:

import argparse
parser = argparse.ArgumentParser(
        description='this is the description',
        epilog="This is the epilog",
        argument_default=argparse.SUPPRESS  
        )

parser.add_argument('-v', '--verbose', help='verbose', action='store_true', default=False)

root_group = parser.add_mutually_exclusive_group()

group_list = root_group.add_mutually_exclusive_group()
group_list.add_argument('-m', help='list only modules', action='store_const', dest='list', const='modules', default='all')
group_list.add_argument('-p', help='list only ports', action='store_const', dest='list', const='ports', default='all')
group_list.add_argument('--list', help='list only module or ports', choices=['modules','ports'], metavar='<modules/ports>', default='all')

group_simulate = root_group.add_mutually_exclusive_group()
group_simulate.add_argument('-M', help='simulate module down', nargs=1, metavar='module_name', dest='simulate')
group_simulate.add_argument('-P', help='simulate FC port down', nargs=1, metavar='fc_port_name', dest='simulate')
group_simulate.add_argument('-I', help='simulate iSCSI port down', nargs=1, metavar='iSCSI_port_name', dest='simulate')
group_simulate.add_argument('--simulate', help='simulate module or port down', nargs=1, dest='simulate')

args = parser.parse_args()

print args

Result:

$ python test.py -m -P asfafs
usage: test.py [-h] [-v] [[-m | -p | --list <modules/ports>]
                [-M module_name | -P fc_port_name | -I iSCSI_port_name | --simulate SIMULATE]
test.py: error: argument -P: not allowed with argument -m 

$ python test.py -m -p
usage: test.py [-h] [-v] [[-m | -p | --list <modules/ports>]
                [-M module_name | -P fc_port_name | -I iSCSI_port_name | --simulate SIMULATE]
test.py: error: argument -p: not allowed with argument -m
Bakuriu
  • 98,325
  • 22
  • 197
  • 231
  • 1
    This is exactly what I was looking for! Thank you very much Bakuriu – Elia Feb 02 '13 at 10:49
  • 4
    Nesting groups like this works, but does not do anything significant. When an action is added to `group_simulate` it gets added to `root_group` as well. It is also added to `parser` and its `optional arguments` group. The net effect is that all 7 actions are mutually exclusive. Also the `usage` code does not handle nested groups. Note the `[[` and the lack of `|` between the nested groups. – hpaulj Aug 01 '13 at 02:03
  • It turns out that while `group_simulate` has a `container` attribute that points to the `root_group`, `root_group` does not have a list of its nested groups. There is a `_mutually_exclusive_groups` attribute, but this is shared (same reference) among the parser and all groups (exclusive or not). http://bugs.python.org/issue10984 has a patch with a formatter that can display overlapping exclusive groups - but it show's each group independently, not nested. – hpaulj Aug 01 '13 at 02:22
1

Use Docopt! You shouldn't have to write a usage doc and then spend hours trying to figure out how to get argparse to create it for you. If you know POSIX you know how to interpret a usage doc because it is a standard. Docopt know how to interpret usage docs that same as you do. We don't need an abstraction layer.

I think the OP has failed to describe their own intentions based on what I read in their help text. I'm going to try and speculate what they are trying to do.


test.py

"""
usage: test.py [-h | --version]
       test.py [-v] (-m | -p)
       test.py [-v] --list (modules | ports)
       test.py [-v] (-M <module_name> | -P <fc_port_name> | -I <iSCSI_port_name>)

this is the description

optional arguments:
  -h, --help                show this help message and exit
  -v, --verbose             verbose
  -m                        list only modules (same as --list modules)
  -p                        list only ports   (same as --list ports)
  --list                    list only module or ports
  -M module_name            simulate module down
  -P fc_port_name           simulate FC port down
  -I iSCSI_port_name        simulate iSCSI port down

This is the epilog
"""

from pprint import pprint
from docopt import docopt

def cli():
    arguments = docopt(__doc__, version='Super Tool 0.2')
    pprint(arguments)

if __name__ == '__main__':
    cli()

While it would be possible to communicate all of the usage in a single line with complex nested conditionals, this is more legible. This is why docopt makes so much sense. For a CLI program you want to make sure you communicate to the user clearly. Why learn some obscure module syntax in the hope that you can convince it to create the communication to the user for you? Take the time to look at other POSIX tools with option rules similar to your needs and copy-pasta.

Bruno Bronosky
  • 66,273
  • 12
  • 162
  • 149
  • 2
    What usage doc would produce this desired behavior? I'm not too impressed with the `docopt` sales pitches so far. – hpaulj May 19 '16 at 01:22
  • @hpaulj Because the OP accepted that answer, which as you pointed out has unbalanced brackets in the usage message and has the net effect of puttin all 7 into one group... I'm not sure what they REALLY want. It's hard to say, but I'll take a stab at it. – Bruno Bronosky May 19 '16 at 20:11
  • Is this modeled on `argp` with multiple independent parsers (each usage line handled by a different parser)? Python already has a parser based on `getopt`, which is much simpler. – hpaulj May 19 '16 at 21:44
  • @hpaulj I'm not sure how the internals work. I only dive into that when something doesn't work. What is `argp`? What could possibly be easier than writing the exact user message you want shown and having it interpreted directly? – Bruno Bronosky May 25 '16 at 16:34
  • I was trying to follow up on your claim that knowing `POSIX` argument handling was enough to write a good `docopt` usage. A search on 'posix arguments' lead me to a GNU C Library, which has 2 parsers, `getopt` and `argp`, http://www.gnu.org/software/libc/manual/html_node/Argp.html – hpaulj May 25 '16 at 16:38
  • I don't see anything in the conventions page like http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html#Argument-Syntax about multiple usage lines or your `xor` `( | )` syntax. That looks like a `docopt` extension, not pure `POSIX`. – hpaulj May 25 '16 at 17:39
0

A simpler version of this parser is

parser=argparse.ArgumentParser(description="this is the description",
                epilog='this is the epilog')
parser.add_argument('-v', '--vebose', action='count')
g1=parser.add_mutually_exclusive_group()
g1.add_argument('--list', help='list module or ports (default=%(default)s)', choices=['modules','ports','all'], default='all')
g1.add_argument('--simulate', '-M','-P','-C', help='simulate [module down/ FS port down/ iSCSI port down]', dest='simulate', metavar='module/port')

With a help that looks like:

usage: stack14660876.py [-h] [-v]
                        [--list {modules,ports,all} | --simulate module/port]

this is the description

optional arguments:
  -h, --help            show this help message and exit
  -v, --vebose
  --list {modules,ports,all}
                        list module or ports (default=all)
  --simulate module/port, -M module/port, -P module/port, -C module/port
                        simulate [module down/ FS port down/ iSCSI port down]

this is the epilog

Beside verbose (here I substituted a count) the OP sets to attributes, list and simulate. list has a default value of all, and can be set to modules or ports. -m and -p are just short cuts, and don't really add to the definition. Shortcuts can be handy when defining lots of options, especially if the options can be used together (e.g. -vpm). But here only one option is allowed (besides -v).

simulate takes an unconstrained string. The M/P/C options are just documentation conveniences, and don't constrain the values or add meaning.

This is a nice exercise in pushing the boundaries of argparse (or any other parser), but I think it is too complicated to be useful. Despite all the groupings it comes down to allowing only one option.

==========================

Comments about docopt and POSIX argument handling prompted me to look at C argument libraries. getopt is the old standard. Python has a functional equivalent, https://docs.python.org/2/library/getopt.html

The other parser in the GNU library is argp.

http://www.gnu.org/software/libc/manual/html_node/Argp.html

I haven't seen, yet, a clear description of what it adds to the getopt syntax. But the following paragraph is interesting.

Argp also provides the ability to merge several independently defined option parsers into one, mediating conflicts between them and making the result appear seamless. A library can export an argp option parser that user programs might employ in conjunction with their own option parsers, resulting in less work for the user programs. Some programs may use only argument parsers exported by libraries, thereby achieving consistent and efficient option-parsing for abstractions implemented by the libraries.

It sounds a bit like the argparse subparser mechanism. That is, there's some sort of meta-parser that can delegate the action to one (or more) subparsers. But in argparse subparsers have to be explicitly named by the user.

A possible extension is to have the meta-parser look at the context. For example in the OP case, if it sees any of [--list, -p, -m] use the list subparser, if any of the simulate arguments, use the simulate subparser. That might give some more powerful grouping tools. And it might be possible to implement that sort of thing with the stock argparse. You can create and run several different parsers on the same sys.argv.

hpaulj
  • 221,503
  • 14
  • 230
  • 353