Skip to content Skip to sidebar Skip to footer

Argparse On Demand Imports For Types, Choices Etc

I have quite a big program which has a CLI interaction based on argparse, with several sub parsers. The list of supported choices for the subparsers arguments are determined based

Solution 1:

To delay the fetching of choices, you could parse the command-line in two stages: In the first stage, you find only the subparser, and in the second stage, the subparser is used to parse the rest of the arguments:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('subparser', choices=['foo','bar'])

deffoo_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('fooval', choices='123')
    return parser

defbar_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('barval', choices='ABC')
    return parser

dispatch = {'foo':foo_parser, 'bar':bar_parser}
args, unknown = parser.parse_known_args()
args = dispatch[args.subparser]().parse_args(unknown)
print(args)

It could be used like this:

% script.py foo 2Namespace(fooval='2')

% script.py bar ANamespace(barval='A')

Note that the top-level help message will be less friendly, since it can only tell you about the subparser choices:

% script.py -h
usage: script.py [-h] {foo,bar}
...

To find information about the choices in each subparser, the user would have to select the subparser and pass the -h to it:

% script.py bar -- -h
usage: script.py [-h] {A,B,C}

All arguments after the -- are considered non-options (to script.py) and are thus parsed by the bar_parser.

Solution 2:

Here's a quick and dirty example of a 'lazy' choices. In this case choices are a range of integers. I think a case that requires expensive DB lookups could implemented in a similar fashion.

# argparse with lazy choicesclassLazyChoice(object):
    # large rangedef__init__(self, argmax):
        self.argmax=argmax
    def__contains__(self, item):
        # a 'lazy' test that does not enumerate all choicesreturn item<=self.argmax
    def__iter__(self):
        # iterable for display in error message# use is in:# tup = value, ', '.join(map(repr, action.choices))# metavar bypasses this when formatting help/usagereturniter(['integers less than %s'%self.argmax])

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--regular','-r',choices=['one','two'])
larg = parser.add_argument('--lazy','-l', choices=LazyChoice(10))
larg.type = intprint parser.parse_args()

Implementing the testing part (__contains__) is easy. The help/usage can be customized with help and metavar attributes. Customizing the error message is harder. http://bugs.python.org/issue16468 discusses alternatives when choices are not iterable. (also on long list choices: http://bugs.python.org/issue16418)

I've also shown how the type can be changed after the initial setup. That doesn't solve the problem of setting type based on subparser choice. But it isn't hard to write a custom type, one that does some sort of Db lookup. All a type function needs to do is take a string, return the correct converted value, and raise ValueError if there's a problem.

Solution 3:

I have solved the issue by creating a simple ArgumentParser subclass:

import argparse

classArgumentParser(argparse.ArgumentParser):
    def__init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.lazy_init = Nonedefparse_known_args(self, args=None, namespace=None):
        if self.lazy_init isnotNone:
            self.lazy_init()
            self.lazy_init = Nonereturnsuper().parse_known_args(args, namespace)

Then I can use it as following:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command', title='commands', parser_class=ArgumentParser)
subparsers.required = True

subparser = subparsers.add_parser(
    'do-something', help="do something",
    description="Do something great.",
)

deflazy_init():
    from my_database import data

    subparser.add_argument(
        '-o', '--option', choices=data.expensive_fetch(), action='save',
    )

subparser.lazy_init = lazy_init

This will really initialize a sub-parser only when parent parser tries to parse arguments for the sub-parser. So if you do program -h it will not initialize the sub-parser, but if you do program do-something -h it will.

Solution 4:

This is a script that tests the idea of delaying the creation of a subparser until it is actually needed. In theory it might save start up time, by only creating the subparser that's actually needed.

I use the nargs=argparse.PARSER to replicate the subparser behavior in the main parser. help behavior is similar.

# lazy subparsers test# lazy behaves much like a regular subparser case, but only creates one subparser# for N=5 time differences do not rise above the noiseimport argparse

defregular(N):
    parser = argparse.ArgumentParser()
    sp = parser.add_subparsers(dest='cmd')
    for i inrange(N):
        spp = sp.add_parser('cmd%s'%i)
        spp.set_defaults(func='cmd%s'%(10*i))
        spp.add_argument('-f','--foo')
        spp.add_argument('pos', nargs='*')
    return parser

deflazy(N):
    parser = argparse.ArgumentParser()
    sp = parser.add_argument('cmd', nargs=argparse.PARSER, choices=[])
    for i inrange(N):
        sp.choices.append('cmd%s'%i)
    return parser

defsubpar(cmd):
    cmd, argv = cmd[0], cmd[1:]
    parser = argparse.ArgumentParser(prog=cmd)
    parser.add_argument('-f','--foo')
    parser.add_argument('pos', nargs='*')
    parser.set_defaults(func=cmd)
    args = parser.parse_args(argv)
    return args

N = 5
mode = True#False
argv = 'cmd1 -f1 a b c'.split()
if mode:
    args = regular(N).parse_args(argv)
    print(args)
else:
    args = lazy(N).parse_args(argv)
    print(args)
    ifisinstance(args.cmd, list):
        sargs = subpar(args.cmd)
        print(sargs)

test runs with different values of mode (and N=5)

1004:~/mypy$ time python3 stack44315696.py 
Namespace(cmd='cmd1', foo='1', func='cmd10', pos=['a', 'b', 'c'])

real    0m0.052s
user    0m0.044s
sys 0m0.008s
1011:~/mypy$ time python3 stack44315696.py 
Namespace(cmd=['cmd1', '-f1', 'a', 'b', 'c'])
Namespace(foo='1', func='cmd1', pos=['a', 'b', 'c'])

real    0m0.051s
user    0m0.048s
sys 0m0.000s

N has to be much larger to start seeing a effect.

Post a Comment for "Argparse On Demand Imports For Types, Choices Etc"