Subparser -- show help for both parser and subparser - python

I've looked through dozens of similar SO questions but haven't find suitable solutions so please forgive me in case of a dublicate.
I have a problem similar to this one
I want to have a parser+subparser pair, with --help option causing help being shown for the both if subparser is "activated".
The only way I was able to get full (parser + subparser) help is:
common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument('-c', required = True)
parser = argparse.ArgumentParser(parents=[common_parser])
subparsers = parser.add_subparsers(dest="sub")
subparser = subparsers.add_parser("sub_option", parents=[common_parser])
subparser.add_argument('-o', required = False)
settings = parser.parse_args()
But then script requires the -c option to be entered twice (apparently for parser and subparser). If I don't use parents then I get normal behaviour but I don't the help I get for subparser contains only -o description (I want also -c to be shown)
P.S. Python 2.7

For a start let's distinguish between parsing behavior and help display.
I assume you have multiple subparsers (otherwise why use the subparser mechanism). -o is an argument specific to sub_option, and presumably the other subparsers have their own arguments.
What is the purpose of -c? Is it something that is common to all subparsers? Since subparsers are required, it doesn't make sense to talk about an argument that only matters to the main parser.
One way to deal with a common argument is to define it for all subparsers. The parents mechanism that you use saves you a bit of typing. Just omit it from the main parser definition. That gets rid of the problem with having to supply it twice.
If -c is defined for all the subparsers, then there isn't a need to show it in the main parser help, is there?
The whole subparser mechanism is cleanest when the subparser command is the 1st argument string, with all of its arguments, positionals and options following. It's possible to define arguments for the main parser, but it often complicates both use and help.
The issue of display the help for both the main parser and (all) the subparsers has come up before, both on SO, and on the python bug/issues. There isn't a simple solution. Some tools that may help are:
generate help under program control with parser.print_help(), and subparse.print_help().
add_subparsers() command takes parameters like prog, title and description which can be used to control the help, including the usage of the subparser help.
add_subparser() takes the same sort of parameters as ArgumentParser (since it defines a parser), description and usage may be useful.
Looking a previous question
How to show help for all subparsers in argparse?
I realized that if -c is not required, it can be defined for both the main and subparser, and appear as expected in the helps. But by making it 'required', both parsers have to see it - but only the value seen by the subparser appears in the namespace.
Also positionals defined in the main parser appear in the subparser usage.
Another subparsers help display question
argparse subparser monolithic help output
http://bugs.python.org/issue20333 argparse subparser usage message hides main parser usage discusses the question of how much of main parser usage should show up in the subparse usage line. Currently just positionals (defined before the subparsers) show up. In the patch I suggest adding required optionals as well. But you can always fudge this by defining your own prog for subparsers.

Related

Argparse, displaying custom help text without any of the boilerplate argparse text

After looking at about a dozen questions, I can't seem to find an answer.
I have a python CLI i've written using argparse. I have a main command that does nothing but regurgitate help text and then 4 subcommands. My boss wants a very specific output for the help text. He has me write it out as a text file and then we use that text file to display the help text.
However, in some circumstances, it STILL outputs parts of the argparse help text.
For example, if I run my program with no subcommands, it just outputs our help text from the file. But if I use "-h" or "--help" it will output our help text, followed by the list of positional and optional arguments and other argparse stuff. We don't want that.
I could use "add_help=False" but we want the user to be able to type -h and still get our help text. If we set add help to false, it will display our help text followed by the error "-h not recognized".
Also doing this does nothing for when the user uses -h after a subcommand. I set help=None and usage is set to my custom help text for each subcommand, but it still shows the boilerplate argparse info at the end.
This is what I want to happen: user types in the main command with no subcommands prints my custom help text and nothing else. The user types the main command, no subcommand, followed by -h/--help and it prints my custom help text and nothing else. User types in the main command, one of the subcommands, followed by -h/--help and it outputs my help text and nothing else. User types the main command, a subcommand, and then wrong arguments or too many/ too few arguments displays my help text and nothing else. Basically I only ever want it to print nothing, or print just my help text.
how do I do that? here is my main function where the parsers and subparsers are all configured:
def main():
# Import help text from file
p = Path(__file__).with_name("help.txt")
with p.open() as file:
help_text = file.read()
# Configure the top level Parser
parser = argparse.ArgumentParser(prog='myprog', description='My program', usage=help_text)
subparsers = parser.add_subparsers()
# Create Subparsers to give subcommands
parser_one = subparsers.add_parser('subcommandone', prog='subcommandone', usage=help_text, help=None)
parser_one.add_argument('arg1', type=str)
parser_one.add_argument('-o', '--option1', default='mydefault', type=str)
parser_two= subparsers.add_parser('subcommandtwo', usage=help_text, help=None, prog='subcommandtwo')
parser_three= subparsers.add_parser('subcommandthree', usage=help_text, help=None, prog='subcommandthree')
parser_four= subparsers.add_parser('subcommandfour', usage=help_text, help=None, prog='subcommandfour')
# Assign subparsers to their respective functions
parser_one.set_defaults(func=functionone)
parser_two.set_defaults(func=functiontwo)
parser_three.set_defaults(func=functionthree)
parser_four.set_defaults(func=functionfour)
parser.set_defaults(func=base_case)
# Parse the arguments and call appropriate functions
args = parser.parse_args()
if len(sys.argv) == 1:
args.func(args, parser)
else:
args.func(args)
Any thoughts?
You can use sys.exit() after the help text has been displayed, and before the parsing has begun, to avoid problems with "-h not recognized".
So anywhere before the line
# Parse the arguments and call appropriate functions
add
if len(sys.argv) == 1 or '-h' in sys.argv or '--help' in sys.argv:
print(help_text)
sys.exit(1)
In situations where that is not good enough you can subclass argparse.HelpFormatter like so
usage_help_str = 'myscript command [options]'
epilog_str = "More info can be found at https://..."
class Formatter(argparse.HelpFormatter):
# override methods and stuff
def formatter(prog):
return Formatter(prog)
parser = argparse.ArgumentParser(formatter_class=formatter, epilog=epilog_str, usage=usage_help_str, add_help=False)
I tried looking around for documentation on subclassing the helpFormatter, but I couldn't find anything. It looks like people are just looking at the source code to figure out how to subclass it.

argparse action menu with variable options

I'm trying to build a program that takes the first argument as an action (like program list, program create, program delete, etc.), and then uses the rest of the provided options in context to the action (like -c, --all, etc.).
How can I define the same optional argument several times, for each action I define in the first argument?
#hpaulj's comment helped me solve this. I've used subparsers and created separated contexts to store a config for each main action that I have in my script.
An example can be found here: https://stackoverflow.com/a/53324772/901925
You can use something like this:
parser = argparse.ArgumentParser()
parser.add_argument("action", type=str, help="action like create or delete")
parser.add_argument("-c", action="count", default=0)
in this case, the first argument given without -c will be stored in action variable.

Print all argparse arguments including defaults

I want to log the usage of a python program which uses the argparse module. Currently, the logger records the command line usage similar to the answer given in this post. However, that only gives the command line arguments, and not including the defaults set later in argparse (which is the intended use, after all). Is there a simple way to print ALL the argparse options to create a nice, tidy usage log entry that includes the default values?
It isn't difficult to go into the argparse namespace to fetch each argument by name, but I am hoping that someone has a concise way of extracting the needed info.
In response to the accepted answer:
Great! If anyone is interest, here is my implementation with logging:
logger.info("Usage:\n{0}\n".format(" ".join([x for x in sys.argv])))
logger.debug("All settings used:") for k,v in sorted(vars(args).items()):
logger.debug("{0}: {1}".format(k,v))
I've done something similar in an application, hopefully the below snippets give you what you need. It is the call to vars that gave me a dictionary of all the arguments.
parser = argparse.ArgumentParser(description='Configure')
....
args = parser.parse_args()
......
options = vars(args)

argparse conflict resolver for options in subcommands turns keyword argument into positional argument

I have a Python script that runs two sub-commands who accept the same option, --config. I would like to create a third sub-command that can run the first two subcommands together, sequentially.
Using argparse, I've created a subparser for each sub-command, as well as a third subparser, whose parents are the two sub-commands. Just to clarify:
subcommand1 = subparsers.add_parser('subcommand1')
subcommand1.add_argument('--config', help="The config")
subcommand2 = subparsers.add_parser('subcommand2')
subcommand2.add_argument('--config', help="The config")
wrappercommand = subparsers.add_parser('wrappercommand',
parents=[subcommand1, subcommand2],
conflict_handler='resolve')
Everything works when I run wrappercommand, or subcommand2. However, subcommand1 breaks, with this as the output:
$ run_command.py subcommand1 --config path_to_config.ini
usage: run_command.py subcommand1 config
optional arguments:
help show this help message and exit
config The config
It looks like argparse has turned a keyword arg ("--config") into a positional one ("config"). Is this the expected behavior when conflicting options are resolved by argparse?
I think you are pushing this conflict handler into unintended and untested territory. Normally parents are standalone parsers that don't get used. They are just a source for Actions. And conflicts regarding -h are handled with add_help=False.
By way of background: with the default conflict_handler (error) you'd get error messages when creating the wrappercommand subparser:
argparse.ArgumentError: argument -h/--help: conflicting option string(s): -h, --help
and after adding some add_help=False, you'd still get:
argparse.ArgumentError: argument --config: conflicting option string(s): --config
The resolve conflict handler replaces the error messages with some sort of 'resolution'. The script below demonstrates what is happening.
The resolve handler deleted the option_strings for the subcommand1 actions , while leaving the actions in place. In effect it turns both into positionals. And since help has nargs=0, it is always run. Hence, the help display.
The intention of _handle_conflict_resolve is to remove evidence of the first argument, so the new argument can be added. That works fine when the conflict is produced by two add_argument commands with the same option strings. But here the conflict is produced by 'copying' actions from 2 parents. But parent actions are copied by reference, so changes in the 'child' end up affecting the 'parent'.
Some possible solutions:
add the arguments to wrappercommand directly. This parents mechanism just adds arguments from the parents to the child. It does not 'run' the parents sequentially.
write your own _handle_conflict_... function to correctly resolve the conflict.
remove the conflicts so you can use the parents without using the resolve handler.
I have filed a bug report with this example
http://bugs.python.org/issue22401 :
parent1 = argparse.ArgumentParser(add_help=False)
parent1.add_argument('--config')
parent2 = argparse.ArgumentParser(add_help=False)
parent2.add_argument('--config')
parser = argparse.ArgumentParser(parents=[parent1,parent2],
conflict_handler='resolve')
def foo(parser):
print [(id(a), a.dest, a.option_strings) for a in parser._actions]
foo(parent1)
foo(parent2)
foo(parser)
which produces:
[(3077384012L, 'config', [])]
[(3076863628L, 'config', ['--config'])]
[(3076864428L, 'help', ['-h', '--help']), (3076863628L, 'config', ['--config'])]
Note the missing option_strings for parent1, and the matching id for the other 2. parent1 cannot be used again, either as a parent or a parser.
argparse - Combining parent parser, subparsers and default values
is another case where copying parent's actions by reference creates complications (in changing defaults).

Using OptionParser vs sys.argv

For a script, I'm currently using OptionParser to add variables to an input. However, all of my current options are booleans, and it seems it would just be easier to parse using argv instead. For example:
$ script.py option1 option4 option6
And then do something like:
if 'option1' in argv:
do this
if 'option2' in argv:
do this
etc...
Would it be suggested to use argv over OptionParser when the optionals are all booleans?
"However, all of my current options are booleans, and it seems it
would just be easier to parse using argv instead."
There's nothing wrong with using argv, and if it's simpler to use argv, there's no reason not to.
OptionParser has been deprecated, and unless you're stuck on an older version of python, you should use the ArgParser module.
For one-off scripts, there's nothing wrong with parsing sys.argv yourself. There are some advantages to using an argument parsing module instead of writing your own.
Standardized. Do you allow options like "-test", because the standard is usually 2 underscores for multichar options (e.g. "--test"). With a module, you don't have to worry about defining standards because they're already defined.
Do you need error-catching and help messages? Because you get a lot of that for free with ArgParse.
Will someone else be maintaining your code? There's already lots of documentation and examples of ArgParse. Plus, it's somewhat self documenting, because you have to specify the type and number of arguments, which isn't always apparent from looking at a sys.argv parser.
Basically, if you ever expect your command line options to change over time, or expect that your code will have to be modified by someone else, the overhead of ArgParse isn't that bad and would probably save you time in the future.

Categories

Resources