Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python argparse: Complex Argument Parsing Scenario

I would like to implement the following command line arguments parsing scenario:

I have 4 arguments: -g, -wid, -w1, and -w2.

-w1 and -w2 always appear together

-wid and (-w1 -w2) are mutually exclusive, but one or the other is required

-g is optional; if it is not specified only (-w1 -w2) can appear, but not -wid

Is there an elegant way to implement this using argparse, but without sub-commands (I am already in a sub-command)?

I was thinking of custom actions, but then in the body of the action I'd need to know when it is called for the last time (i.e. when the last argument is being parsed), which I do not know how to do since the number and the order of the arguments may vary.

Some more explanation just in case: the tool I am writing creates a gadget using a widget and a gadget parameter -g. The widget is either an existing widget - and then it is referred to by its id -wid, or a new widget that is created using parameters -w1 and -w2. If -g is not specified then the tool will just create and store a new widget using (-w1 -w2) without creating the gadget.

Thank you in advance.

like image 481
malenkiy_scot Avatar asked Nov 01 '22 02:11

malenkiy_scot


1 Answers

If you choose your defaults correctly, you can easily test for logical combinations of the argument values in the 'namespace' after 'parse_args'. You can also issue a parser error at that time. There is a bug report asking for mutually inclusive groups. There I suggest a mechanism for adding generalized combination testing. But it still requires your own logical tests.

http://bugs.python.org/issue11588 Add "necessarily inclusive" groups to argparse

The key to the solution I propose in that issue is making a seen_non_default_actions variable available to the programmer. This is a list (set actually) of actions which have been seen (taking into account that optional-positionals are always 'seen'). I would like to see more discussion of how implement a mixture of inclusive and exclusive groupings.

You specify:

I have 4 arguments: -g, -wid, -w1, and -w2. -w1 and -w2 always appear together -wid and (-w1 -w2) are mutually exclusive, but one or the other is required -g is optional; if it is not specified only (-w1 -w2) can appear, but not -wid

Which I would attempt to summarize as:

complex_group('g', required_next_exclusive_group('wid', inclusive_group('w1','w2)))

w1,w2 could be replaced with a '--w1w2',nargs=2. A simple 'mutually_inclusive_group' would work here. But argparse cannot handle nested groups.

wid and w1w2 could be put in a required mutually_exclusive_group.

-g requires a test like if args.g is None and args.wid is None: error()


Here's a script which behaves as you require (I think), using my latest patch in Issue11588. act_w1 etc are the actual action objects, which may appear in the seen_actions list. The test functions are registered with the subparser, and performed near the end of its parse_know_args().

parser = argparse.ArgumentParser(usage='custom usage')
sp = parser.add_subparsers(dest='cmd')
sp.required = True
spp = sp.add_parser('cmd1')
act_g = spp.add_argument('-g')
act_wid = spp.add_argument('--wid')
act_w1 = spp.add_argument('--w1')
act_w2 = spp.add_argument('--w2')

@spp.crosstest # decorator to register this function with spp
def test1(spp, seen_actions, *args):
    # seen_actions - list of actions that were seen by parser
    if 1==len({act_w1, act_w2}.intersection(seen_actions)):
        # error if only one of these was seen
        parser.error('-w1 and -w2 always appear together')
@spp.crosstest
def test2(spp, seen_actions, *args):
    # required mutually exclusive wid and (w1,w2 group)
    if act_wid in seen_actions:
        if act_w1 in seen_actions or act_w2 in seen_actions:
            parser.error('-wid and (-w1 -w2) are mutually exclusive')
    elif act_w1 not in seen_actions:
        parser.error('wid or (w1 and w2) required')
@spp.crosstest
def test3(spp, seen_actions, *args):
    # is this the simplest logical way of expressing this?
    if act_g not in seen_actions and act_wid in seen_actions:
        parser.error('not g, so not wid')
args = parser.parse_args()

In this example I save and test for the presence of the action objects. Testing could also be done using the dest strings. I'm exploring ways of making this testing more intuitive and user friendly. An expanded set of decorators seems most promising.

like image 129
hpaulj Avatar answered Nov 15 '22 04:11

hpaulj