Verbose level with argparse and multiple -v options

Question:

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
Asked By: Charles Brunet

||

Answers:

argparse supports the append action which lets you specify multiple arguments. Check http://docs.python.org/library/argparse.html, search for “append“.

Answered By: yan

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.

Answered By: unutbu

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)
Answered By: FMc

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.

Answered By: Paddy3118

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.

Answered By: Ben

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
Answered By: cfi

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
Answered By: Eric Pruitt

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.)

Answered By: Rowshi
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.