1

I am trying to create an argparse optional argument that may -OR- may not have an associated input value. I want the following behavior:
1. argument not specified, value=None
2. argument specified with a value, value=user_input
3. argument specified without a value, value=derived from a positional value

The first 2 are easy. It's the third one I can't figure out. I found 2 posts that do something similar:
python argparse optional positional argument with detectable switch
This one sets the default value to a constant for an optional argument without a value.
Argparse optional positional arguments?
This topic is close, but not quite what I need either (he derives the default value from a system call):
I want mine to be a determined from an positional value.

Simple example code I created:

parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('-c', '--csv', nargs='?')
parser.add_argument('-p', '--pnf', nargs='?')

When I set the input and print:

args = parser.parse_args('my.h5 -c my_file.csv --pnf'.split())
print ('Input = %s' % args.input)
print ('CSV file = %s' % args.csv)
print ('PNF file = %s' % args.pnf)

I get:

Input = my.h5
CSV file = my_file.csv
PNF file = None

If I modify my input to:

args = parser.parse_args('my.h5 -c'.split())

The resulting output is:

Input = my.h5
CSV file = None
PNF file = None

When Value = None, I can't tell if the optional argument was not defined, or was defined but without a value. In the second case, I want to derive the CSV File name from the positional argument (in this example the derived name would be my.csv). I want to do the same when --pnf is defined (where default PNF would be my.pnf for above). Is there a way to do this?

kcw78
  • 7,131
  • 3
  • 12
  • 44
  • Use `const='foobar'` as described in the link. After parsing conditionally replace 'foobar' with `args.input`. – hpaulj Nov 09 '18 at 16:10

4 Answers4

2

I can't tell if the optional argument was not defined, or was defined but without a value

If you create your parser like this:

parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)

Now it is possible to distinguish between the three cases.

  • If -c val was passed, it will be present in args with value "val".
  • If -c was passed without value, it will be present in args with value None.
  • If -c was omitted entirely, it won't be present in args.

The same goes for -p.

If you only want this suppression to apply for one option, rather than on the whole parser, that is possible too:

parser.add_argument('-c', '--csv', nargs='?', default=argparse.SUPPRESS)
wim
  • 338,267
  • 99
  • 616
  • 750
  • Thanks, works like a champ (with a small change to get the arguments and values) I saw that in the python.org docs, but couldn't decipher how it worked. Thanks! – kcw78 Nov 09 '18 at 18:37
0

Making use of the const parameter:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('-c', '--csv', nargs='?', const='foobar')
parser.add_argument('-p', '--pnf', nargs='?', const='foobar')

args = parser.parse_args()
print(args)

if args.csv and args.csv=='foobar':
    args.csv = args.input

args.pnf = args.input if (args.pnf and args.pnf=='foobar') else args.pnf

print(args)

Your two sample inputs:

0933:~/mypy$ python3 stack53228663.py my.h5 -c my_file.csv --pnf
Namespace(csv='my_file.csv', input='my.h5', pnf='foobar')
Namespace(csv='my_file.csv', input='my.h5', pnf='my.h5')

0933:~/mypy$ python3 stack53228663.py my.h5 -c
Namespace(csv='foobar', input='my.h5', pnf=None)
Namespace(csv='my.h5', input='my.h5', pnf=None)
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Thanks hpaulj, that's another approach. Not sure which is more "pythonic". Personally, I like `argparse.SUPPRESS` b/c it's more readable (to me). – kcw78 Nov 09 '18 at 18:47
  • You can also set this default in the `add_argument` command: `default=argparse.SUPPRESS`. In my version, `default` is `None`. And your 'const' is effectively `None`. Which is easier to test for, a `None` value or a missing attribute or key? Just as long as you are clear about the alternatives it really doesn't matter which you use. – hpaulj Nov 09 '18 at 19:29
  • "Which is easier: test a value or test for a missing key"? Yes, exactly, either way I'm writing some code to test and set the value. :-) In a perfect world, I'd like an action that combines action='store' and action='store_true'. When the optional argument is not entered, it's returned as 'False'. When it's entered without a value, it's returned as 'True'. And, when entered with a value, it's returned with that value -- or maybe returned as a tuple with: (argname, True/False, value). – kcw78 Nov 09 '18 at 20:36
  • 1
    That's easy - `nargs='?', default=False, const=True`. `store_true` is just `store_const` with those default and const values (and `nargs=0`). – hpaulj Nov 09 '18 at 20:41
  • Yeah, I like that one. Didn't realize I could use `default=` and `const=` together. Good to know! Test logic is very clean and readable. You should post as an answer so I can +1. Thanks! – kcw78 Nov 09 '18 at 21:25
0

For completeness (and future reference), I am posting the modified code required to get arguments when using argument_default=argparse.SUPPRESS. See below:

parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
parser.add_argument('input')
parser.add_argument('-c', '--csv', nargs='?')
parser.add_argument('-p', '--pnf', nargs='?')
args = parser.parse_args('my.h5 -c my_file --pnf'.split())
for d_key, d_val in vars(args).items() :
  print (d_key, d_val)

Results in this output:

input my.h5
csv my_file
pnf None

For the second set of inputs

args = parser.parse_args('my.h5 -c'.split())
print(vars(args))
for d_key, d_val in vars(args).items() :
  print (d_key, d_val)

Output looks like this:

input my.h5
csv None
kcw78
  • 7,131
  • 3
  • 12
  • 44
0

This shows the logic I implemented using default=False, const=True, based on earlier comments from @hpaulj.

parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('-c', '--csv', nargs='?', default=False, const=True)
parser.add_argument('-p', '--pnf', nargs='?', default=False, const=True)
args = parser.parse_args('my.h5 -c'.split())
print(vars(args))
HDF5_FILE = args.input
if isinstance(args.csv, str) :
  CSV_FILE = args.csv
elif args.csv :
  CSV_FILE=HDF5_FILE[:-3] + '_v3_stress.csv'
else :
  CSV_FILE = ''
# repeat above logic for args.pnf
print ('input=', HDF5_FILE, ',  csv=', CSV_FILE, ' pnf=', PNF_FILE )

Resulting output looks like this:

{'input': 'my.h5', 'csv': True, 'pnf': False}
hdf5= my.h5 ,  csv= my_v3_stress.csv  pnf= 

Modified 'parse_args()' and resulting output:

args = parser.parse_args('my.h5 -c my_file.csv -p'.split())

Gives:

{'input': 'my.h5', 'csv': 'my_file.csv', 'pnf': True}
hdf5= my.h5 ,  csv= my_file.csv  pnf= my_v3_stress.nrf

If I had a lot of variables to check, I would move the if/elif/else logic to a def that returns the desired value.

kcw78
  • 7,131
  • 3
  • 12
  • 44