Argparse with Required Subparser

Argparse with required subparser

You need to give subparsers a dest.

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd')
subparsers.required = True

Now:

1909:~/mypy$ argdev/python3 stack23349349.py
usage: stack23349349.py [-h] {foo} ...
stack23349349.py: error: the following arguments are required: cmd

In order to issue this 'missing arguments' error message, the code needs to give that argument a name. For a positional argument (like subparses), that name is (by default) the 'dest'. There's a (minor) note about this in the SO answer you linked.

One of the few 'patches' to argparse in the last Python release changed how it tests for 'required' arguments. Unfortunately it introduced this bug regarding subparsers. This needs to be fixed in the next release (if not sooner).

update

If you want this optional subparsers behavior in Py2, it looks like the best option is to use a two stage parser as described in

How to Set a Default Subparser using Argparse Module with Python 2.7

There has been some recent activity in the related bug/issue

https://bugs.python.org/issue9253

update

A fix to this is in the works: https://github.com/python/cpython/pull/3027

argparse with required subcommands

There was a change in 3.3 in the error message for required arguments, and subcommands got lost in the dust.

http://bugs.python.org/issue9253#msg186387

There I suggest this work around, setting the required attribute after the subparsers is defined.

parser = ArgumentParser(prog='test')
subparsers = parser.add_subparsers()
subparsers.required = True
subparsers.dest = 'command'
subparser = subparsers.add_parser("foo", help="run foo")
parser.parse_args()

update

A related pull-request: https://github.com/python/cpython/pull/3027

how can I make subparsers be required?

Instead of printing help, I would prefer to raise an error:

if which_subcmd not in parsedargs:
msg = "Subcommands needed: subcmd1, subcmd2"
raise argparse.ArgumentTypeError(msg)

This way is more consistent with other argparse errors. But just a matter of taste. I don't see anything wrong on your approach as long as you exit from your script after that print_help() statement.

Is it possible to inherit required options in argparse subparsers?

Separate the main parser and parent parser functionality:

import argparse

parent_parser = argparse.ArgumentParser(description="The parent parser", add_help=False)

parent_parser.add_argument("-p", type=int, required=True,
help="set the parent required parameter")

main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers(title="actions", required=True, dest='command')
parser_command1 = subparsers.add_parser("command1", parents=[parent_parser],
description="The command1 parser",
help="Do command1")
parser_command1.add_argument("--option1", help="Run with option1")

parser_command2 = subparsers.add_parser("command2", parents=[parent_parser],
description="The command2 parser",
help="Do command2")

args = main_parser.parse_args()

The main parser and subparser both write to the same args namespace. If both define a 'p' argument, the subparser's value (or default) will overwrite any value set by the main. So it's possible to use the same 'dest' in both, but it is generally not a good idea. And each parser has to meet its own 'required' specifications. There's no 'sharing'.

The parents mechanism copies arguments from the parent to the child. So it saves typing or copy-n-paste, but little else. It's most useful when the parent is defined elsewhere and imported. Actually it copies by reference, which sometimes raises problems. In general it isn't a good idea to run both the parent and child. Use a 'dummy' parent.

Put subparser command at the beginning of arguments

Creating another parser helped me getting what I wanted.
The root parser should add all the optional arguments - and also have add_help=False, to avoid an help message conflict -, then another parser - parser2, with a lot of fantasy - will be created.

The second parser will have subparsers, and they all needs to specify as parents the root parser.

parser = argparse.ArgumentParser(add_help=False)

parser.add_argument() ...

parser2 = argparse.ArgumentParser()
subparsers = parser2.add_subparsers(title="Commands", dest="command")
subparsers.required = True

summary_parser = subparsers.add_parser("summary", parents=[parser])
summary_parser.add_argument("v", "--verbose", action="store_true")

# parse args
args = parser2.parse_args()

Now the output will be this:

usage: script.py [-h] {summary} ...

optional arguments:
-h, --help show this help message and exit

Commands:
{summary}
summary For each report print its summary, then exit
usage: script.py summary [-h] -x X -y Y -f FILES [FILES ...] [-v]

optional arguments:
-h, --help show this help message and exit
-x X
-y Y
-f FILES [FILES ...], --files FILES [FILES ...]
-v, --verbose

Optional subparsers?

If you go the subparsers route, you could define two parsers, 'ls' and 'extract'. 'ls' wouldn't have any arguments; 'extract' would take one positional, 'profile'.

subparsers are optional, (Argparse with required subparser), but 'profile', as currently defined, is required.

An alternative is to define two optionals, and omit the positional.

'-ls', True/False, if True to the list
'-e profile', if not None, do the extract.

Or you could leave the positional profile, but make it optional (nargs='?').

Another possibility is to look at the profile value after parsing. If it is a string like 'ls', then list instead of extract. This feels like the cleanest choice, however, the usage will not document this.


parser.add_argument('-l','--ls', action='store_true', help='list')
parser.add_argument('profile', nargs='?', help='The profile')

or

sp = parser.add_subparsers(dest='cmd')
sp.add_parser('ls')
sp1 = sp.add_parser('extract')
sp1.add_argument('profile', help='The profile')

A required mutually exclusive group

gp = parser.add_mutually_exclusive_group(required=True)
gp.add_argument('--ls', action='store_true', help='list')
gp.add_argument('profile', nargs='?', default='adefault', help='The profile')

produces:

usage: aws-env [-h] [-n] (--ls | profile)

How to Set a Default Subparser using Argparse Module with Python 2.7

Another idea is to use a 2 stage parsing. One handles 'globals', returning strings it can't handle. Then conditionally handle the extras with subparsers.

import argparse

def cmd1(args):
print('cmd1', args)
def cmd2(args):
print('cmd2', args)

parser1 = argparse.ArgumentParser()

parser1.add_argument("-i", "--info", help="Display more information")

parser2 = argparse.ArgumentParser()
subparsers = parser2.add_subparsers(dest='cmd')

parserCmd1 = subparsers.add_parser("cmd1", help="First Command")
parserCmd1.set_defaults(func=cmd1)

parserCmd2 = subparsers.add_parser("cmd2", help="Second Command")
parserCmd2.add_argument("-o", "--output", help="Redirect Output")
parserCmd2.set_defaults(func=cmd2)

args, extras = parser1.parse_known_args()
if len(extras)>0 and extras[0] in ['cmd1','cmd2']:
args = parser2.parse_args(extras, namespace=args)
args.func(args)
else:
print('doing system with', args, extras)

sample runs:

0901:~/mypy$ python stack46667843.py -i info
('doing system with', Namespace(info='info'), [])
0901:~/mypy$ python stack46667843.py -i info extras for sys
('doing system with', Namespace(info='info'), ['extras', 'for', 'sys'])
0901:~/mypy$ python stack46667843.py -i info cmd1
('cmd1', Namespace(cmd='cmd1', func=<function cmd1 at 0xb74b025c>, info='info'))
0901:~/mypy$ python stack46667843.py -i info cmd2 -o out
('cmd2', Namespace(cmd='cmd2', func=<function cmd2 at 0xb719ebc4>, info='info', output='out'))
0901:~/mypy$


Related Topics



Leave a reply



Submit