问题
I have a small python script, which uses argparse
to let the user define options. It uses two flags for different modes and an argument to let the user define a file. See the simplified example below:
#!/usr/bin/python3
import argparse
from shutil import copyfile
def check_file(f):
# Mock function: checks if file exists, else "argparse.ArgumentTypeError("file not found")"
return f
def main():
aFile = "/tmp/afile.txt"
parser = argparse.ArgumentParser(description="An example",formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("-f", "--file", help="A file, used with method A.", default=aFile, type=check_file)
parser.add_argument("-a", "--ay", help="Method A, requires file.", action='store_true')
parser.add_argument("-b", "--be", help="Method B, no file required.", action='store_true')
args = parser.parse_args()
f = args.file
a = args.ay
b = args.be
if a:
copyfile(f, f+".a")
elif b:
print("Method B")
if __name__ == "__main__":
main()
Method A requires the file.
Method B does not.
If I run the script with method A, I either use the default file or one that is defined with -f
/--file
. The script checks if the file exists and everything is fine.
Now, if I run the script with method B, it shouldn't require the file, but the default option is checked and if it doesn't exist the argparse function raises the exception and the script exits.
How can I configure argparse to make -f
optional, if -b
is defined and require it, if -a
is defined?
edit: I just realized that it would be enough for me to make -f
and -b
mutually exclusive. But then, if I run -b
only, the check_file
is executed anyways. Is there a way to prevent that?
#!/usr/bin/python3
import argparse
from shutil import copyfile
def check_file(f):
# Mock function: checks if file exists, else "argparse.ArgumentTypeError("file not found")"
print("chk file")
return f
def main():
aFile = "/tmp/afile.txt"
parser = argparse.ArgumentParser(description="An example",formatter_class=argparse.RawTextHelpFormatter)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-f", "--file", help="A file, used with method A.", default=aFile, type=check_file)
parser.add_argument("-a", "--ay", help="Method A, requires file.", action='store_true')
group.add_argument("-b", "--be", help="Method B, no file required.", action='store_true')
args = parser.parse_args()
f = args.file
a = args.ay
b = args.be
if a:
print("File: "+str(f))
elif b:
print("Method B")
print("file: "+str(f))
if __name__ == "__main__":
main()
Output:
chk file
Method B
file: /tmp/afile.txt
回答1:
You can defined subparser with ay/be as subcommand or alternatively declare a second parser instance for a. Something like:
parser = argparse.ArgumentParser(
description="An example",
formatter_class=argparse.RawTextHelpFormatter
)
# ensure either option -a or -b only
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-a", "--ay", help="Method A, requires file.",
action='store_true')
group.add_argument("-b", "--be", help="Method B, no file required.",
action='store_true')
# define a parser for option -a
parser_a = argparse.ArgumentParser()
parser_a.add_argument("-f", "--file", help="A file, used with method A.",
type=check_file, required=True)
parser_a.add_argument("-a", "--ay", help="Method A, requires file.",
action='store_true')
# first parse - get either -a/-b
args = parser.parse_known_args(sys.argv[1:])
# if -a, use the second parser to ensure -f is in argument
# note parse_known_args return tuple, the first one is the populated namespace
if args[0].ay:
args = parser_a.parse_args(sys.argv[1:])
回答2:
Your problem lies with how argparse
handles defaults. You'd get this behavior even if -f
was the only argument. If the default is a string value, it will be 'evaluated' if the Action isn't seen.
parser.add_argument("-f", "--file", help="A file, used with method A.", default=aFile, type=check_file)
At the start of parsing defaults are put into the args
namespace. During parsing it keeps track of whether Actions have been seen. At the end of parsing it checks Namespace values for Actions which haven't been seen. If they match the default (the usual case) and are strings, it passes the default through the type
function.
In your -f
case, the default is probably a file name, a string. So it will be 'evaluated' if the user doesn't provide an alternative. In earlier argparse
versions defaults were evaluate regardless of whether they were used or not. For something like a int
or float
type that wasn't a problem, but for FileType
it could result in unneeded file opening/creation.
Ways around this?
- write
check_file
so it gracefully handlesaFile
. - make sure
aFile
is valid socheck_file
runs without error. This the usual case. - use a non-string default, e.g. an already open file.
use the default default None, and add the default value after parsing.
if args.file is None: args.file = aFile
Combining this with -a
and -b
actions you have to decide whether:
if
-a
, is a-f
value required? If-f
isn't provided, what's the rightdefault
.if
-b
, does it matter whether-f
has a default or whether the user provides this argument? Could you just ignore it?
If -f
is useful only when -a
is True, why not combine them?
parser.add_argument('-a', nargs='?', default=None, const='valid_file', type=check_file)
With ?
, this works in 3 ways. (docs on const)
- no
-a
,args.a = default
- bare
-a
,args.a = const
- -a afile
,
args.a =afile
An even simpler example of this behavior
In [956]: p = argparse.ArgumentParser()
In [957]: p.add_argument('-f',type=int, default='astring')
...
In [958]: p.parse_args('-f 1'.split())
Out[958]: Namespace(f=1)
In [959]: p.parse_args(''.split())
usage: ipython3 [-h] [-f F]
ipython3: error: argument -f: invalid int value: 'astring'
The string default is passed through int
resulting in an error. If I'd set default to something else like a list, default=[1,2,3]
, it would have run even though int
would have choked on the default.
来源:https://stackoverflow.com/questions/43134549/python-argparse-requiring-option-depending-on-the-defined-flags