15

When using argparse, some subcommands need the same options and I'm using parents to avoid repeatedly defining them in every sub-command.

script filename: testarg.py

import argparse                                                                  

parser = argparse.ArgumentParser(add_help=False)                                 
parser.add_argument('-H', '--host', default='192.168.122.1')                     
parser.add_argument('-P', '--port', default='12345')                             
subparsers = parser.add_subparsers()                                             

# subcommand a                                                                   
parser_a = subparsers.add_parser('a', parents=[parser])                          
parser_a.add_argument('-D', '--daemon', action='store_true')                     

parser_a.add_argument('-L', '--log', default='/tmp/test.log')                    

# subcommand b                                                                   
parser_b = subparsers.add_parser('b', parents=[parser])                          
parser_b.add_argument('-D', '--daemon', action='store_true')                     

# subcommand c                                                                   
parser_c = subparsers.add_parser('c', parents=[parser])                          
args = parser.parse_args()                                                       

print args   

But when I run command:

>>>./testarg.py a
usage: testarg.py a [-h] [-H HOST] [-P PORT] [-D] [-L LOG] {a,b,c} ...
testarg.py a: error: too few arguments

expecting output:

>>>./testarg.py a
Namespace(daemon=False, host='192.168.122.1', log='/tmp/test.log', port='12345')

>>>./testarg.py b -H 127.0.0.1 -P 11111
Namespace(daemon=False, host='127.0.0.1', port='11111')

>>>./testarg.py c
Namespace(host='192.168.122.1', port='12345')

also, 

>>>./testarg.py c -H 127.0.0.1 -P 12222
Namespace(host='127.0.0.1', port='12222')

What am I missing?

Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
Fujiao Liu
  • 2,195
  • 2
  • 24
  • 28

2 Answers2

40

Make a separate parent parser and pass it to subparsers

import argparse                                                                  

parent_parser = argparse.ArgumentParser(add_help=False)                                 
parent_parser.add_argument('-H', '--host', default='192.168.122.1')                     
parent_parser.add_argument('-P', '--port', default='12345')                             

parser = argparse.ArgumentParser(add_help=False) 
subparsers = parser.add_subparsers()                                             

# subcommand a                                                                   
parser_a = subparsers.add_parser('a', parents = [parent_parser])                          
parser_a.add_argument('-D', '--daemon', action='store_true')                     

parser_a.add_argument('-L', '--log', default='/tmp/test.log')                    

# subcommand b                                                                   
parser_b = subparsers.add_parser('b', parents = [parent_parser])                          
parser_b.add_argument('-D', '--daemon', action='store_true')                     

# subcommand c                                                                   
parser_c = subparsers.add_parser('c', parents = [parent_parser])                          
args = parser.parse_args()                                                       

print args   

This gives desired result

$ python arg.py a
Namespace(daemon=False, host='192.168.122.1', log='/tmp/test.log', port='12345')
$ python arg.py b -H 127.0.0.1 -P 11111
Namespace(daemon=False, host='127.0.0.1', port='11111')
$ python arg.py c
Namespace(host='192.168.122.1', port='12345')
Konstantin
  • 24,271
  • 5
  • 48
  • 65
  • thanks, it works. I read official argparse doc but did not figure out the method, how do you know this? any doc? – Fujiao Liu Nov 11 '15 at 08:20
  • The docs have a `parents` section, and a `subparsers` section, which you've read. You are putting those together in an unexpected way. It's hard to anticipate all applications. – hpaulj Nov 11 '15 at 08:46
  • The `add_help` here `parser = argparse.ArgumentParser(add_help=False)` is not needed. – hyankov Jan 12 '20 at 17:37
  • 2
    I needed `add_help=False` for the `parent_parser`, but not for `parser`. Without this on the parent parser it throws an error. Setting `add_help=False` on `parent_parser` does not prevent the parent parser options from being listed in --help for `parser`. – Alcamtar Mar 12 '20 at 18:23
  • This is a nice solution! BEWARE however: a bug currently exists in CPython that may silently skip options provided before subcommands: https://github.com/python/cpython/pull/30146 – Lucas Cimon Dec 16 '21 at 18:33
9

When you use parser itself as a parents of the subparsers, you recursively add subparsers to each subparser. The add_subparsers command actually defines a positional argument, one that gets choices, {'a','b','c'}. It ends up expecting prog.py a a a ..., each subparser expects another subparser command etc.

I've never seen anyone try this kind of definition, and it took a bit of thinking to realize what was happening.

@Alik's approach is a correct one. Define the parent parser separately, and don't use it directly. It is just a source for those -H and -P Actions that you want added to each subparser. That's all you want to add to the subparsers.

Another approach is to simply define -H and -P in the main parser.

parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', default='192.168.122.1')
parser.add_argument('-P', '--port', default='12345')
subparsers = parser.add_subparsers()

# subcommand a
parser_a = subparsers.add_parser('a')
parser_a.add_argument('-D', '--daemon', action='store_true')
....

It will function in the same way, except that -H and -P will have to be specified before the subparser command.

0015:~/mypy$ python stack33645859.py -H 127.0.0.1 -P 1111 b
Namespace(daemon=False, host='127.0.0.1', port='1111')

They still appear in the namespace in the same way, it's just that order in the commandline is different. help will also be different.

A third option is to add the common arguments programmatically, with a loop or function. A crude example is:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
splist = []
for cmd in ['a','b','c']:
    p = subparsers.add_parser(cmd)
    p.add_argument('-H', '--host', default='192.168.122.1')
    p.add_argument('-P', '--port', default='12345')
    splist.append(p)
splist[0].add_argument('-D', '--daemon', action='store_true')

Functionally it will be similar to @Alik's approach, with a subtle difference. With the parent, only one pair of H and P Action objects is created. References are added to each subparser.

With mine, each subparser gets its own H and P Action object. Each subparser could define different defaults for those arguments. I remember this being an issue in one other SO question.

Coding work is similar in all cases.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Thanks for your great explanation. many thanks. Actually, I tried both methods you suggested. I ask the questions because I think the -P and -H are a part of every sub-commands but not the root parser, the -H and -P have to specified before sub-command is not what i want. do you have any suggestion about how to do sub-commands with common options pythonic? – Fujiao Liu Nov 11 '15 at 09:00
  • 1
    My last suggestion looks quite pythonic to me. – hpaulj Nov 11 '15 at 09:07