23

I am using arparse to update a config dict using values specified on the command line. Since i only want to update the values in the config for which a value was explicitly mentioned on the command line.

Therefore i try to identify not-specified arguments by checking for each action if getattr(args, action.dest) == action.default or equality of the type converted arg. Then i update all my values in the dict for which this is false.

But this of course fails, if i explicitly specify an argument on the command line which is the same as my default argument. Is there a possibility to identify these explicitly mentioned arguments withing argparser or do i have to identify them manually in sys.argv?

Thanks!

Edit:

To make my intentions clearer. I have an argument like the following:

parser.add_argument('--test', default='meaningful_default')

and a config like

config = { 'test' : 'nondefault_val'}

Now i want to update the config only with the explicitly specified arguments. Comparing the args attributes with the default values works as long as i don't specify something like prog.py --test meaningful_default to update my config again with a value which just happens to be also the default value

aem
  • 444
  • 1
  • 4
  • 12

5 Answers5

12

If you prefer to use argparse and to be able to specify default values, there is a simple solution using two parsers.

I. Define your main parser and parse all your arguments with proper defaults:

parser = argparse.ArgumentParser()
parser.add_argument('--test1', default='meaningful_default1')
parser.add_argument('--test2', default='meaningful_default2')
...
args, unparsed = parser.parse_known_args()

II. Define an aux parser with argument_default=argparse.SUPPRESS to exclude unspecified arguments. Add all the arguments from the main parser but without any defaults:

aux_parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
for arg in vars(args): aux_parser.add_argument('--'+arg)
cli_args, _ = aux_parser.parse_known_args()

This is not an extremely elegant solution, but works well with argparse and all its benefits.

MtDersvan
  • 497
  • 3
  • 11
  • 1
    this seems to be the only answer that actually gets close to answering the question. – אלימלך שרייבר Jan 20 '21 at 08:13
  • Thanks for this. I ended up using this solution for my needs. A small modification to this is if the argument's action is `action=store_true`, you would need to add `action=store_true` into the `add_argument` method in the loop. Otherwise, arguments that are of `action=store_true` are expected to have a second argument and it will crash. There's no easy way to check between `store_true` vs. `store_false`, but assuming all of these types of variables are one or the other, you can just put in an `if` statement in the loop to check if the value of the argument is `bool`, then add in the action. – rayryeng Mar 09 '22 at 21:21
  • The extra parser can be avoided by reusing `parser` and setting `parser.set_defaults(**{arg: None for arg in vars(args)})`. The command line args are then, after `aux_args = parser.parse_args()`, "just" `cmd_line_args = [arg for arg, value in vars(aux_args).items() if value is not None]`. – heiner Nov 23 '22 at 00:15
6

You can use custom action to tell if an arg value was defaulted or set on command line:

import argparse

class FooAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, values)
        setattr(namespace, self.dest+'_nondefault', True)

parser = argparse.ArgumentParser()
parser.add_argument('--test0', default='meaningful_default', action=FooAction)
parser.add_argument('--test1', default='meaningful_default', action=FooAction)

args = parser.parse_args('--test0 2'.split())

if hasattr(args, 'test0_nondefault'):
    print('argument test0 set on command line')
else:
    print('default value of test0 is used')

if hasattr(args, 'test1_nondefault'):
    print('argument test1 set on command line')
else:
    print('default value of test1 is used')
5

The parser maintains a seen_actions set object while parsing (in the _parse_known_args method). At the end of parsing it checks this set against the required arguments (ones with required=True), and may issue a error. A variation is also used with mutually exclusive groups.

But this variable is not available outside of that function. So short of some sort of 'hook' that lets you apply your own tests within the parse_args action, your best option is to test defaults. Or you can look at sys.argv[1:].

The default default is None. That is nice for this purpose because your user cannot give this value. That is, there's no string that converts to None (at least not in any of the normal type methods).

parser.add_argument('--foo') # default=None
...
if args.foo is None:
    # clearly foo has not been specified.
    args.foo = 'the real default'
else:
    # foo was specified
    pass
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • 6
    Since i rely on non-None defaults, this is not really an possibility. I am now using `any([arg.startswith(option) for arg in sys.argv[1:] for option in a.option_strings])` while looping over all actions. This seems to work. – aem Aug 18 '15 at 06:28
  • 4
    pretty much defeats the purpose of the `default=` parameter. – n611x007 Aug 18 '15 at 10:49
  • 1
    When you get down to the details, the handling of defaults is suprisingly complex. In this case, conveniently setting a default, and knowing whether the user gave a value (even the default one), come into conflict. – hpaulj Aug 18 '15 at 17:03
5

You can use combinations of "const" and "default" to emulate what you want. "nargs" has to be '?' in this case. For example:

args_group.add_argument('--abc',
                         metavar='',
                         nargs='?',
                         action='store',
                         const='github.com',
                         default='',
                         help='blabla [ default: %(const)s ]')

Then abc's value will be as follows given different command line arguments.

abc not in command line:

abc =

--abc:

abc = github.com

--abc stackoverflow.com:

abc = stackoverflow.com

So you will know abc doesn't appear in command line when it's blank, for example:

if not args.abc:
    # to something
Lohengrin
  • 239
  • 3
  • 7
0

One resolution is via unique objects. Instead of

parser.add_argument("--testdir", default="tests/")

we can do

testdir_default = "tests/"
parser.add_argument("--testdir", default=testdir_default)
args = parser.parse_args()

if args.testdir is testdir_default:
   ...
else: 
   ...

There is one caveat: Python caches certain commonly used values, in particular integers between -5 and 256.

x = 1
y = 1
x is y  # True
Hyperplane
  • 1,422
  • 1
  • 14
  • 28