Barry McNamara
Barry McNamara

Reputation: 749

Argparse: option taking one or two arguments

I want to have an option which takes one or two arguments, specifically a timestamp and an optional tolerance.

I understand that I should just use nargs='+' and error out if I get more than two values, which is what I am doing.

I am also using metavar=('timestamp', 'tolerance') so the two values can be named.

However, the help message still looks like this:
usage: foo.py [-h] [-t timestamp [tolerance ...]]
Which incorrectly implies that -t can take more than two arguments.

How can I get it to just say [-t timestamp [tolerance]]? My actual code is below:

import argparse
import sys

parser = argparse.ArgumentParser()
parser.add_argument('-t', '--timestamp', nargs='+', metavar=('timestamp', 'tolerance'))
args = parser.parse_args()
if args.timestamp and len(args.timestamp) > 2:
    sys.exit('Argument --timestamp takes one or two values')

Upvotes: 8

Views: 4157

Answers (2)

Hai Vu
Hai Vu

Reputation: 40773

My approach is to create a function which split the value. If there are more than 2 values, or less than 1, throw a ValueError so argparse can complain in my behalf.

I also use a name space to name the stamp and tolerance, making it easy for the caller.

#!/usr/bin/env python3
import argparse


def stamp_and_tolerance(value=None):
    if value is None:
        return argparse.Namespace(stamp=None, tolerance=None)

    tokens = value.split(",")
    if len(tokens) == 1:
        return argparse.Namespace(stamp=tokens[0], tolerance=None)
    elif len(tokens) == 2:
        return argparse.Namespace(stamp=tokens[0], tolerance=tokens[1])

    raise ValueError()


parser = argparse.ArgumentParser()
parser.add_argument(
    "-t",
    "--timestamp",
    type=stamp_and_tolerance,
    default=stamp_and_tolerance(),
    metavar="stamp[,tolerance]",
    help="Timestamp and optional tolerance",
)
args = parser.parse_args()

print(f"Stamp: {args.timestamp.stamp}")
print(f"Tolerance: {args.timestamp.tolerance}")

Sample runs

./my.py -h
usage: my.py [-h] [-t stamp[,tolerance]]

options:
  -h, --help            show this help message and exit
  -t stamp[,tolerance], --timestamp stamp[,tolerance]
                        Timestamp and optional tolerance

./my.py
Stamp: None
Tolerance: None

./my.py -t mystamp
Stamp: mystamp
Tolerance: None

./my.py -t mystamp,mytolerance
Stamp: mystamp
Tolerance: mytolerance

Notes

  • I use comma as the separator, it can be almost anything
  • The metavar correctly communicates what the argument looks like
  • I set the default so that I don't have to bother with if args.timestamp != None
  • I use SimpleNamespace so the arguments are named, making it nicer to access

Upvotes: 0

curob
curob

Reputation: 745

I realize I am really late to the party on this, but I had to accomplish the same thing for a work project. Below is a much simplified version of what I did.

FULL DISCLOSURE: This is obviously hacky as it relies on a private function; this was the only way I saw to do this as there does not appear to be built in support. My solution was part of an application that was packaged to include the specific version of python (3.x) that I needed (as I access a private API to accomplish this) and my project has significant automated testing to catch any breakage in the future. You have been warned.

import argparse
import re as _re

class CustomParser(argparse.ArgumentParser):

    def _match_argument(self, action, arg_strings_pattern):
        if action.dest == 'name':
            # Account for flexible number of arguments. The pattern is copied from the parent class'
            # _get_nargs_pattern() function. 
            narg_pattern = '(-*A{1,2})'
            match = _re.match(narg_pattern , arg_strings_pattern)

            if match:
                return len(match.group(1))
            else:
                raise argparse.ArgumentError(action, "expected {} or {} arguments".format(1, 2))
        else:
            return super()._match_argument(action, arg_strings_pattern)

if __name__ == '__main__':
    parser = CustomParser("Flexible argument number test")
    # nargs must be 2 so that the help output properly formats the metavar argument.
    # Notice that I added '[]' around the optional argument to be consistent with argparse.
    parser.add_argument("--name", nargs=2, metavar=("FIRST", "[LAST]"),
                        help="Your name: FIRST LAST. The last name is optional.")

    args = parser.parse_args()
    print(args)

Example output:

SCRIPT --help
usage: Flexible argument number test [-h] [--name FIRST [LAST]]

optional arguments:
  -h, --help           show this help message and exit
  --name FIRST [LAST]  Your name: FIRST LAST. The last name is optional.

SCRIPT --name John
Namespace(name=['John'])

SCRIPT --name John Smith
Namespace(name=['John', 'Smith'])

SCRIPT --name
usage: Flexible argument number test [-h] [--name FIRST [LAST]]
Flexible argument number test: error: argument --name: expected 1 or 2 arguments

Obviously there are things that you would probably want to address such as:

  • Parameterizing the name of the option that you are looking for so that the 'name' value is not duplicated
  • Have a dictionary of arguments for which the number of arguments can vary and lookup the parameter instead of having the if-statement that only works for a single argument.

Upvotes: 0

Related Questions