Search code examples
pythondictionaryargparse

python ArgParse custom action with key value options using metavar causes duplicates in -h output


I'm using a custom action for argument parser to allow key=value options for specific arguments.

test.py --arg2 input1=something input2=something_else

The custom action works fine, but when I use metavar to list all of the custom options, I get duplicates.

Here is my custom action:

class KeyValue(argparse.Action):
def __call__(self, parser, namespace,
             values, option_string=None):
    setattr(namespace, self.dest, dict())

    for value in values:
        # split it into key and value
        key, value = value.split('=')
        # assign into dictionary
        getattr(namespace, self.dest)[key] = value

Here is the utilization:

    sub_1.add_argument(
        "--arg2",
        action=KeyValue,
        nargs="*",
        default=NoArgumentProvided(),
        help='arg 2 help',
        metavar="key1=val1 key2=val2"
    )

What the help looks like, with duplicates showing up:

usage: arg_issue.py test [-h] [--arg1]
                     [--arg2 [key1=val1 key2=val2 [key1=val1 key2=val2 ...]]]

test description

optional arguments:
  -h, --help            show this help message and exit
  --arg1                arg1 help
  --arg2 [key1=val1 key2=val2 [key1=val1 key2=val2 ...]]
                        arg 2 help

Any ideas why I'm getting duplicates? It's clearly from the custom Action, but I'm not sure why?

Full Code:

import argparse


class NoArgumentProvided(object):
    pass


class KeyValue(argparse.Action):
    def __call__(self, parser, namespace,
                 values, option_string=None):
        setattr(namespace, self.dest, dict())

        for value in values:
            # split it into key and value
            key, value = value.split('=')
            # assign into dictionary
            getattr(namespace, self.dest)[key] = value


def main():
    parser = argparse.ArgumentParser(
        description='parser',
        formatter_class=argparse.RawTextHelpFormatter,
    )

    # top-level args.
    parser.add_argument('--verbose',
                        help='Verbose mode',
                        action='store_true',
                        required=False,
                        default=NoArgumentProvided())
    # Add the main sub parsers
    subparsers = parser.add_subparsers(dest='action')

    sub_1 = subparsers.add_parser(
        'test',
        help='test help',
        description='test description')

    sub_1.add_argument(
        "--arg1",
        action='store_true',
        required=False,
        default=NoArgumentProvided(),
        help='arg1 help'
    )

    sub_1.add_argument(
        "--arg2",
        action=KeyValue,
        nargs="*",
        default=NoArgumentProvided(),
        help='arg 2 help',
        metavar="key1=val1 key2=val2"
    )

    subparsers.required = True

    args = parser.parse_args()


if __name__=="__main__":
    main()

Solution

  • If the metavar is a tuple of strings

    p.add_argument('--foo', nargs='*', metavar=('one','two'))
    

    the help will be

    usage: ipython3 [-h] [--foo [one [two ...]]]
    
    optional arguments:
      -h, --help            show this help message and exit
      --foo [one [two ...]]
    

    With nargs='*', the usage is 2 parts, [one [two ...]]. Use of a custom action class doesn't change that display.

    Keep the metavar simple, and elaborate in the help as needed. description can also add details.

    edit

    A patch a couple of years ago has simplified the * help.

    https://bugs.python.org/issue38438 argparse "usage" overly-complex with nargs="*"

    With Python 3.10

    In [218]: import argparse310 as argparse
    In [219]: p=argparse.ArgumentParser()
    In [220]: p.add_argument('--foo',nargs='*');
    In [221]: p.print_help()
    usage: ipython3 [-h] [--foo [FOO ...]]
    
    optional arguments:
      -h, --help       show this help message and exit
      --foo [FOO ...]
    

    The tuple version is still available:

    In [226]: p._actions[1].metavar=('one','two')
    In [227]: p.print_help()
    usage: ipython3 [-h] [--foo [one [two ...]]]
    
    optional arguments:
      -h, --help            show this help message and exit
      --foo [one [two ...]]