Check for certain arguments and assign values to an arbitrary variable - python

I'm writing a program and need to look for one of two arguments set on the command line, and save a value to a single variable based on which one is set.
If I call the program like this:
myprogram -a --foo 123
I want the variable action set to 'a value'. Call it like this:
myprogram -b --foo 123
And action should be set to 'another value'. Call it with neither:
myprogram -c --foo 123
And it should exit with usage info.
Obviously I can do this with some if statements after the fact:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='count')
parser.add_argument('-b', action='count')
parser.add_argument('--foo')
....
args = parser.parse_args()
if args.a == 0 and args.b == 0:
parser.print_usage()
sys.exit(1)
if args.a > 0:
action = 'a value'
elif args.b > 0:
action = 'another value'
But I'm wondering if argparse can do it for me with less code. From what I've seen in the documentation it's not possible, but Python is very very new to me. Thanks.

Look at action='store_const'.
In [240]: parser=argparse.ArgumentParser()
In [241]: parser.add_argument('-a', dest='action',
action='store_const', const='a value')
In [242]: parser.add_argument('-b', dest='action',
action='store_const', const='another value')
In [243]: parser.add_argument('--foo')
In [244]: parser.parse_args('-a --foo 123'.split())
Out[244]: Namespace(action='a value', foo='123')
In [245]: parser.parse_args('-b --foo 123'.split())
Out[245]: Namespace(action='another value', foo='123')
In [246]: parser.parse_args('-c --foo 123'.split())
usage: ipython3 [-h] [-a] [-b] [--foo FOO]
ipython3: error: unrecognized arguments: -c
SystemExit: 2
So args.action will have a value' orb value' depending on the argument. Note both -a and -b store to the same dest.
I left -c undefined, so it uses the normal undefined exit with usage. that could be refined.
Defining a c like this would let you do your own exit:
In [247]: parser.add_argument('-c', dest='action', action='store_const', const='exit')
In [248]: args=parser.parse_args('-c --foo 123'.split())
In [249]: if args.action=='exit':parser.print_usage()
usage: ipython3 [-h] [-a] [-b] [--foo FOO] [-c]
If you used action='store_true' instead of 'count' for your -a and -b, you could simplify the if tree to:
if args.a:
action = 'a value'
elif args.b:
action = 'another value'
else:
parser.print_usage()
sys.exit(1)

I used hapulj's answer, but still ended up with an if statement to check if neither was set. Then I found the ArgumentParser.add_mutually_exclusive_group() function, and ended up with this, which works perfectly.
import argparse
parser = argparse.ArgumentParser()
actiongroup = parser.add_mutually_exclusive_group(required=True)
actiongroup.add_argument('-a', action='store_const', dest='action', const='a value')
actiongroup.add_argument('-b', action='store_const', dest='action', const='another value')
parser.add_argument('--foo')
....
args = parser.parse_args()
Now, arguments -a and -b can't be omitted, and both can't be specified at the same time.

Related

argparse: How to map choices to different values?

When using argparse, I would like a user to select from choices, but I would like their choice to determine a more complex value (similar to how store_const works).
For example, when choosing from a smoker status of ['current', 'former', 'never'], I would like current to map to 'Current every day smoker.'
Is there an elegant way to do this?
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--smoker', choices=['current','former','never'])
print(vars(parser.parse_args()))
Normal output:
$ ./script.py --smoker current
{'smoker': 'current'}
Desired:
$ ./script.py --smoker current
{'smoker': 'Current every day smoker.'}
I thought I could do this with a lambda type argument, but argparse enforces the list of choices:
choice_dict = {'current': 'Current everyday...'}
parser.add_argument('--smoker', type=lambda x: choice_dict[x], choices=choice_dict.keys())
print(vars(parser.parse_args()))
./script.py --smoker current
usage: script.py [-h] [--smoker {current}]
script.py: error: argument --smoker: invalid choice: 'Current everyday...' (choose from 'current')
Though I agree with 0x5452 comment that it is better to decouple this formatting from the parser, you can use an Action to do what you want:
import argparse
CONVERSION_TABLE = {'current': 'Currently smokes',
'former': 'Used to smoke',
'never': 'Never smoke'}
class RenameOption(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, CONVERSION_TABLE[values])
parser = argparse.ArgumentParser()
parser.add_argument('--smoker', action=RenameOption, choices=CONVERSION_TABLE)
print(vars(parser.parse_args()))
The result is:
$ python test.py --smoker current
{'smoker': 'Currently smokes'}
$ python test.py --smoker no
usage: test.py [-h] [--smoker {current,former,never}]
test.py: error: argument --smoker: invalid choice: 'no' (choose from 'current', 'former', 'never')

argparse -- requiring either 2 values or none for an optional argument

I'm trying to make an optional argument for a script that can either take no values or 2 values, nothing else. Can you accomplish this using argparse?
# desired output:
# ./script.py -a --> works
# ./script.py -a val1 --> error
# ./script.py -a val1 val2 --> works
version 1 -- accepts 0 or 1 values:
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--action", nargs="?", const=True, action="store", help="do some action")
args = parser.parse_args()
# output:
# ./script.py -a --> works
# ./script.py -a val1 --> works
# ./script.py -a val1 val2 --> error
version 2 - accepts exactly 2 values:
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--action", nargs=2, action="store", help="do some action")
args = parser.parse_args()
# output:
# ./script.py -a --> error
# ./script.py -a val1 --> error
# ./script.py -a val1 val2 --> works
How do you combine these 2 different versions so that the script accepts 0 or 2 values for the argument, but rejects it when it only has 1 value?
You'll have to do your own error checking here. Accept 0 or more value, and reject anything other than 0 or 2:
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--action", nargs='*', action="store", help="do some action")
args = parser.parse_args()
if args.action is not None and len(args.action) not in (0, 2):
parser.error('Either give no values for action, or two, not {}.'.format(len(args.action)))
Note that args.action is set to None when no -a switch was used:
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("-a", "--action", nargs='*', action="store", help="do some action")
_StoreAction(option_strings=['-a', '--action'], dest='action', nargs='*', const=None, default=None, type=None, choices=None, help='do some action', metavar=None)
>>> args = parser.parse_args([])
>>> args.action is None
True
>>> args = parser.parse_args(['-a'])
>>> args.action
[]
Just handle that case yourself:
parser.add_argument("-a", "--action", nargs='*', action="store", help="do some action")
args = parser.parse_args()
if args.action is not None:
if len(args.action) not in (0, 2):
parser.error('Specify no or two actions')
# action was specified but either there were two actions or no action
else:
# action was not specified
Of course you should update the help text in that case so that the user has a chance to know this before running into the error.
Have your option take a single, optional comma-separated string. You'll use a custom type to convert that string to a list and verify that it has exactly two items.
def pair(value):
rv = value.split(',')
if len(rv) != 2:
raise argparse.ArgumentParser()
return rv
parser.add_argument("-a", "--action", nargs='?',
type=pair, metavar='val1,val2',
help="do some action")
print parser.parse_args()
Then you'd use it like
$ ./script.py -a
Namespace(action=None)
$ ./script.py -a val1,val2
Namespace(action=['val1','val2'])
$ ./script.py -a val1
usage: tmp.py [-h] [-a [ACTION]]
script.py: error: argument -a/--action: invalid pair value: 'val1'
$ ./script.py -a val1,val2,val3
usage: tmp.py [-h] [-a [ACTION]]
script.py: error: argument -a/--action: invalid pair value: 'val1,val2,val3'
You can adjust the definition of pair to use a different separator and to return something other than a list (a tuple, for example).
The metavar provides a better indication that the argument to action is a pair of values, rather than just one.
$ ./script.py -h
usage: script.py [-h] [-a [val1,val2]]
optional arguments:
-h, --help show this help message and exit
-a [val1,val2], --action [val1,val2]
do some action
How about the required argument:parser.add_argument("-a", "--action", nargs=2, action="store", help="do some action", required=False)

Python argparse value range help message appearance

I have an argument for a program that is an integer from 1-100 and I just don't like the way that it shows up in the -h help message when using argparse (it literally lists 0, 1, 2, 3, 4, 5,... etc)
Is there any way to change this or have it represented in another way?
Thanks
EDIT:
Here is the code for those who asked:
norse = parser.add_argument_group('Norse')
norse.add_argument('-n', '--norse', required=False, help='Run the Norse IPViking scan.', action='store_true')
norse.add_argument('--threshold', required=False, type=int, choices=range(0,101), help='Threshold (0-100) denoting at what threat level to provide additional data on an IP \
address. Default is 49.', default=49)
Use the metavar parameter of add_argument().
For example:
norse = parser.add_argument_group('Norse')
norse.add_argument('-n', '--norse', required=False, help='Run the Norse IPViking scan.', action='store_true')
norse.add_argument('--threshold', required=False, type=int, choices=range(0,101),
metavar="[0-100]",
help='Threshold (0-100) denoting at what threat level to provide additional data on an IP \
address. Default is 49.', default=49)
Test:
from argparse import ArgumentParser
norse = ArgumentParser()
norse.add_argument('-n', '--norse', required=False, help='Run the Norse IPViking scan.', action='store_true')
norse.add_argument('--threshold', required=False, type=int, choices=range(0,101), metavar="[0-100]", help='Threshold (0-100) denoting at what threat level to provide additional data on an IP address. Default is 49.', default=49)
norse.print_help()
Results
usage: -c [-h] [-n] [--threshold [0-100]]
optional arguments:
-h, --help show this help message and exit
-n, --norse Run the Norse IPViking scan.
--threshold [0-100] Threshold (0-100) denoting at what threat level to
provide additional data on an IP address. Default is
49.
You can customize action, e.g:
#!/usr/bin/env python
import argparse
class Range(argparse.Action):
def __init__(self, minimum=None, maximum=None, *args, **kwargs):
self.min = minimum
self.max = maximum
kwargs["metavar"] = "[%d-%d]" % (self.min, self.max)
super(Range, self).__init__(*args, **kwargs)
def __call__(self, parser, namespace, value, option_string=None):
if not (self.min <= value <= self.max):
msg = 'invalid choice: %r (choose from [%d-%d])' % \
(value, self.min, self.max)
raise argparse.ArgumentError(self, msg)
setattr(namespace, self.dest, value)
norse = argparse.ArgumentParser('Norse')
norse.add_argument('--threshold', required=False, type=int, min=0, max=100,
action=Range,
help='Threshold [%(min)d-%(max)d] denoting at what threat \
level to provide additional data on an IP address. \
Default is %(default)s.', default=49)
args = norse.parse_args()
print args
Test it:
~: user$ ./test.py --threshold 10
Namespace(threshold=10)
~: user$ ./test.py --threshold -1
usage: Norse [-h] [--threshold [0-100]]
Norse: error: argument --threshold: invalid choice: -1 (choose from [0-100])
~: user$ ./test.py -h
usage: Norse [-h] [--threshold [0-100]]
optional arguments:
-h, --help show this help message and exit
--threshold [0-100] Threshold [0-100] denoting at what threat level to
provide additional data on an IP address. Default is
49.
With a custom type, it is easier to control the error message (via the ArgumentTypeError). I still need the metavar to control the usage display.
import argparse
def range_type(astr, min=0, max=101):
value = int(astr)
if min<= value <= max:
return value
else:
raise argparse.ArgumentTypeError('value not in range %s-%s'%(min,max))
parser = argparse.ArgumentParser()
norse = parser.add_argument_group('Norse')
...
norse.add_argument('--range', type=range_type,
help='Value in range: Default is %(default)s.',
default=49, metavar='[0-101]')
parser.print_help()
print parser.parse_args()
producing:
2244:~/mypy$ python2.7 stack25295487.py --ran 102
usage: stack25295487.py [-h] [-n] [--threshold [0:101]] [--range [0-101]]
optional arguments:
-h, --help show this help message and exit
Norse:
...
--range [0-101] Value in range: Default is 49.
usage: stack25295487.py [-h] [-n] [--threshold [0:101]] [--range [0-101]]
stack25295487.py: error: argument --range: value not in range 0-101
I could use functools.partial to customize the range values:
type=partial(range_type, min=10, max=90)
Here are a couple ways you can do it instead
def parseCommandArgs():
parser = argparse.ArgumentParser()
parser.add_argument('-i', dest='myDest', choices=range(1,101), type=int, required=True, metavar='INT[1,100]', help='my help message')
return parser.parse_args()
You can also use action instead, which I highly recommend since it allows more customization
def verify():
class Validity(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if values < 1 or values > 100:
# do something
pass
return Validity
def parseCommandArgs():
parser = argparse.ArgumentParser()
parser.add_argument('-i', dest='myDest', required=True, metavar='INT[1,100]', help='my help message', action=verify())
return parser.parse_args()
I have written a library for this: argparse-range
>>> from argparse import ArgumentParser
>>> from argparse_range import range_action
>>> parser = ArgumentParser()
>>> parser.add_argument("rangedarg", action=range_action(0, 10), help="An argument")
>>> args = parser.parse_args(["0"])
>>> args.rangedarg
0
>>> parser.parse_args(["20"])
Traceback (most recent call last):
....
argparse.ArgumentTypeError: Invalid choice: 20 (must be in range 0..=10)
Helptext is added transparently:
foo.py --help
usage: foo.py [-h] rangedarg
positional arguments:
rangedarg An argument (must be in range 0..=10)
optional arguments:
-h, --help show this help message and exit
Other features include:
Handles default, metavar, and nargs consistently with default argparse handling
Type inference, with explicit overrides using the type argument

Verbose level with argparse and multiple -v options

I'd like to be able to specify different verbose level, by adding more -v options to the command line. For example:
$ myprogram.py
$ myprogram.py -v
$ myprogram.py -vv
$ myprogram.py -v -v -v
would lead to verbose=0, verbose=1, verbose=2, and verbose=3 respectively. How can I achieve that using argparse?
Optionally, it could be great to also be able to specify it like
$ myprogram -v 2
argparse supports action='count':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='count', default=0)
for c in ['', '-v', '-v -v', '-vv', '-vv -v', '-v -v --verbose -vvvv']:
print(parser.parse_args(c.split()))
Output:
Namespace(verbose=0)
Namespace(verbose=1)
Namespace(verbose=2)
Namespace(verbose=2)
Namespace(verbose=3)
Namespace(verbose=7)
The only very minor niggle is you have to explicitly set default=0 if you want no -v arguments to give you a verbosity level of 0 rather than None.
You could do this with nargs='?' (to accept 0 or 1 arguments after the -v flag) and a custom action (to process the 0 or 1 arguments):
import sys
import argparse
class VAction(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, const=None,
default=None, type=None, choices=None, required=False,
help=None, metavar=None):
super(VAction, self).__init__(option_strings, dest, nargs, const,
default, type, choices, required,
help, metavar)
self.values = 0
def __call__(self, parser, args, values, option_string=None):
# print('values: {v!r}'.format(v=values))
if values is None:
self.values += 1
else:
try:
self.values = int(values)
except ValueError:
self.values = values.count('v')+1
setattr(args, self.dest, self.values)
# test from the command line
parser = argparse.ArgumentParser()
parser.add_argument('-v', nargs='?', action=VAction, dest='verbose')
args = parser.parse_args()
print('{} --> {}'.format(sys.argv[1:], args))
print('-'*80)
for test in ['-v', '-v -v', '-v -v -v', '-vv', '-vvv', '-v 2']:
parser = argparse.ArgumentParser()
parser.add_argument('-v', nargs='?', action=VAction, dest='verbose')
args=parser.parse_args([test])
print('{:10} --> {}'.format(test, args))
Running script.py -v -v from the command line yields
['-v', '-v'] --> Namespace(verbose=2)
--------------------------------------------------------------------------------
-v --> Namespace(verbose=1)
-v -v --> Namespace(verbose=2)
-v -v -v --> Namespace(verbose=3)
-vv --> Namespace(verbose=2)
-vvv --> Namespace(verbose=3)
-v 2 --> Namespace(verbose=2)
Uncomment the print statement to see better what the VAction is doing.
You could handle the first part of your question with append_const. Otherwise, you're probably stuck writing a custom action, as suggested in the fine answer by unutbu.
import argparse
ap = argparse.ArgumentParser()
ap.add_argument('-v', action = 'append_const', const = 1)
for c in ['', '-v', '-v -v', '-vv', '-vv -v']:
opt = ap.parse_args(c.split())
opt.v = 0 if opt.v is None else sum(opt.v)
print opt
Output:
Namespace(v=0)
Namespace(v=1)
Namespace(v=2)
Namespace(v=2)
Namespace(v=3)
Here's my take on this that doesn't use any new classes, works in both Python 2 and 3 and supports relative adjustments from the default using "-v"/"--verbose" and "-q"/"--quiet", but it doesn't support using numbers e.g. "-v 2":
#!/usr/bin/env python
import argparse
import logging
import sys
LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
DEFAULT_LOG_LEVEL = "INFO"
def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument(
"--verbose", "-v",
dest="log_level",
action="append_const",
const=-1,
)
parser.add_argument(
"--quiet", "-q",
dest="log_level",
action="append_const",
const=1,
)
args = parser.parse_args(argv[1:])
log_level = LOG_LEVELS.index(DEFAULT_LOG_LEVEL)
# For each "-q" and "-v" flag, adjust the logging verbosity accordingly
# making sure to clamp off the value from 0 to 4, inclusive of both
for adjustment in args.log_level or ():
log_level = min(len(LOG_LEVELS) - 1, max(log_level + adjustment, 0))
log_level_name = LOG_LEVELS[log_level]
print(log_level_name)
logging.getLogger().setLevel(log_level_name)
if __name__ == "__main__":
main(sys.argv)
Example:
$ python2 verbosity.py -vvv
DEBUG
$ python3 verbosity.py -vvv -q
INFO
$ python2 verbosity.py -qqq -vvv -q
WARNING
$ python2 verbosity.py -qqq
CRITICAL
Expanding on unutbu's answer, here's a custom action including handling of a --quiet/-q combination. This is tested in Python3. Using it in Python >=2.7 should be no big deal.
class ActionVerbose(argparse.Action):
def __call__(self, parser, args, values, option_string=None):
#print(parser, args, values, option_string)
# Obtain previously set value in case this option call is incr/decr only
if args.verbose == None:
base = 0
else:
base = args.verbose
# One incr/decr is determined in name of option in use (--quiet/-q/-v/--verbose)
option_string = option_string.lstrip('-')
if option_string[0] == 'q':
incr = -1
elif option_string[0] == 'v':
incr = 1
else:
raise argparse.ArgumentError(self,
'Option string for verbosity must start with v(erbose) or q(uiet)')
# Determine if option only or values provided
if values==None:
values = base + incr
else:
# Values might be an absolute integer verbosity level or more 'q'/'v' combinations
try:
values = int(values)
except ValueError:
values = values.lower()
if not re.match('^[vq]+$', values):
raise argparse.ArgumentError(self,
"Option string for -v/-q must contain only further 'v'/'q' letters")
values = base + incr + values.count('v') - values.count('q')
setattr(args, self.dest, values)
#classmethod
def add_to_parser(cls,
parser, dest='verbose', default=0,
help_detail='(0:errors, 1:info, 2:debug)'):
parser.add_argument('--verbose', nargs='?', action=ActionVerbose, dest=dest, metavar='level',
default=default,
help='Increase or set level of verbosity {}'.format(help_detail))
parser.add_argument('-v', nargs='?', action=ActionVerbose, dest=dest, metavar='level',
help='Increase or set level of verbosity')
parser.add_argument('--quiet', nargs='?', action=ActionVerbose, dest=dest, metavar='level',
help='Decrease or set level of verbosity')
parser.add_argument('-q', nargs='?', action=ActionVerbose, dest=dest, metavar='level',
help='Decrease or set level of verbosity')
There's a convenience class method which can be used to set up all four option handlers for --verbose, -v, -q, --quiet. Use it like this:
parser = argparse.ArgumentParser()
ActionVerbose.add_to_parser(parser, default=defaults['verbose'])
# add more arguments here with: parser.add_argument(...)
args = parser.parse_args()
When using a script having these arguments you can do:
./script -vvvvvv -v 4 -v 0 -v -vvv --verbose --quiet 2 -v qqvvqvv
With this command line args.verbose would be 4.
Any -v/-q/--verbose/--quiet with a given number is a hard, absolute set of args.verbose to that given number (=verbosity level).
Any -v/--verbose without a number is an increment of that level.
Any -q/--quiet without a number is a decrement of that level.
Any -v/-q may immediately be followed up with more v/q letters, the resulting level is the old level + sum(count('v')) - sum(count('q'))
Overall default is 0
The custom action should be fairly easy to modify in case you want a different behaviour. For example, some people prefer that any --quiet resets the level to 0, or even to -1. For this, dremove the nargs from the add_argument of -q and --quiet, and also hardcode to set value = 0 if option_string[0] == 'q'.
Proper parser errors are nicely printed if usage is wrong:
./script -vvvvvv -v 4 -v 0 -v -vvv --verbose --quiet 2 -v qqvvqvav
usage: script [-h] [--verbose [level]]
[-v [level]] [--quiet [level]] [-q [level]]
script: error: argument -v: Option string for -v/-q must contain only further 'v'/'q' letters
argparse supports the append action which lets you specify multiple arguments. Check http://docs.python.org/library/argparse.html, search for "append".
Your first proposed method would be more likely to confuse. Different option names for different levels of verbosity, or one verbose flag optionally followed by a numeric indicator of the level of verbosity is less likely to confuse a user and would allow more flexibility in assigning verbosity levels.
I've come up with an alternative; while it doesn't exactly match OP's request, it fulfilled my requirements and I thought it worth sharing.
Use a mutually exclusive group to either count the number of short options or store the integer value of a long option.
import argparse
parser = argparse.ArgumentParser()
verbosity_group = parser.add_mutually_exclusive_group()
verbosity_group.add_argument(
'-v',
action='count',
dest='verbosity',
help='Turn on verbose output. Use more to turn up the verbosity level'
)
verbosity_group.add_argument(
'--verbose',
action='store',
type=int,
metavar='N',
dest='verbosity',
help='Set verbosity level to `N`'
)
parser.set_defaults(
verbosity=0
)
parser.parse_args()
parser.parse_args([])
# Namespace(verbosity=0)
parser.parse_args(['-v', '-vv'])
# Namespace(verbosity=3)
parser.parse_args(['--verbose=4'])
# Namespace(verbosity=4)
parser.parse_args(['--verbose'])
# error: argument --verbose: expected one argument
As you can see, it allows you to "stack" single char options and allows you to use the long option name to set the value explicitly. The downside is that you cannot use the long option as a switch (the last example generates an exception.)

Control formatting of the argparse help argument list?

import argparse
parser = argparse.ArgumentParser(prog='tool')
args = [('-u', '--upf', 'ref. upf', dict(required='True')),
('-s', '--skew', 'ref. skew', {}),
('-m', '--model', 'ref. model', {})]
for args1, args2, desc, options in args:
parser.add_argument(args1, args2, help=desc, **options)
parser.print_help()
Output:
usage: capcheck [-h] -u UPF [-s SKEW] [-m MODEL]
optional arguments:
-h, --help show this help message and exit
-u UPF, --upf UPF ref. upf
-s SKEW, --skew SKEW ref. skew
-m MODEL, --model MODEL
ref. model
How do I print ref. model in the same line as -m MODEL, --model MODEL instead of that appearing on a separate line when I run the script with -h option?
You could supply formatter_class argument:
parser = argparse.ArgumentParser(prog='tool',
formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=27))
args = [('-u', '--upf', 'ref. upf', dict(required='True')),
('-s', '--skew', 'ref. skew', {}),
('-m', '--model', 'ref. model', {})]
for args1, args2, desc, options in args:
parser.add_argument(args1, args2, help=desc, **options)
parser.print_help()
Note: Implementation of argparse.HelpFormatter is private only the name is public. Therefore the code might stop working in future versions of argparse. File a feature request to provide a public interface for the customization of max_help_position on http://bugs.python.org/
Output
usage: tool [-h] -u UPF [-s SKEW] [-m MODEL]
optional arguments:
-h, --help show this help message and exit
-u UPF, --upf UPF ref. upf
-s SKEW, --skew SKEW ref. skew
-m MODEL, --model MODEL ref. model
Inspired by #jfs's answer, I have come up with this solution:
def make_wide(formatter, w=120, h=36):
"""Return a wider HelpFormatter, if possible."""
try:
# https://stackoverflow.com/a/5464440
# beware: "Only the name of this class is considered a public API."
kwargs = {'width': w, 'max_help_position': h}
formatter(None, **kwargs)
return lambda prog: formatter(prog, **kwargs)
except TypeError:
warnings.warn("argparse help formatter failed, falling back.")
return formatter
Having that, you can call it with any HelpFormatter that you like:
parser = argparse.ArgumentParser(
formatter_class=make_wide(argparse.ArgumentDefaultsHelpFormatter)
)
or
parser = argparse.ArgumentParser(
formatter_class=make_wide(argparse.HelpFormatter, w=140, h=20)
)
What this does is make sure that the wider formatter can actually be created using the width and max_help_position arguments. If the private API changes, that is noted by make_wide by a TypeError and the formatter is returned unchanged. That should make the code more reliable for deployed applications.
I'd welcome any suggestions to make this more pythonic.
If you are providing a custom formatter_class to your ArgumentParser
parser = argparse.ArgumentParser(formatter_class=help_formatter)
and then use subparsers, the formatter will only apply to the top-level help message. In order to use the same (or some other) formatter for all subparsers, you need to provide formatter_class argument for each add_parser call:
subparsers = parser.add_subparsers(metavar="ACTION", dest="action")
child_parser = subparsers.add_parser(
action_name, formatter_class=help_formatter
)
As the argparse library tries to use the COLUMNS environment variable to get the terminal width, we also can set this variable, and let argparse do its job.
import os
import argparse
rows, columns = os.popen('stty size', 'r').read().split()
os.environ["COLUMNS"] = str(columns)
parser = argparse.ArgumentParser(etc...
Tested and approved on RHEL/Python 2.7.5
Credits to https://stackoverflow.com/a/943921 for getting the real terminal width
Another approach: hijack sys.argv, check it for --help and -h, if found extract help text using argparse.format_help, massage it, print it, and exit.
import sys, re, argparse
RGX_MID_WS = re.compile(r'(\S)\s{2,}')
def main(argv):
# note add_help = False
parser = argparse.ArgumentParser(description = '%(prog)s: testing help mods', formatter_class= argparse.RawTextHelpFormatter, add_help = False)
parser.add_argument('bar', nargs='+', help='two bars that need to be frobbled')
parser.add_argument('--foo', action='store_true', help='foo the bars before frobbling\nfoo the bars before frobbling')
parser.add_argument('--xxxxx', nargs=2, help='many xes')
parser.add_argument('--bacon', help ='a striped food')
parser.add_argument('--badger', help='in a striped pyjamas')
parser.add_argument('--animal', dest='animal', choices=('zabra', 'donkey', 'bat') ,help ='could be one of these')
# may exit
lArgs = help_manage(parser)
args = parser.parse_args() # args = lArgs
print('bars are: ', args.bar)
def help_manage(parser):
"""
check for -h, --help, -h in a single-letter cluster;
if none found, return, otherwise clean up help text and exit
"""
lArgs = sys.argv[1:]
lArgsNoHelp = [sOpt for sOpt in lArgs if (not sOpt in ('--help', '-h')) and not (sOpt[0] == '-' and sOpt[1] != '-' and 'h' in sOpt)]
# no change? then no --help params
if len(lArgsNoHelp) == len(lArgs): return
sHelp = parser.format_help()
# to see help as formated by argparse, uncomment:
# print(sHelp)
# exit()
for sLine in sHelp.split('\n'): print(clean_line(sLine))
exit()
def clean_line(sLine):
"""
this is just an example, and goes nowhere near covering all possible
argument properties
"""
# avoid messing with usage: lines
if 'usage' in sLine: return sLine
if sLine.startswith(' ') and '[' in sLine: return sLine
if sLine.endswith(' arguments:'): return sLine + '\n'
sLine = sLine.lstrip()
sLine = RGX_MID_WS.sub(r'\1\n', sLine)
if sLine.startswith('-'): sLine = '\n' + sLine
return sLine.replace('{', '\n(can be: ').replace('}', ')').replace('\n\n', '\n')
if __name__ == '__main__':
bRes = main(sys.argv[1:])
sys.exit(bRes)
Help without formatting:
usage: argparse_fix_min2.py [--foo] [--xxxxx XXXXX XXXXX] [--bacon BACON]
[--badger BADGER] [--animal {zabra,donkey,bat}]
bar [bar ...]
argparse_fix_min2.py: testing help mods
positional arguments:
bar two bars that need to be frobbled
optional arguments:
--foo foo the bars before frobbling
foo the bars before frobbling
--xxxxx XXXXX XXXXX many xes
--bacon BACON a striped food
--badger BADGER in a striped pyjamas
--animal {zabra,donkey,bat}
could be one of these
with formatting:
usage: argparse_fix_min2.py [--foo] [--xxxxx XXXXX XXXXX] [--bacon BACON]
[--badger BADGER] [--animal {zabra,donkey,bat}]
bar [bar ...]
argparse_fix_min2.py: testing help mods
positional arguments:
bar
two bars that need to be frobbled
optional arguments:
--foo
foo the bars before frobbling
foo the bars before frobbling
--xxxxx XXXXX XXXXX
many xes
--bacon BACON
a striped food
--badger BADGER
in a striped pyjamas
--animal
(can be: zabra,donkey,bat)
could be one of these
"""

Categories