argparse optional subparser (for --version)
Asked Answered
L

7

22

I have the following code (using Python 2.7):

# shared command line options, like --version or --verbose
parser_shared = argparse.ArgumentParser(add_help=False)
parser_shared.add_argument('--version', action='store_true')

# the main parser, inherits from `parser_shared`
parser = argparse.ArgumentParser(description='main', parents=[parser_shared])

# several subcommands, which can't inherit from the main parser, since
# it would expect subcommands ad infinitum
subparsers = parser.add_subparsers('db', parents=[parser_shared])

...

args = parser.parse_args()

Now I would like to be able to call this program e.g. with the --version appended to the normal program or some subcommand:

$ prog --version
0.1

$ prog db --version
0.1

Basically, I need to declare optional subparsers. I'm aware that this isn't really supported, but are there any workarounds or alternatives?

Edit: The error message I am getting:

$ prog db --version
# works fine

$ prog --version
usage: ....
prog: error: too few arguments
Lithia answered 15/12, 2011 at 14:27 Comment(0)
M
22

According to documentation, --version with action='version' (and not with action='store_true') prints automatically the version number:

parser.add_argument('--version', action='version', version='%(prog)s 2.0')
Marxism answered 15/12, 2011 at 14:29 Comment(1)
This does not work in 3.9Timeous
B
10

FWIW, I ran into this also, and ended up "solving" it by not using subparsers (I already had my own system for printing help, so didn't lose anything there).

Instead, I do this:

parser.add_argument("command", nargs="?",
                    help="name of command to execute")

args, subcommand_args = parser.parse_known_args()

...and then the subcommand creates its own parser (similar to a subparser) which operates only on subcommand_args.

Bobker answered 26/2, 2014 at 22:4 Comment(0)
L
7

This seems to implement the basic idea of an optional subparser. We parse the standard arguments that apply to all subcommands. Then, if anything is left, we invoke the parser on the rest. The primary arguments are a parent of the subcommand so the -h appears correctly. I plan to enter an interactive prompt if no subcommands are present.

import argparse

p1 = argparse.ArgumentParser( add_help = False )    
p1.add_argument( ‘–flag1′ )

p2 = argparse.ArgumentParser( parents = [ p1 ] )
s = p2.add_subparsers()
p = s.add_parser( ‘group’ )
p.set_defaults( group=True )

( init_ns, remaining ) = p1.parse_known_args( )

if remaining:
    p2.parse_args( args = remaining, namespace=init_ns )
else:
    print( ‘Enter interactive loop’ )

print( init_ns )
Leboff answered 4/4, 2015 at 1:15 Comment(0)
C
5

As discussed in http://bugs.python.org/issue9253 (argparse: optional subparsers), as of Python 3.3, subparsers are now optional. This was an unintended result of a change in how parse_args checked for required arguments.

I found a fudge that restores the previous (required subparsers) behavior, explicitly setting the required attribute of the subparsers action.

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

See that issue for more details. I expect that if and when this issue gets properly patched, subparsers will be required by default, with some sort of option to set its required attribute to False. But there is a big backlog of argparse patches.

Crescentic answered 27/2, 2014 at 6:4 Comment(0)
C
2

Yeah, I just checked svn, which is used as an object example in the add_subparsers() documentation, and it only supports '--version' on the main command:

python zacharyyoung$ svn log --version
Subcommand 'log' doesn't accept option '--version'
Type 'svn help log' for usage.

Still:

# create common parser
parent_parser = argparse.ArgumentParser('parent', add_help=False)
parent_parser.add_argument('--version', action='version', version='%(prog)s 2.0')

# create the top-level parser
parser = argparse.ArgumentParser(parents=[parent_parser])
subparsers = parser.add_subparsers()

# create the parser for the "foo" command
parser_foo = subparsers.add_parser('foo', parents=[parent_parser])

Which yields:

python zacharyyoung$ ./arg-test.py --version
arg-test.py 2.0
python zacharyyoung$ ./arg-test.py foo --version
arg-test.py foo 2.0
Carrier answered 15/12, 2011 at 17:15 Comment(0)
W
2

While we wait for this feature to be delivered, we can use code like this:

# Make sure that main is the default sub-parser
if '-h' not in sys.argv and '--help' not in sys.argv:
    if len(sys.argv) < 2:
        sys.argv.append('main')
    if sys.argv[1] not in ('main', 'test'):
        sys.argv = [sys.argv[0], 'main'] + sys.argv[1:]
Wartow answered 13/11, 2012 at 22:48 Comment(2)
Note that we wait for this basic feature since 2009.Defoliate
I've started using docopt instead of the builtin argument parser. It does support mixed usage with or without "actions", aka "verbs". docopt.orgWartow
E
0

Although @eumiro's answer address the --version option, it can only do so because that is a special case for optparse. To allow general invocations of:

 prog
 prog --verbose
 prog --verbose main
 prog --verbose db 

and have prog --version work the same as prog --verbose main (and prog main --verbose) you can add a method to Argumentparser and call that with the name of the default subparser, just before invoking parse_args():

import argparse
import sys

def set_default_subparser(self, name, args=None):
    """default subparser selection. Call after setup, just before parse_args()
    name: is the name of the subparser to call by default
    args: if set is the argument list handed to parse_args()

    , tested with 2.7, 3.2, 3.3, 3.4
    it works with 2.6 assuming argparse is installed
    """
    subparser_found = False
    for arg in sys.argv[1:]:
        if arg in ['-h', '--help']:  # global help if no subparser
            break
    else:
        for x in self._subparsers._actions:
            if not isinstance(x, argparse._SubParsersAction):
                continue
            for sp_name in x._name_parser_map.keys():
                if sp_name in sys.argv[1:]:
                    subparser_found = True
        if not subparser_found:
            # insert default in first position, this implies no
            # global options without a sub_parsers specified
            if args is None:
                sys.argv.insert(1, name)
            else:
                args.insert(0, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

def do_main(args):
    print 'main verbose', args.verbose

def do_db(args):
    print 'db verbose:', args.verbose

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--version', action='version', version='%(prog)s 2.0')
subparsers = parser.add_subparsers()
sp = subparsers.add_parser('main')
sp.set_defaults(func=do_main)
sp.add_argument('--verbose', action='store_true')
sp = subparsers.add_parser('db')
sp.set_defaults(func=do_db)

parser.set_default_subparser('main')
args = parser.parse_args()

if hasattr(args, 'func'):
    args.func(args)

The set_default_subparser() method is part of the ruamel.std.argparse package.

Eveland answered 15/10, 2014 at 9:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.