1

I have a program in Python 3.8 which takes multiple different commands (e.g. mode), each of which have a different valid constellation of the same options (i.e., one option might be only valid in two commands, another in all of them, etc.) I therefore want to define each option (say --my-arg) once, as a argument dictionary (e.g. my_arg), and then pass it to multiple add_argument calls as appropriate for each command using **. But for some reason, the option_strings is being dropped in the call, and I don't know why.

Minimum reproducible example:

import argparse
parser = argparse.ArgumentParser('Test keyword arguments')
arg_parser = parser.add_subparsers(dest='what_to_run')
parser_mode = arg_parser.add_parser('mode')
my_arg = {'option_strings': ['--my-arg'], 'dest': 'my_arg',
             'help': 'My string argument goes here'}
parser_mode.add_argument(**my_arg)
parser.parse_args(['mode', '--my-arg', 'my value'])

This is the latter part of the output:

>>> parser_mode.add_argument(**my_arg)
_StoreAction(option_strings=[], dest='my_arg', nargs=None, const=None, default=None, type=None, choices=None, help='My string argument goes here', metavar=None)
>>> parser.parse_args(['mode', '--my-arg', 'my value'])
usage: Test keyword arguments [-h] {mode} ...
Test keyword arguments: error: unrecognized arguments: --my-arg

Note that option_strings is [], when ['--my-arg'] is expected.

If instead I replicate the option as a positional argument and remove it from the dictionary, it works:

import argparse
parser = argparse.ArgumentParser('Test keyword arguments')
arg_parser = parser.add_subparsers(dest='what_to_run')
parser_mode = arg_parser.add_parser('mode')
my_arg = {'dest': 'my_arg', 'help': 'My string argument goes here'}
parser_mode.add_argument('--my-arg', **my_arg)
parser.parse_args(['mode', '--my-arg', 'my value'])

Output:

>>> parser_mode.add_argument('--my-arg', **my_arg)
_StoreAction(option_strings=['--my-arg'], dest='my_arg', nargs=None, const=None, default=None, type=None, choices=None, help='My string argument goes here', metavar=None)
>>> parser.parse_args(['mode', '--my-arg', 'my value'])
Namespace(my_arg='my value', what_to_run='mode')

This is a tolerable fix, I suppose (I guess I could write a function to do this for me...), but I beyond wanting to eliminate duplication, I want to understand what's going on.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Vercingatorix
  • 1,838
  • 1
  • 13
  • 22
  • 3
    Because it's a positional argument, not a keyword argument. `add_argument` has no arguments named `option_strings`. – jonrsharpe Oct 29 '21 at 13:31
  • Can you clarify what exactly is confusing you? You need to pass the CLI arg as a positional – ``**my_arg`` does not (cannot!) do that. – MisterMiyagi Oct 29 '21 at 13:31
  • OK -- interesting. It accepts the keyword 'option_strings'; if I use something like 'name' or 'flags', it gives me an error when I invoke `add_argument`. – Vercingatorix Oct 29 '21 at 13:37
  • `_StoreAction` _does_ have a keyword argument named `option_strings`. The value you pass gets erased here: https://github.com/python/cpython/blob/2c8a0027e5555e371c1293f26b3262000b8cfe8a/Lib/argparse.py#L1527-L1528. – jonrsharpe Oct 29 '21 at 14:00
  • @jonrsharpe Niiiiice. Thanks for finding it for me. – Vercingatorix Oct 29 '21 at 14:05

1 Answers1

1

Apparently you can't get there from here. I am opting to use a helper function:

import argparse
def add_argument(parser, kwargs):
    return parser.add_argument(*(kwargs['option_strings']), **kwargs)

parser = argparse.ArgumentParser('Test keyword arguments')
arg_parser = parser.add_subparsers(dest='what_to_run')
parser_mode = arg_parser.add_parser('mode')
my_arg = {'option_strings': ['--my-arg'], 'dest': 'my_arg',
             'help': 'My string argument goes here'}
add_argument(parser_mode, my_arg)
parser.parse_args(['mode', '--my-arg', 'my value'])

Output:

>>> add_argument(parser_mode, my_arg)
_StoreAction(option_strings=['--my-arg'], dest='my_arg', nargs=None, const=None, default=None, type=None, choices=None, help='My string argument goes here', metavar=None)
>>> parser.parse_args(['mode', '--my-arg', 'my value'])
Namespace(my_arg='my value', what_to_run='mode')

Thanks to those who commented.

Vercingatorix
  • 1,838
  • 1
  • 13
  • 22