Search code examples
pythonargparsesubparsers

How to make optional subparser in python3?


I want to input args that not configured in argparse:

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

subparsers = parser.add_subparsers(help='sub-command help', dest="character", required=False)
subparsers.required = False

base_subparser = argparse.ArgumentParser(add_help=False)
# define common shared arguments
base_subparser.add_argument('--disable', choices=['false', 'true'])
base_subparser.add_argument('--foo', choices=['false', 'true'])
# create the parser for the "a" command
parser_a = subparsers.add_parser('a', help='a help', parents=[base_subparser])
parser_a.add_argument('--bar', choices='ABC', help='bar help')

# create the parser for the "b" command
parser_b = subparsers.add_parser('b', help='b help', parents=[base_subparser])
parser_b.add_argument('--baz', choices='XYZ', help='baz help')

argcomplete.autocomplete(parser)
args = parser.parse_known_args()
print(args)

I use the parse_known_args() which use a list to store the args not configured in argparse. However, when I use ./prog.py key = val, it shows argument character: invalid choice: 'key=val' (choose from 'a', 'b'). So I have to choose 'a' or 'b', how can I input the args not configured in argparse without choose one of the subparsers.


Solution

  • The error you see is the same as produced by a '?' positional with choices:

    In [25]: import argparse
    
    In [26]: parser = argparse.ArgumentParser() 
    In [27]: parser.add_argument('foo', nargs='?', choices=['a','b'])
    

    'foo' is optional:

    In [28]: parser.parse_known_args([])
    Out[28]: (Namespace(foo=None), [])
    
    In [29]: parser.parse_known_args(['a'])
    Out[29]: (Namespace(foo='a'), [])
    

    but any string is parsed as a possible 'foo' value:

    In [30]: parser.parse_known_args(['c'])
    usage: ipykernel_launcher.py [-h] [{a,b}]
    ipykernel_launcher.py: error: argument foo: invalid choice: 'c' (choose from 'a', 'b')
    

    providing a proper choice first, allows it to treat 'c' as an extra:

    In [31]: parser.parse_known_args(['a','c'])
    Out[31]: (Namespace(foo='a'), ['c'])
    

    Or if the string looks like a optional's flag:

    In [32]: parser.parse_known_args(['-c'])
    Out[32]: (Namespace(foo=None), ['-c'])
    

    Another possibility is to go ahead and name a subparser, possibly a dummy one, and provide the extra. The subparser will be the one that actually puts that string in the 'unknowns' category.

    In [40]: parser = argparse.ArgumentParser()
    In [41]: subp = parser.add_subparsers(dest='cmd')
    In [44]: p1 = subp.add_parser('a')
    
    In [45]: parser.parse_known_args(['a','c'])
    Out[45]: (Namespace(cmd='a'), ['c'])
    
    In [46]: parser.parse_known_args([])       # not-required is the default
    Out[46]: (Namespace(cmd=None), [])
    

    Keep in mind that the main parser does not "know" anything about subparsers, except is a positional. It's doing its normal allocating strings to actions. But once it calls a subparser, that parser has full control over the parsing. Once it's done it passes the namespace back to the main, but the main doesn't do any more parsing - it just wraps things up and exits (with results or error).


    Since subp is a positional with a special Action subclass, _SubParsersAction, I was thinking it might be possible to create a flagged argument with that class

    parser.add_argument('--foo', action=argparse._SubParsersAction)
    

    but there's more going on in add_subparsers, so it isn't a trivial addition. This is a purely speculative idea.