Pergunta

I am using argparse to build a command with subcommands:

mycommand [GLOBAL FLAGS] subcommand [FLAGS]

I would like the global flags to work whether they are before or after the subcommand. Is there a clean way to do this that doesn't involve repeating code?

For example:

  parser = argparse.ArgumentParser()
  subparsers = parser.add_subparsers(dest='subparser_name')

  parser.add_argument('--disable')    # This flag...

  sp = subparsers.add_parser('compile')
  sp.add_argument('zones', nargs='*')
  sp.add_argument('--disable')        # Is repeated...

  sp = subparsers.add_parser('launch')
  sp.add_argument('zones', nargs='*')
  sp.add_argument('--disable')        # over and over...

I want to do this for many flags, so repeating myself over and over seems... unpythonic.

Foi útil?

Solução

This is a perfect use case for parents argparse feature:

Sometimes, several parsers share a common set of arguments. Rather than repeating the definitions of these arguments, a single parser with all the shared arguments and passed to parents= argument to ArgumentParser can be used.

Define a base parent ArgumentParser, add arguments that will be shared across subparsers. Then, add subparsers and set your base parser as a parent by providing parents keyword argument:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subparser_name')

base_subparser = argparse.ArgumentParser(add_help=False)
# define common shared arguments
base_subparser.add_argument('--disable')

sp = subparsers.add_parser('compile', parents=[base_subparser])
# define custom arguments
sp = subparsers.add_parser('launch', parents=[base_subparser])
# define custom arguments

Note that add_help=False here helps to avoid problems with conflicting help argument.

Also see: Python argparse - Add argument to multiple subparsers.

Outras dicas

I see two issues in your example:

1) The use of '--disable' in both the parser and subparsers. Nested ArgumentParser deals with that overlapping dest.

2) The repeated set of arguments in the subparsers. parents is certainly one way to simplify that. But you could easily write your own code:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subparser_name')

parser.add_argument('--disable', dest='main_disable')    # This flag...

for name in ['compile', 'launch']:
    sp = subparsers.add_parser(name)
    sp.add_argument('zones', nargs='*')
    sp.add_argument('--disable', dest=name+'_disable')        # Is repeated...

You are asking for argparse solution, but as you also call for a Pythonic solution, I will deliberately propose an alternative using package docopt:

get it:

$ pip install docopt

Write your code into mycommand file:

"""
Usage:
    mycommand compile [--disable] <zone>...
    mycommand launch  [--disable] <zone>...

Arguments:
    <zone>  zone name

Options:
  -h --help
  --disable  disable

"""
from docopt import docopt

if __name__ == "__main__":
    args = docopt(__doc__)
    print args

Then call it from command line:

basic help (no arguments):

$ python mycommand
Usage:
    mycommand compile [--disable] <zone>...
    mycommand launch  [--disable] <zone>...

Asking for a help:

$ python mycommand -h
Usage:
    mycommand compile [--disable] <zone>...
    mycommand launch  [--disable] <zone>...

Arguments:
    <zone>  zone name

Options:
  -h --help
  --disable  disable

Using compile subcommand:

$ python mycommand compile zoneAlfa zoneBeta
{'--disable': False,
 '<zone>': ['zoneAlfa', 'zoneBeta'],
 'compile': True,
 'launch': False}

add flag --disable:

$ python mycommand compile --disable zoneAlfa zoneBeta
{'--disable': True,
 '<zone>': ['zoneAlfa', 'zoneBeta'],
 'compile': True,
 'launch': False}

launch subcommand works too:

$ python mycommand launch --disable zoneAlfa zoneBeta
{'--disable': True,
 '<zone>': ['zoneAlfa', 'zoneBeta'],
 'compile': False,
 'launch': True}

My usage of argument parsers started with argparse too, and I hated the complexity of code, which was needed for doing something.

Later I turned into plac, which is very efficient way for turning a python function into very usable command for console.

Still, I was limited by set of rules to follow and understand, which were not very clear to me in many cases. With docopt I appreciate, that I can write the docstring first, following standard rules of POSIX, and then use it in my code. If you would care about validation of arguments, I will direct you to samples of this great package.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top