shikhanshu
shikhanshu

Reputation: 1548

Python argparse : How can I get Namespace objects for argument groups separately?

I have some command line arguments categorized in groups as follows:

cmdParser = argparse.ArgumentParser()
cmdParser.add_argument('mainArg')

groupOne = cmdParser.add_argument_group('group one')
groupOne.add_argument('-optA')
groupOne.add_argument('-optB')

groupTwo = cmdParser.add_argument_group('group two')
groupTwo.add_argument('-optC')
groupTwo.add_argument('-optD')

How can I parse the above, such that I end up with three different Namespace objects?

global_args - containing all the arguments not part of any group
groupOne_args - containing all the arguments in groupOne
groupTwo_args - containing all the arguments in groupTwo

Thank you!

Upvotes: 19

Views: 8673

Answers (4)

mathandy
mathandy

Reputation: 2010

Here's a simple method that calls the parse_known_args() method after each group is defined to get the names for those arguments separately.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('mainArg')
global_names = set(vars(parser.parse_known_args()[0]).keys())

group1 = parser.add_argument_group('group 1')
group1.add_argument('-optA')
group1.add_argument('-optB')
group1_names = set(vars(parser.parse_known_args()[0]).keys()) - global_names

group2 = parser.add_argument_group('group 2')
group2.add_argument('-optC')
group2.add_argument('-optD')
group2_names = set(vars(parser.parse_known_args()[0]).keys()) - global_names - group1_names

args = parser.parse_args()
global_args = argparse.Namespace(**dict((k, v) for k, v in vars(args).items() if k in global_names))
group1_args = argparse.Namespace(**dict((k, v) for k, v in vars(args).items() if k in group1_names))
group2_args = argparse.Namespace(**dict((k, v) for k, v in vars(args).items() if k in group2_names))

print(global_args)
print(group1_args)
print(group2_args)

E.g. python args.py hi -optA fooA -optB fooB -optC fooC -optD fooD will output:

Namespace(mainArg='hi')
Namespace(optA='fooA', optB='fooB')
Namespace(optC='fooC', optD='fooD')

Upvotes: 0

Rami Hassan
Rami Hassan

Reputation: 155

I was looking for a solution for this for a very long time,
And I think I finally got it.
So I will just put it here...

from argparse import ArgumentParser

def _parse_args():
    parser = ArgumentParser()
    parser.add_argument('-1', '--flag-1', action='store_true', default=False)
    parser.add_argument('-2', '--flag-2', action='store_true', default=False)
    parser.add_argument('-3', '--flag-3', action='store_true', default=False)

    args, unknown = parser.parse_known_args()
    print(f"args        : {args}")
    print(f"unknown     : {unknown}")

    hidden = ArgumentParser(add_help=False)
    hidden.add_argument('-d', '--debug', action='store_true', default=False)
    hidden_args = hidden.parse_args(unknown)
    print(f"hidden_args : {hidden_args}")

if __name__ == "__main__":
    _parse_args()

as a result:
show help:

ubuntu → playAround $ ./test.py -h
usage: test.py [-h] [-1] [-2] [-3]

optional arguments:
  -h, --help    show this help message and exit
  -1, --flag-1
  -2, --flag-2
  -3, --flag-3

With debug flag:

ubuntu → playAround $ ./test.py -d
args        : Namespace(flag_1=False, flag_2=False, flag_3=False)
unknown     : ['-d']
hidden_args : Namespace(debug=True)

with flags 1 and 2:

ubuntu → playAround $ ./test.py -12
args        : Namespace(flag_1=True, flag_2=True, flag_3=False)
unknown     : []
hidden_args : Namespace(debug=False)

with flags 1 and 2 and debug:

ubuntu → playAround $ ./test.py -12 -d
args        : Namespace(flag_1=True, flag_2=True, flag_3=False)
unknown     : ['-d']
hidden_args : Namespace(debug=True)

The only thing you can't do with this approch is to pass the debug short flag along side to the other short flags:

ubuntu → playAround $ ./test.py -12d
usage: test.py [-h] [-1] [-2] [-3]
test.py: error: argument -2/--flag-2: ignored explicit argument 'd'

Upvotes: 0

Ignacio
Ignacio

Reputation: 156

you can do it in this way:

import argparse
parser = argparse.ArgumentParser()

group1 = parser.add_argument_group('group1')
group1.add_argument('--test1', help="test1")

group2 = parser.add_argument_group('group2')
group2.add_argument('--test2', help="test2")

args = parser.parse_args('--test1 one --test2 two'.split())

arg_groups={}

for group in parser._action_groups:
    group_dict={a.dest:getattr(args,a.dest,None) for a in group._group_actions}
    arg_groups[group.title]=argparse.Namespace(**group_dict)

This will give you the normal args, plus dictionary arg_groups containing namespaces for each of the added groups.

(Adapted from this answer)

Upvotes: 14

hpaulj
hpaulj

Reputation: 231395

Nothing in argparse is designed to do that.

For what it's worth, the parser starts off with two argument groups, one that displays as positionals and the other as optionals (I forget the exact titles). So in your example there will actually be 4 groups.

The parser only uses argument groups when formatting the help. For parsing all arguments are put in a master parser._actions list. And during parsing the parser only passes around one namespace object.

You could define separate parsers, with different sets of arguments, and call each with parse_known_args. That works better with optionals (flagged) arguments than with positionals. And it fragments your help.

I have explored in other SO questions a novel Namespace class that could nest values based on some sort of dotted dest (names like group1.optA, group2.optC, etc). I don't recall whether I had to customize the Action classes or not.

The basic point is that when saving a value to the namespace, a parser, or actually a Action (argument) object does:

setattr(namespace, dest, value)

That (and getattr/hasattr) is all that the parser expects of the namespace. The default Namespace class is simple, little more than a plain object subclass. But it could be more elaborate.

Upvotes: 5

Related Questions