Argparse custom help from text file - python

I want to use the argparse library because of its flexibility, but I am having trouble disabling the default help dialogue to show a custom one from a text file. All I want to do is display the text from the text file when the "-h" or "--help" option is passed. Here is an example of how I am trying this:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("file", type=str, nargs='+')
parser.add_argument("-xmin", type=float)
parser.add_argument("-xmax", type=float)
parser.add_argument("-ymin", type=float)
parser.add_argument("-ymax", type=float)
parser.add_argument("-h", "--help", action="store_true")
args = parser.parse_args()
if args.help is True:
print isip_get_help()
exit(-1)
But it still outputs:
nedc_[1]: python isip_plot_det.py -h
usage: isip_plot_det.py [-xmin XMIN] [-xmax XMAX] [-ymin YMIN] [-ymax YMAX]
[-h]
file [file ...]
isip_plot_det.py: error: too few arguments
Any ideas?

What you are getting is an error message, not the help (i.e. it's not produced by your -h).
isip_plot_det.py: error: too few arguments
The error message shows the usage part of the normal help. You can change that with a usage parameter:
parser = ArgumentParser(usage = 'my custom usage line')
You can also test the usage display with
parser.print_usage()
or
astr = parser.format_usage()
to get a printable string.
The normal help argument uses a special help action class. Its call method is:
def __call__(self, parser, namespace, values, option_string=None):
parser.print_help()
parser.exit()
Notice that it displays the help with parser.print_help(), and then exits. That occurs as soon as it parses the -h string. That way it doesn't produce any errors like the too few arguments or unrecognized arguments (which are produced at the end of parsing).
So another way of customizing the help is to subclass ArgumentParser, and define your own print_help method. You can also customize the exit and error methods.
The default print_help is:
def print_help(self, file=None):
if file is None:
file = sys.stdout
self._print_message(self.format_help(), file)
You could customize format_help instead.
class MyParser(argparse.ArgumentParser):
def format_help(self):
return 'my custom help message\n second line\n\n'
Sample usage:
In [104]: parser=MyParser(usage='custom usage')
In [105]: parser.parse_args(['-h'])
my custom help message
second line
...
In [106]: parser.parse_args(['unknown'])
usage: custom usage
ipython3: error: unrecognized arguments: unknown
...

The failure is caused by the missing required argument file. The reason why action would not be subjected to this validation requirement is simply because they are executed first, i,e, args.help will be set to True. However, once the parser completes parsing the arguments it would have triggered a sys.exit due to validation failure, and this ends with printing out the default usage and your code (that prints your desired help message) will simply never be executed (you can try calling your program as is with a -h file arguments and your help message should print).
You can either add your custom action using parser.register (not officially supported as it's considered private API, but it works - read argparse.py to see how it all works), or alternatively subclass ArgumentParser and override the print_help method to call isip_get_help().
You could try something like this:
class MyArgumentParser(argparse.ArgumentParser):
def print_help(self, file=None):
print(isip_get_help())
exit(-1)
parser = MyArgumentParser()
...
Leave the default add_help in, but with print_help overridden to call your custom help it should work.

Related

How do I suppress an argument when nothing is input on command line?

#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s')
parser.add_argument('--choice', '-c', default = argparse.SUPPRESS)
args = parser.parse_args()
def main(selection, choice):
print(selection)
print(choice)
if __name__=='__main__':
main(args.selection, args.choice)
The example provided is just to provide something simple and short that accurately articulates the actual problem I am facing in my project. My goal is to be able to ignore an argument within the code body when it is NOT typed into the terminal. I would like to be able to do this through passing the argument as a parameter for a function. I based my code off of searching 'suppress' in the following link: https://docs.python.org/3/library/argparse.html
When I run the code as is with the terminal input looking like so: python3 stackquestion.py -s cheese, I receive the following error on the line where the function is called:
AttributeError: 'Namespace' object has no attribute 'choice'
I've tried adding the following parameter into parser like so:
parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
I've also tried the above with
parser.add_argument('--choice', '-c')
But I get the same issue on the same line.
#Barmar answered this question in the comments. Using 'default = None' in parser.add_argument works fine; The code runs without any errors. I selected the anser from #BorrajaX because it's a simple solution to my problem.
According to the docs:
Providing default=argparse.SUPPRESS causes no attribute to be added if the command-line argument was not present:
But you're still assuming it will be there by using it in the call to main:
main(args.selection, args.choice)
A suppressed argument won't be there (i.e. there won't be an args.choice in the arguments) unless the caller specifically called your script adding --choice="something". If this doesn't happen, args.choice doesn't exist.
If you really want to use SUPPRESS, you're going to have to check whether the argument is in the args Namespace by doing if 'choice' in args: and operate accordingly.
Another option (probably more common) can be using a specific... thing (normally the value None, which is what argparse uses by default, anyway) to be used as a default, and if args.choice is None, then assume it hasn't been provided by the user.
Maybe you could look at this the other way around: You want to ensure selection is provided and leave choice as optional?
You can try to set up the arguments like this:
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s', required=True)
parser.add_argument('--choice', '-c')
args = parser.parse_args()
if __name__ == '__main__':
if args.choice is None:
print("No choice provided")
else:
print(f"Oh, the user provided choice and it's: {args.choice}")
print(f"And selection HAS TO BE THERE, right? {args.selection}")

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 module with multiple command and arguments

I'm trying to parse commands with arguments using the python 3 Built-in argparse module.
I have read the argparse documentation partially, however, I could not find anything that meets my requirements.
I parse the arguments as input (I have my reasons).
I have multiple commands, for each, there are both essential and optional arguments.
For example:
restart --name (the name is replaced)
restart is the command and name is the essential argument.
Currently my code would count the "--" in the input and call the function with corresponding booleans (if --all given, is_all boolean parameter will be true)
I can also add an optional argument --all (all is not replaced).
Sounds like you are looking for something like this
def get_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("--arg1", required=False, default=None)
parser.add_argument("--arg2", required=False, default=None)
return parser.parse_args()
args = get_arguments()
if args.arg1:
# do something
Really hard to answer this without seeing your code or example of what you want.
I'm assuming you're doing something like a shell of sorts. I'm going to also assume that each line entered has a command, each with their own arguments.
from argparse import ArgumentParser
def get_parser(cmd):
'''Returns a parser object for a given command'''
# Instantiate the parser object, add the appropriate arguments for the command
return parser # This is an example -- you need to instantiate it
def main():
while True:
try:
in_line = input('> ')
if not in_line.strip(): # Quit if empty
break
args = in_line.split()
parser = get_parser(args[0])
opts = parser.parser_args(args)
# Do stuff with opts depending on command
except EOFError:
break
except SystemExit:
pass # Prevent failures from killing the program by trapping sys.exit()

Get version string from argparse

I'm trying to get back the version string I defined in argparse for use in logging.
I'm using a typical setup along the lines of:
__version__ = "0.1"
parser = argparse.ArgumentParser()
parser.add_argument('--version', '-V', action='version', version="%(prog)s " + __version__)
args = parser.parse_args()
When I print parser.version() or parser.print_version() or parser.format_version() I get None. One solution is to call parser.parse_args(['-V']) but that also terminates the execution of the program. I know I can just re-create the string and pass it to the logger, but I thought there must be a way to get this from argparse. I'm using python v2.7.5 and argparse v1.1.
There is no public API to get that information. The parser.version attribute is deprecated. You'd have to find the argparse._VersionAction object and get it from there:
version_action = next((action for action in parser._get_optional_actions()
if isinstance(action, argparse._VersionAction)), None)
print version_action.version if version_action else 'unknown'
This uses private methods that are subject to change, and all you get is the exact same string you gave to argparse:
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--version', '-V', action='version', version="%(prog)s " + __version__)
_VersionAction(option_strings=['--version', '-V'], dest='version', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help="show program's version number and exit", metavar=None)
>>> version_action = next((action for action in parser._get_optional_actions() if isinstance(action, argparse._VersionAction)), None)
>>> print version_action.version if version_action else 'unknown'
%(prog)s 0.1
Much easier just to store that value somewhere else too, then pass it to argparse and use the value directly.
format_version (and print_version which uses format_version) displays the parser.version attribute, which you can set with the version parameter. But as Martijn wrote, that approach to showing a version is depricated. You'll get a warning message.
That action='version' argument takes a different route. It has its own version parameter (but using parser.version as a backup). When triggered by the -v argument string, it displays that version info and calls system exit.
You could still call -v, and avoid the system exit with a try block:
try:
p.parse_args(['-V'])
except SystemExit:
pass
That's handy for testing, but not something you want in production. Plus it traps other exits like help and errors.
As with any argument, you can save a link to the Action, and display, use, or even modify its attributes.
version_action = parser.add_argument('--version', '-V', action='version',
version="%(prog)s " + __version__)
print version_action
print version_action.version
assert isinstance(version_action, argparse._VersionAction)
Functionally this is the same as Martijn's search of the parser._get_optional_actions(), but simpler.
You could replicate the action of the _VersionAction.__call__, without the system exit, with:
def foo(action, parser):
formatter = parser._get_formatter()
formatter.add_text(action.version)
return formatter.format_help()
foo(version_action, parser)
Though in this case all it does is fill in the %(prog)s string.
http://bugs.python.org/issue9399 'Provide a 'print' action for argparse', discusses adding a 'print' or 'write' action type. It would behave like the 'version' one, but without the system exit, and possibly more control over formatting and print destination.
No, there must not be a way to get it from argparse. Why would there be? You tell argparse what the version number is, not the other way around.
And you don't need to "recreate" the string. Just create it once, and then pass it into argparse.
I'm sure it's available somehow, from some attribute on argparse, but getting it from there really makes no sense, and requires you to use argparse internals, which may change in the future.

Argparse: Way to include default values in '--help'?

Suppose I have the following argparse snippet:
diags.cmdln_parser.add_argument( '--scan-time',
action = 'store',
nargs = '?',
type = int,
default = 5,
help = "Wait SCAN-TIME seconds between status checks.")
Currently, --help returns:
usage: connection_check.py [-h]
[--version] [--scan-time [SCAN_TIME]]
Test the reliability/uptime of a connection.
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--scan-time [SCAN_TIME]
Wait SCAN-TIME seconds between status checks.
I would prefer something like:
--scan-time [SCAN_TIME]
Wait SCAN-TIME seconds between status checks.
(Default = 5)
Peeking at the help formatter code revealed limited options. Is there a clever way to get argparse to print the default value for --scan-time in a similar fashion, or should I just subclass the help formatter?
Use the argparse.ArgumentDefaultsHelpFormatter formatter:
parser = argparse.ArgumentParser(
# ... other options ...
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
To quote the documentation:
The other formatter class available, ArgumentDefaultsHelpFormatter, will add information about the default value of each of the arguments.
Note that this only applies to arguments that have help text defined; with no help value for an argument, there is no help message to add information about the default value to.
The exact output for your scan-time option then becomes:
--scan-time [SCAN_TIME]
Wait SCAN-TIME seconds between status checks.
(default: 5)
Add '%(default)s' to the help parameter to control what is displayed.
parser.add_argument("--type", default="toto", choices=["toto","titi"],
help = "type (default: %(default)s)")
Notes:
It is %+ default in parenthesis + format characters (not to be confused with curly brackets {default} we find in format or f-string)
Don't forget to add the "specifier character" for the type representation at the end (i.e. s for strings, d for integers, f for floats, etc.)
You can also add the usual "printf" format specifiers (like number of digits for floats, leading zeros, etc.)
You can refer to printf documentation for more details.
Wrapper class
This is the most reliable and DRY approach I've found so far to both show defaults and use another formatter such as argparse.RawTextHelpFormatter at the same time:
#!/usr/bin/env python3
import argparse
class ArgumentParserWithDefaults(argparse.ArgumentParser):
def add_argument(self, *args, help=None, default=None, **kwargs):
if help is not None:
kwargs['help'] = help
if default is not None and args[0] != '-h':
kwargs['default'] = default
if help is not None:
kwargs['help'] += ' Default: {}'.format(default)
super().add_argument(*args, **kwargs)
parser = ArgumentParserWithDefaults(
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument('-a', default=13, help='''my help
for a''')
parser.add_argument('-b', default=42, help='''my help
for b''')
parser.add_argument('--no-default', help='''my help
for no-default''')
parser.add_argument('--no-help', default=101)
parser.print_help()
print()
print(parser.parse_args())
Output:
usage: main.py [-h] [-a A] [-b B] [--no-default NO_DEFAULT]
[--no-help NO_HELP]
optional arguments:
-h, --help show this help message and exit
-a A my help
for a Default: 13
-b B my help
for b Default: 42
--no-default NO_DEFAULT
my help
for no-default
--no-help NO_HELP
Namespace(a=13, b=42, no_default=None, no_help=101)
ArgumentDefaultsHelpFormatter + RawTextHelpFormatter multiple inheritance
Multiple inheritance just works, but it does not seem to be public API:
#!/usr/bin/env python3
import argparse
class RawTextArgumentDefaultsHelpFormatter(
argparse.ArgumentDefaultsHelpFormatter,
argparse.RawTextHelpFormatter
):
pass
parser = argparse.ArgumentParser(
formatter_class=RawTextArgumentDefaultsHelpFormatter
)
parser.add_argument('-a', default=13, help='''my help
for a''')
parser.add_argument('-b', default=42, help='''my help
for b''')
parser.print_help()
Output:
usage: a.py [-h] [-a A] [-b B]
optional arguments:
-h, --help show this help message and exit
-a A my help
for a (default: 13)
-b B my help
for b (default: 42)
It just works works because as we can see trivially from the sources https://github.com/python/cpython/blob/v3.6.5/Lib/argparse.py#L648 that:
RawTextHelpFormatter implements _split_lines
ArgumentDefaultsHelpFormatter implements _get_help_string
so we can guess that they will work together just fine.
However, this does not seem to be public API, and neither are the methods of formatter_class, so I don't think there is a public API way to do it currently. argparse docstring says:
All other classes in this module are considered implementation details.
(Also note that HelpFormatter and RawDescriptionHelpFormatter are only
considered public as object names -- the API of the formatter objects is
still considered an implementation detail.)
See also: Customize argparse help message
Tested on Python 3.6.5.
It is often useful to be able to automatically include the default values in the help output, but only those that were explicitly specified (with default=..). The methods already mentioned have some shortcomings in this respect:
The ArgumentDefaultsHelpFormatter method prints out (default: None) for every argument whose default was not explicitly specified, and (default: False) for 'flags' (action='store_true'). This clutters the help output. To avoid it, default=argparse.SUPPRESS needs to be manually added for each such argument.
The '%(default)s' method requires manually adding it to all the arguments' help strings that we do want printed in help.
Both methods end up needing manual intervention to print out only the "right" defaults.
One way to do this automatically is to augment the ArgumentDefaultsHelpFormatter to ignore the Nones and Falses default values:
class ExplicitDefaultsHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
if action.default in (None, False):
return action.help
return super()._get_help_string(action)
Use it in place of ArgumentDefaultsHelpFormatter:
parser = argparse.ArgumentParser(
formatter_class=ExplicitDefaultsHelpFormatter
)
This will print only the explicitly set default values in the help output.
Note: if an argument's default was explicitly set as None or False, it won't be shown in help with this class; add %(default)s string to help for that argument if you want it in the help output.

Categories

Resources