Show hidden option using argparse - python

I'm using argprase to create an option, and it's a very specific option to do one specific job. The script currently has roughly 30 knobs, and most aren't used regularly.
I'm creating an option:
opt.add_argument('-opt',help="Some Help", help=argparse.SUPPRESS)
But i want there to be two ways to show the help for the script:
my_script -help
my_script -help-long
I want the -help-long to also show all the hidden args. I couldn't find a way to do this.
Is there a way to implement this behavior?

I don't think there's a builtin way to support this. You can probably hack around it by checking sys.argv directly and using that to modify how you build the parser:
import sys
show_hidden_args = '--help-long' in sys.argv
opt = argparse.ArgumentParser()
opt.add_argument('--hidden-arg', help='...' if show_hidden_args else argparse.SUPPRESS)
opt.add_argument('--help-long', help='Show all options.', action='help')
args = opt.parse_args()
Of course, if writing this over and over is too inconvenient, you can wrap it in a helper function (or subclass ArgumentParser):
def add_hidden_argument(*args, **kwargs):
if not show_hidden_args:
kwargs['help'] = argparse.SUPPRESS
opt.add_argument(*args, **kwargs)
And you'll probably want to add a non-hidden --help-long argument so that users know what it supposedly does...

This is a variation on #mgilson's answer, looking in sys.argv to see whether we should suppress some help or not
import argparse
import sys
def hide_args(arglist):
for action in arglist:
action.help=argparse.SUPPRESS
hidelist=[]
parser = argparse.ArgumentParser()
a1 = parser.add_argument('--foo',help='normal')
a2 = parser.add_argument('--bar',help='hidden')
hidelist.append(a2)
if '-h' in sys.argv[1:]:
hide_args(hidelist)
args = parser.parse_args()
Here I've chosen to interpret --help as asking for a long help; -h for the short. I could have added a separate --longhelp argument instead.
1207:~/mypy$ python3 stack37303960.py --help
usage: stack37303960.py [-h] [--foo FOO] [--bar BAR]
optional arguments:
-h, --help show this help message and exit
--foo FOO normal
--bar BAR hidden
for a short help
1207:~/mypy$ python3 stack37303960.py -h
usage: stack37303960.py [-h] [--foo FOO]
optional arguments:
-h, --help show this help message and exit
--foo FOO normal
add_argument returns a pointer to the Action object that it created. Here I save selected ones in the hidelist. Then I conditionally iterate through that list and change the help to SUPPRESS. Many of the attributes of an Action can be changed after the initial creation (experiment in an interactive session).
The parser also maintains a list of actions. The default help is the first one on the parser._actions list. It uses this list both for parsing and formatting the help.
In [540]: parser._actions[0]
Out[540]: _HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None)

You could achieve something by subclassing ArgumentParser and _HelpAction:
class LongHelp(argparse._HelpAction):
def __init__(self,*args, **kwargs):
super().__init__(*args, **kwargs)
def __call__(cls, parser, namespace, values, option_string):
print(parser.long_help)
class ArgParserWithLongHelp(argparse.ArgumentParser):
def __init__(self):
super().__init__(self)
self.long_help = {}
self.add_argument("--long-help", action=LongHelp )
def add_argument(self, *args, **kwargs):
if kwargs.get('long_help'):
self.long_help.update({k:kwargs['long_help'] for k in args})
kwargs.pop('long_help')
super().add_argument(*args, **kwargs)
opt = ArgParserWithLongHelp()
opt.add_argument('-opt', help=argparse.SUPPRESS, long_help='Some extra help')
args = opt.parse_args()

Related

Show subparser help and not main parser help when non-existent arguments used?

I have a small CLI app (myscript.py) that is defined like so.
import sys
import argparse
class MyParser(argparse.ArgumentParser):
'''
Overriden to show help on default.
'''
def error(self, message):
print(f'error: {message}')
self.print_help()
sys.exit(2)
def myfunc(args):
'''
Some function.
'''
print(args.input_int**2)
def main():
# Define Main Parser
main_par = MyParser(
prog='myapp',
description='main help')
# Define Command Parser
cmd_par = main_par.add_subparsers(
dest='command',
required=True)
# Add Subcommand Parser
subcmd_par = cmd_par.add_parser(
'subcmd',
description='subcmd help')
# Add Subcommand Argument
subcmd_par.add_argument(
'-i', '--input-int',
type=int,
help='some integer',
required=True)
# Add FromName Dispatcher
subcmd_par.set_defaults(
func=myfunc)
# Parse Arguments
args = main_par.parse_args()
# Call Method
args.func(args)
if __name__ == '__main__':
main()
The MyParser class simply overrides the error() method in argparse.ArgumentParser class to print help on error.
When I execute
$ python myscript.py
I see the default / main help. Expected.
When I execute
$ python myscript.py subcmd
I see the subcmd help. Expected.
When I execute
$ python myscript.py subcmd -i ClearlyWrongValue
I also see the subcmd help. Expected.
However, very annoyingly if I do the following
$ python myscript.py subcmd -i 2 --non-existent-argument WhateverValue
I see the default / main help and not subcmd help.
What can I do, to ensure that this last case shows me the subcmd help and not the main help? I thought the subparser structure would automatically procure the help from subcmd as found in the third case, but it is not so? Why?
The unrecognized args error is raised by parse_args
def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
return args
The subparser is called via the cmd_par.__call__ with:
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
for key, value in vars(subnamespace).items():
setattr(namespace, key, value)
if arg_strings:
vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
That is it is called with parse_known_args, and it's extras are returned to the main as UNRECOGNIZED. So it's the main than handles these, not the subparser.
In the $ python myscript.py subcmd -i ClearlyWrongValue case, the subparser raises a ArgumentError which is caught and converted into a self.error call.
Similarly, the newish exit_on_error parameter handles this kind of ArgumentError, but does not handle the urecognized error. There was some discussion of this in the bug/issues.
If you used parse_known_args, the extras would be ['--non-existent-argument', 'WhateverValue'], without distinguishing which parser initially classified them as such.

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

How do I get the user specified list of optional command line args not containing the defaults using argparse in python?

I want to know which options were explicitly passed through command-line.
Consider the following argparse setup in test.py:
parser.add_argument("--foo", default=True, action="store_true")
parser.add_argument("--bar", default=False, action="store_true")
When I execute ./test.py --foo --bar, I shall get foo=True, bar=True in the Namespace.
In this case, --foo and --bar were passed explicitly through command-line.
When I execute ./test.py --bar, I shall still get foo=True, bar=True in the Namespace.
So, I need to find which args were actually passed while executing through command-line (in the 2nd case : --bar), without sacrificing the defaults functionality.
One approach is to search in argv, but it's not efficient and doesn't look elegant.
I want to know, if there is any argparse api or any other better approach which shall allow me to do this?
Simply set no default. If the variable is not set, the user did not pass it. After checking that, you can handle the default yourself.
With a 'store_true' action, the builtin default is 'False'
With
parser.add_argument('--foo',action='store_true')
no input produces
Namespace(foo=False)
while '--foo' produces
Namespace(foo=True)
with
parser.add_argument('--foo',action='store_true', default=True)
it is always foo=True. That argument is useless. DO NOT set your own default when using 'store_true' or 'store_false'.
If you want to know whether the user gave you a --foo or not, use the first form, and check whether the namespace value is true or not. If in later code you need foo to be True regardless of what the user gave you, set it explicitly, after you have used argparse.
The answers and comments here recommended to not set defaults, and then handle the defaults on my own within the code. However, the add_argument calls aren't completely under my control, so this wasn't really an option.
Initially I went with checking the presence of the options in sys.argv. This approach quickly proved inefficient, bug-prone and not at all scalable.
Finally, I ended up with this which seems to be working just fine:
class _Reflection(object):
def __init__(self, source, reflection, name=None):
self.source = source
self.reflection = reflection
self.name = name
def __getattr__(self, attribute):
self.attribute = attribute
return _Reflection(self.source.__getattribute__(attribute), self.reflection.__getattribute__(attribute), name=attribute)
def __call__(self, *args, **kwargs):
source_output = self.source(*args, **kwargs)
if self.name == 'add_argument':
# if the method being called is 'add_argument',
# over-ride the 'default' argument's value to 'None' in our secondary argparser.
kwargs['default'] = None
reflection_output = self.reflection(*args, **kwargs)
return _Reflection(source_output, reflection_output)
class ReflectionArgumentParser(object):
def create(self, *args, **kwargs):
self.parser = argparse.ArgumentParser(*args, **kwargs)
self._mirror = argparse.ArgumentParser(*args, **kwargs)
return _Reflection(self.parser, self._mirror)
def parse_args(self, *args, **kwargs):
return self.parser.parse_args(*args, **kwargs)
def filter_defaults(self, *args, **kwargs):
return self._mirror.parse_args(*args, **kwargs)
mirrorParser = ReflectionArgumentParser()
parser = mirrorParser.create()
parser.add_argument('-f', '--foo', default=False, action="store_true")
parser.add_argument('-b', '--baz', default=0, action="store_const", const=10)
parser.add_argument('bar', nargs='*', default='bar')
print mirrorParser.parse_args([])
# Outputs: Namespace(bar='bar', baz=0, foo=False)
print mirrorParser.filter_defaults([])
# Outputs: Namespace(bar=[], baz=None, foo=None)
print mirrorParser.filter_defaults('--foo -b lorem ipsum'.split())
# Outputs: Namespace(bar=['lorem', 'ipsum'], baz=10, foo=True)
I have tried this implementation with argument-groups and subparsers.
This doesn't deal with set_defaults method, however the additions required are trivial.
This is possible using the ArgumentParser.get_default(dest) method.
Basically, you iterate over all of the parsed arguments and collect which ones are not equal to the default value:
args = parser.parse_args()
non_default = {arg: value for (arg, value) in vars(args).iteritems() if value != parser.get_default(arg)}
Although this doesn't work in your specific example because --foo is a do-nothing argument (it sets the variable to the default value).

argparse: don't show usage on -h

The code
from argparse import ArgumentParser
p = ArgumentParser(description = 'foo')
p.add_argument('-b', '--bar', help = 'a description')
p.parse_args()
...results in the output:
$ python argparsetest.py -h
usage: argparsetest.py [-h] [-b BAR]
foo
optional arguments:
-h, --help show this help message and exit
-b BAR, --bar BAR a description
What I'd like is:
$ python argparsetest.py -h
foo
optional arguments:
-h, --help show this help message and exit
-b BAR, --bar BAR a description
e.g., no usage message when asking for help. Is there some way to do this?
definitely possible -- but I'm not sure about documented ...
from argparse import ArgumentParser,SUPPRESS
p = ArgumentParser(description = 'foo',usage=SUPPRESS)
p.add_argument('-b', '--bar', help = 'a description')
p.parse_args()
From reading the source, I've hacked a little something together which seems to work when displaying error messages as well ... warning -- This stuff is mostly undocumented, and therefore liable to change at any time :-)
from argparse import ArgumentParser,SUPPRESS
import sys as _sys
from gettext import gettext as _
class MyParser(ArgumentParser):
def error(self, message):
usage = self.usage
self.usage = None
self.print_usage(_sys.stderr)
self.exit(2, _('%s: error: %s\n') % (self.prog, message))
self.usage = usage
p = MyParser(description = 'foo',usage=SUPPRESS)
p.add_argument('-b', '--bar', help = 'a description')
p.parse_args()
Note: to not show usage for a specific argument, use
parser.add_argument('--foo', help=argparse.SUPPRESS)
per the documentation.
One additional note: if you don't want help at all you can construct the parser with add_help=False
def parse_args():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("arg")
return parser.parse_known_args()
def main():
args, remaining = parse_args()
print(args)
print(remaining)
if __name__ == '__main__':
main()
user#host % python3 /tmp/test.py f
Namespace(arg='f')
[]
user#host % python3 /tmp/test.py f -h
Namespace(arg='f')
['-h']
user#host % python3 /tmp/test.py f -h --help
Namespace(arg='f')
['-h', '--help']
user#host %

Categories