Nested argparse parsers and help message - python

The situation
One module (let's call it A) implements a "primary" argparse parser, that parses known arguments common for all children classes (using parser.parse_known_args(argv)). Then, it passes remaining arguments to another object that it's calling. Let's assume it may be either object of class B or C. Both B and C have their own argparse parsers which parse remaining arguments (using parser.parse_args(argv)). They take different arguments, specific to class B or C.
Example snippet from class A:
parser = argparse.ArgumentParser(
description="Parent parser",
formatter_class=argparse.RawTextHelpFormatter,
allow_abbrev=False
)
parser.add_argument('--argument_A', action="append", default=None,
help="Help of first argument of parser A")
known, remaining_args = parser.parse_known_args(argv)
my_obj = self.create_obj(b_or_c, remaining_args)
Example snippet from class B:
parser = argparse.ArgumentParser(
description="Class B parser",
formatter_class=argparse.RawTextHelpFormatter,
allow_abbrev=False
)
parser.add_argument('--argument_B', action="append", default=None,
help="Help of first argument of parser B")
B_arguments_parsed = parser.parse_args(argv)
Example snippet from class C:
parser = argparse.ArgumentParser(
description="Class C parser",
formatter_class=argparse.RawTextHelpFormatter,
allow_abbrev=False
)
parser.add_argument('--argument_C', action="append", default=None,
help="Help of first argument of parser C")
C_arguments_parsed = parser.parse_args(argv)
While implementing passing the arguments and parsing them in the right places was easy, I didn't find a simple solution to print proper help.
The question
How do I implement help message so that my parent parser (from class A) prints it's own help and help from parser B or C?
I would like to see help message from parser A and, depending on which object was selected, parser B or C.

Related

Using `argparse` to handle arguments when it is already defined in a super class

I am using the Alexa Gadget Toolkit Python package to create an IoT device. (https://github.com/alexa/Alexa-Gadgets-Raspberry-Pi-Samples/tree/master/src/agt)
Following from Amazon's example scripts, the IoT class is defined and executed as:
from agt import AlexaGadget
class MyGadget(AlexaGadget):
...
if __name__ == "__main__":
MyGadget().main()
I'd like to add an additional "test" argument to my gadget script (to enter a testing mode), so I am using argparser.
import argparse
from agt import AlexaGadget
class MyGadget(AlexaGadget):
def main():
super().main()
parser = argparse.ArgumentParser()
parser.add_argument("--test", action="store_true", required=False)
args = parser.parse_args()
if args.test:
# Do something here
However, when I do this, reagardless of whether or not I call the super().main() before or after defining parser, I receive:
usage: mygadget.py [-h] [--pair] [--clear]
mygadget.py: error: unrecognized arguments: --test
Looking at the AGT class, they also use argparser to define arguments related to the Bluetooth functionality.
Since it is already defined in the parent, how can I also capture my own arguments in my child without modifying Amazon's AGT class?
Thanks!

Argparse with action='store_true' not working as expected

The idea is to add a flag (--slack, or -s) when running the script, so that I don't have to comment out the rep.post_report_to_slack() method every time I don't want to use it. When I run:
$ python my_script.py --slack
I get the error:
my_script.py: error: unrecognized arguments: --slack
Here's the code:
def main():
gc = Google_Connection()
meetings = gc.meetings
rep = Report(meetings)
if args.slack:
rep.post_report_to_slack()
print('posted to slack')
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--slack', help='post to slack',
action='store_true')
args = parser.parse_args()
main()
Your code works, but it relies on args being available in the module namespace, which isn't great because, for one thing, it means you can't use your function without calling the script from the command line. A more flexible and conventional approach would be to write the function to accept whatever arguments it needs, and then pass everything you get from argparse to the function:
# imports should usually go at the top of the module
import argparse
def get_meeting_report(slack=False):
gc = Google_Connection()
meetings = gc.meetings
rep = Report(meetings)
if slack:
rep.post_report_to_slack()
print('posted to slack')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--slack', help='post to slack',
action='store_true')
args = parser.parse_args()
args = vars(args)
get_meeting_report(**args)
Now you can also more easily use your function outside of argparse by calling it directly.

Hook on arguments in argparse python

I am interested in hook extra arguments parsed using argparse in one class to another method in another class which already has few arguments parsed using argparse module.
Project 1
def x():
parser = argparse.ArgumentParser()
parser.add_argument('--abc')
Project 2
def y():
parser = argparse.ArgumentParser()
parser.add_argument('--temp1')
parser.add_argument('--temp2')
When I run x(), I want to add the "--abc" argument to the list of argument y() has which is "temp1", "temp2" at runtime. Is inheritance the best way to go and defining the constructors accordingly ? Could someone provide some sample code snippet ?
Thanks !
argparse implements a parents feature that lets you add the arguments of one parser to another. Check the documentation. Or to adapt your case:
parser_x = argparse.ArgumentParser(add_help=False)
parser_x.add_argument('--abc')
parser_y = argparse.ArgumentParser(parents=[parser_x])
parser_y.add_argument('--temp1')
parser_y.add_argument('--temp2')
parser_y.print_help()
prints:
usage: ipython [-h] [--abc ABC] [--temp1 TEMP1] [--temp2 TEMP2]
optional arguments:
-h, --help show this help message and exit
--abc ABC
--temp1 TEMP1
--temp2 TEMP2
The add_help=False is needed to avoid a conflict between the -h that parser_x would normally add with the one that parser_y gets.
Another way is to let x add its argument to a predefined parser:
def x(parser=None):
if parser is None:
parser = argparse.ArgumentParser()
parser.add_argument('--abc')
return parser
def y():
....
return parser
parsery = y()
parserx = x(parsery)
It might also be useful to know that add_argument returns a reference to the argument (Action object) that it created.
parser = argparse.ArgumentParser()
arg1 = parser.add_argument('--abc')
Do this in a shell and you'll see that arg1 displays as:
_StoreAction(option_strings=['--abc'], dest='abc', nargs=None,
const=None, default=None, type=None, choices=None,
help=None, metavar=None)
arg1 is an object that you can place in lists, dictionaries. You could even, in theory, add it to another parser. That's in effect what the parents mechanism does (i.e. copy action references from the parent to the child).
You can inspire yourself from Django's management commands. They are basically setup as follow:
The entry point is run_from_argv which calls create_parser, parse the command line, extract the parsed arguments and provide them to execute;
The create_parser method creates an argparse parser and uses add_argument to prepopulate default options available for all commands. This function then calls the add_arguments method of the class which is meant to be overloaded by subclasses;
The execute method is responsible to handle the various behaviours associated to the default options. It then calls handle which is meant to be overloaded by subclasses to handle the specific options introduced by add_arguments.
Your requirements are not completely clear but I think that in your case you don't need to bother with an execute method. I’d go with:
import argparse
import sys
class BaseParser:
def create_parser(self, progname):
parser = argparse.ArgumentParser(prog=progname)
parser.add_argument('--temp1')
parser.add_argument('--temp2')
self.add_arguments(parser)
return parser
def add_arguments(self, parser):
pass # to be optionnally defined in subclasses
def parse_command_line(self, argv):
parser = create_parser(argv[0])
options = parser.parse_args(argv[1:])
parsed_options = vars(options)
self.handle(**parsed_options) # HAS TO be defined in subclasses
class X(BaseParser):
def add_arguments(self, parser):
parser.add_argument('--abc')
def handle(self, **options):
abc = options['abc']
temp1 = options['temp1']
temp2 = options['temp2']
# do stuff with thoses variables
class Y(BaseParser):
def handle(self, **options):
temp1 = options['temp1']
temp2 = options['temp2']
# do stuff
x = X()
y = Y()
args = sys.argv
x.parse_command_line(args)
y.parse_command_line(args)
You could simplify the code further if X is a subclass of Y.

how to argparse have argument print a string and do nothing else

I want to have an argument --foobar using Python argparse, so that whenever this argument appears, the program prints a particular string and exits. I don't want to consume any other arguments, I don't want to check other arguments, nothing.
I have to call add_argument somehow, and then perhaps, from parse_args() get some information and based on that, print my string.
But even though I successfully used argparse before, I am surprised to find I have trouble with this one.
For example, none of the nargs values seem to do what I want, and none of the action values seem to fit. They mess up with the other arguments, which I want to ignore once this one is seen.
How to do it?
Use a custom action= parameter:
import argparse
class FoobarAction(argparse.Action):
def __init__(self, option_strings, dest, **kw):
self.message = kw.pop('message', 'Goodbye!')
argparse.Action.__init__(self, option_strings, dest, **kw)
self.nargs = 0
def __call__(self, parser, *args, **kw):
print self.message
parser.exit()
p = argparse.ArgumentParser()
p.add_argument('--ip', nargs=1, help='IP Address')
p.add_argument('--foobar',
action=FoobarAction,
help='Abort!')
p.add_argument('--version',
action=FoobarAction,
help='print the version number and exit!',
message='1.2.3')
args = p.parse_args()
print args
Reference: https://docs.python.org/2.7/library/argparse.html#action-classes
EDIT:
It looks like there is already an action= that does exactly what FoobarAction does. action='version' is the way to go:
import argparse
p = argparse.ArgumentParser()
p.add_argument('--foobar',
action='version',
version='Goodbye!',
help='Abort!')
args = p.parse_args()
print args
I'm just going to post this here, if it helps then great!
import argparse
parser = argparse.ArgumentParser(description='')
parser.add_argument('-foobar', '--foobar', help='Description for foobar argument',
required=False)
args = vars(parser.parse_args())
if args['foobar'] == 'yes':
foobar()
Usage:
python myscrip.py -foobar yes
Use action='store_true' (see the docs).
arg_test.py:
import argparse
import sys
p = argparse.ArgumentParser()
p.add_argument('--foobar', action='store_true')
args = p.parse_args()
if args.foobar:
print "foobar"
sys.exit()
Usage:
python arg_test.py --foobar
Result:
foobar

The python's argparse errors

where report this error : TypeError: 'Namespace' object is not iterable
import argparse
def parse_args():
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument('-a', '--aa', action="store_true", default=False)
parser.add_argument('-b', action="store", dest="b")
parser.add_argument('-c', action="store", dest="c", type=int)
return parser.parse_args()
def main():
(options, args) = parse_args()
if __name__ == '__main__':
main()
Your issue has to do with this line:
(options, args) = parse_args()
Which seems to be an idiom from the deprecated "optparse".
Use the argparse idiom without "options":
import argparse
parser = argparse.ArgumentParser(description='Do Stuff')
parser.add_argument('--verbosity')
args = parser.parse_args()
Try:
args = parse_args()
print args
Results:
$ python x.py -b B -aa
Namespace(aa=True, b='B', c=None)
It's exactly like the error message says: parser.parse_args() returns a Namespace object, which is not iterable. Only iterable things can be 'unpacked' like options, args = ....
Though I have no idea what you were expecting options and args, respectively, to end up as in your example.
The error is in that parse_argv option is not required or used, only argv is passed.
Insted of:
(options, args) = parse_args()
You need to pass
args = parse_args()
And the rest remains same.
For calling any method just make sure of using argv instead of option.
For example:
a = argv.b
The best way (for me) to operate on args, where args = parser.parse_args()
is using args.__dict__. It's good, for example, when you want to edit arguments.
Moreover, it's appropriate to use long notation in your arguments, for example '--second' in '-a' and '--third' in '-b', as in first argument.
If you want to run 'main' you can should miss 'options', but it was said earlier.

Categories