Python argparse creates incorrect usage string when using nested sub-parsers

你说的曾经没有我的故事 提交于 2021-01-29 14:57:11

问题


I would like to build a (complicated) command-line argument parser using argparse module facilities. The 'main' script can accept sub-commands, and some of the sub-commands also have their own sub-commands. Here is a MWE:

#!/usr/bin/env python3
import argparse

def arg_parser():
    parser = argparse.ArgumentParser()

    subparsers = parser.add_subparsers(required=True, dest="cmd")

    p = subparsers.add_parser("c1", help="Command 1")
    p.add_argument("foo", help="foo argument")
    p.add_argument("bar", help="bar argument")
    p.add_argument("-z", "--baz", help="baz argument")

    p = subparsers.add_parser("c2", help="Command 2")
    q = p.add_subparsers(required=True, dest="sub_command")

    r = q.add_parser("s1", help="Command 1 - Sub-command 1")
    r.add_argument("arg1", help="first argument")
    r.add_argument("-a", "--arg2", help="second argument")

    r = q.add_parser("s2", help="Command 1 - Sub-command 2")

    return parser


def main():
    args = arg_parser().parse_args()

    print(args)

if __name__ == "__main__":
    main()

So far everything works well, and the help messages generated by argparse look correct:

$ ./main.py
usage: main.py [-h] {c1,c2} ...

positional arguments:
  {c1,c2}
    c1        Command 1
    c2        Command 2

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

$ ./main.py c2 -h
usage: main.py c2 [-h] {s1,s2} ...

positional arguments:
  {s1,s2}
    s1        Command 1 - Sub-command 1
    s2        Command 1 - Sub-command 2

optional arguments:
  -h, --help  show this help message and exit
$ ./main.py c2 s1 -h
usage: main.py c2 s1 [-h] [-a ARG2] arg1

positional arguments:
  arg1                  first argument

optional arguments:
  -h, --help            show this help message and exit
  -a ARG2, --arg2 ARG2  second argument

Now I would like to split the long arg_parser routine into sub-functions that create the parser as follows:

def sc1_parser():
    r = argparse.ArgumentParser(add_help=False)

    r.add_argument("arg1", help="first argument")
    r.add_argument("-a", "--arg2", help="second argument")

    return r


def c1_parser():
    parser = argparse.ArgumentParser(add_help=None)

    parser.add_argument("foo", help="foo argument")
    parser.add_argument("bar", help="bar argument")
    parser.add_argument("-z", "--baz", help="baz argument")

    return parser


def c2_parser():

    r = argparse.ArgumentParser(add_help=False)

    q = r.add_subparsers(required=True, dest="sub_command")

    q.add_parser("s1", help="Command 1 - Sub-command 1",
                 parents=[sc1_parser()])
    q.add_parser("s2", help="Command 1 - Sub-command 2")

    return r

def top_parser():
    parser = argparse.ArgumentParser()

    subparsers = parser.add_subparsers(required=True, dest="cmd")

    subparsers.add_parser("c1", help="Command 1", parents=[c1_parser()])
    subparsers.add_parser("c2", help="Command 2", parents=[c2_parser()])

    return parser

(and obviously replace the call to 'arg_parser()' by 'top_parser()').

In this case, however, the help messages for sub-commands 's1' and 's2' are incorrect:

$ ./main.py -h
usage: main.py [-h] {c1,c2} ...

positional arguments:
  {c1,c2}
    c1        Command 1
    c2        Command 2

optional arguments:
  -h, --help  show this help message and exit
$ ./main.py c2 -h
usage: main.py c2 [-h] {s1,s2} ...

positional arguments:
  {s1,s2}
    s1        Command 1 - Sub-command 1
    s2        Command 1 - Sub-command 2

optional arguments:
  -h, --help  show this help message and exit
$ ./main.py c2 s1 -h
usage: main.py s1 [-h] [-a ARG2] arg1

positional arguments:
  arg1                  first argument

optional arguments:
  -h, --help            show this help message and exit
  -a ARG2, --arg2 ARG2  second argument
$ ./main.py c2 s2 -h
usage: main.py s2 [-h]

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

Any idea why this is happening?

My guess is that in the first version (arg_parse() routine) the sub-command parser (the parser for s1 and s2) is added to the a parser returned from add_parser method of a sub-parser. But In the second case they are added to a fresh 'ArgumentParser'.


回答1:


If I add to your first script (before the return):

print("progs: ")
print("main: ", parser.prog)
print("   p: ", p.prog)
print("   r: ", r.prog)

a run is:

1319:~/mypy$ python3 stack63361458_0.py c2 s2 -h
progs: 
main:  stack63361458_0.py
   p:  stack63361458_0.py c2
   r:  stack63361458_0.py c2 s2
usage: stack63361458_0.py c2 s2 [-h]

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

usage includes the prog. If you don't provide it, argparse derives it from sys.argv[0], and for subparsers, adds on some strings to show the subparser's name (and required main arguments). Code for that addition is in add_subparses and add_parser.

Adding similar prints to the top_parser case

1349:~/mypy$ python3 stack63361458_0.py c2 s2 -h
s2 prog:  stack63361458_0.py s2
top prog:  stack63361458_0.py
c2 prog:  stack63361458_0.py c2
usage: stack63361458_0.py s2 [-h]

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

c2 gets the expected prog, but s2.prog is created without "knowledge" that it will be called by the c2 subparser. Using parents bypasses that link.

But I could supply my own prog for the sub-sub-parser:

s2 = q.add_parser("s2", help="Command 1 - Sub-command 2", prog=f'MyProg {sys.argv[0]} c2 s2 ')

1358:~/mypy$ python3 stack63361458_0.py c2 s2 -h
s2 prog:  MyProg stack63361458_0.py c2 s2 
top prog:  stack63361458_0.py
c2 prog:  stack63361458_0.py c2
usage: MyProg stack63361458_0.py c2 s2  [-h]

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

We can also supply a custom usage.

Instead of the parents mechanism which creates s2 before c2, we could create c2 and then pass that to the function that creates its arguments:

def c2_parser(q):
    r = q.add_parser("s1", help="Command 1 - Sub-command 1")
    r.add_argument("arg1", help="first argument")
    r.add_argument("-a", "--arg2", help="second argument")

    r = q.add_parser("s2", help="Command 1 - Sub-command 2")

and in main:

    p = subparsers.add_parser("c2", help="Command 2")
    c2_parser(p)

parents are (somewhat) useful as a means of adding the same arguments to multiple subparsers, but aren't necessary when just refactoring larger builds.



来源:https://stackoverflow.com/questions/63361458/python-argparse-creates-incorrect-usage-string-when-using-nested-sub-parsers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!