4

When using argparse, I've not found an elegant/DRY way to use the function/method defaults instead of the defaults passed by argparse.

For example, I have foreign code I am loathe to modify. How do I tell argparse (or elegantly handle after argparse) to use the function defaults if the user does not pass in a clear preference on the command-line?

import argparse

def foreign_func(fav_color="blue"):
    print(fav_color)

clparser = argparse.ArgumentParser()
clparser.add_argument(
    "--color",
    default=None,
    help="Enter color")
clargs = clparser.parse_args()

foreign_func(fav_color=clargs.color)

This will print 'None', instead of "blue"

My default approach is something like:

if clparser.color:
    foreign_func(fav_color=clargs.color)
else:
    foreign_func()

but this seems clunky, especially if there are multiple command-line options.

EDIT: Hi folks, thank you for the fast feedback. To clarify, although it would be nice for argparse to display 'blue' in it's help, that's not my goal.

I'm looking for:

my_prog --color=red
>>> red

my_prog 
>>> blue

With the code above, the program is outputting 'None', not "blue"

JS.
  • 14,781
  • 13
  • 63
  • 75
  • I think that, in order to achieve what you want in an elegant/DRY way, you necessarily have to inspect the foreign code, extract the default values, and pass them to your parser through an Enum, a class, or something like that. – crissal Jun 13 '22 at 18:18
  • I was thinking along the same lines as @crissal - e.g. set default values of argparse.ArgumentParser same as default values of foreign function. Check https://stackoverflow.com/q/12627118/4046632 – buran Jun 13 '22 at 18:21
  • 1
    Although it would be nice to display the foreign_func() defaults in the argparse help, that's not my main goal. My goal is to avoid writing a lot of boilerplate code to avoid stomping on the foreign_func() defaults. – JS. Jun 13 '22 at 18:22
  • 1
    Have you tried using the answers of https://stackoverflow.com/q/12627118/7084566 ? That is, using the inspect library? – Thomas Hilger Jun 13 '22 at 18:22

3 Answers3

3

You can use inspect to get the function's default parameter value, and use that as the default in the argparse code.

import argparse
import inspect

def foreign_func(fav_color="blue"):
    print(fav_color)

clparser = argparse.ArgumentParser()
clparser.add_argument(
    "--color",
    default=inspect.signature(foreign_func).parameters['fav_color'].default,
    help="Enter color")
clargs = clparser.parse_args()

foreign_func(fav_color=clargs.color)

Note that this will behave a bit oddly if there is no default in the function (if it's a required parameter). In that case, the default value will be the inspect._empty sentinel value, rather than an actually meaningful value. But your code seems to be assuming that the parameter is always optional, so I'm going with that too.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • 1
    I think it's strange to scrape the default, just so you can pass the default explicitly, when the whole point of having a default _at all_ is so that a value needn't be passed in. – wim Jun 13 '22 at 18:47
2

You can use a dictionary to specify your arguments by name; if a name is missing, that parameter will use its default. Example:

def func(a=1,b=2):
    print("a=",a,"b=",b)

func()
func(**{})
func(**{'a':10})

will print

a= 1 b= 2
a= 1 b= 2
a= 10 b= 2
Scott Hunter
  • 48,888
  • 12
  • 60
  • 101
  • Building on [this solution](https://www.adamsmith.haus/python/answers/how-to-convert-argparse.namespace()-entries-into-a-dictionary-in-python) it looks like this variation on your idea might work: `foreign_func(**vars(clargs))` – JS. Jun 13 '22 at 18:40
  • 1
    +1 This can be a good approach, when used in combination with [`argparse.SUPPRESS`](https://stackoverflow.com/a/53228802/674039), which simply omits binding the optional values in the args namespace entirely. To me it seems cleaner and more direct to just let the function supply the default value, rather than passing in again a value equal to the default value (which you had to scrape from the function in the first place). – wim Jun 13 '22 at 18:42
  • @wim One downside I'm finding is that the argparse option names must map directly to the function argument names. In the original example, the command-line option names must map exactly to the function names. – JS. Jun 13 '22 at 18:53
  • 2
    @JS. You can use the `dest` kwarg when adding an argument to help with that. It allows you to specify the name which binds in the namespace independently of the name used at the CLI. – wim Jun 13 '22 at 19:11
1

Combining answers/tips from @Scott Hunter, @wim, and Adam Smith, the following solution seems workable.

This solution hinges on converting the argparse NameSpace object to a dict, then "splitting" that dict before passing to foreign_func. Along with the argparse.SUPPRESS option, the dict passed in will have no entry for missing command-line arguments. This avoids "stomping" on foreign_funcs defaults.

Note: I have not tested this approach in the case that foreign_func also has positional arguments, so YMMV.

import argparse

def foreign_func(fav_color="blue"):
    print(fav_color)

clparser = argparse.ArgumentParser()

clparser.add_argument(
    "--color",
    # Use 'dest' option if your command-line option name differs
    # from the function's argument name
    dest="fav_color",
    # 'argparse.SUPPRESS' will add an entry into the clargs "dict"
    # only if the option is passed on the command-line.
    default=argparse.SUPPRESS,
    help="Enter color")

clargs = clparser.parse_args()

print("DBG:", vars(clargs))
foreign_func(**vars(clargs))

Testing:

python my_prog.py --color=red
>>> red

python my_prog.py 
>>> blue
JS.
  • 14,781
  • 13
  • 63
  • 75