thomas_f
thomas_f

Reputation: 1993

argparse with multiple subparsers and positionals

Let's say I have a script, select_libs.py, which enables you to choose which libraries to include in some build process. When running this script I want to be able to specify the library name, branch and version. Here are some use cases.

So I imagine something as simple as this:

import argparse

branches = ["legacy", "stable", "development"]

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='lib configuration')

opencv_parser = subparsers.add_parser("opencv")
opencv_parser.add_argument("opencv_build", choices=branches)
opencv_parser.add_argument("opencv_version", type=str)

boost_parser = subparsers.add_parser("boost")
boost_parser.add_argument("boost_build", choices=branches)
boost_parser.add_argument("boost_version",type=str)

cmd_options = parser.parse_args()

The first two use cases both work, but the third:

select_libs.py opencv stable 3.4.1 boost development 1.67.0

produces this error:

error: unrecognized arguments: boost development 1.67.0

In my head, this should work since each parser have exactly two positional arguments, so it should know that boost is not an argument to opencv and trigger the boost parser accordingly. Clearly I'm wrong, but what have I missed and how can I make it work as intended (if possible)?

My current Python version is 3.5.2.

Upvotes: 3

Views: 3487

Answers (1)

Aran-Fey
Aran-Fey

Reputation: 43136

Argparse is not suited for this kind of thing. add_subparsers assumes that exactly 1 of the sub-commands will be used, so it throws an error if you try to set both opencv and boost. And other than that, argparse has no concept of arguments being associated with other arguments.

Option 1

If you don't mind using keyword options instead of positional options, you can use the solution from this answer:

argv = '-l opencv -b stable -v 3.4.1 -l boost -b development -v 1.67.0'.split()

parser = argparse.ArgumentParser()
parent = parser.add_argument('-l', '--lib', choices=['opencv', 'boost'], action=ParentAction)

parser.add_argument('-b', '--build', action=ChildAction, parent=parent)
parser.add_argument('-v', '--version', action=ChildAction, parent=parent)

args = parser.parse_args(argv)
print(args)
# output:
# Namespace(lib=OrderedDict([('opencv', Namespace(build='stable',
#                                                 version='3.4.1')),
#                            ('boost', Namespace(build='development',
#                                                version='1.67.0'))]))

Option 2

Use the nargs argument to make it a mix of positional and named arguments:

argv = '-l opencv stable 3.4.1 -l boost development 1.67.0'.split()

parser = argparse.ArgumentParser()
parent = parser.add_argument('-l', '--lib', nargs=3, action='append')

args = parser.parse_args(argv)
print(args)
# output:
# Namespace(lib=[['opencv', 'stable', '3.4.1'],
#                ['boost', 'development', '1.67.0']])

Option 3

Parse the arguments manually:

argv = 'opencv stable 3.4.1 boost development 1.67.0'.split()

args = {}
argv_itr = iter(argv)
for lib in argv_itr:
    args[lib] = {'build': next(argv_itr),
                 'version': next(argv_itr)}

print(args)
# output:
# {'opencv': {'build': 'stable',
#             'version': '3.4.1'},
#  'boost': {'build': 'development',
#            'version': '1.67.0'}}

Upvotes: 4

Related Questions