Python argparse requiring option, depending on the defined flags

…衆ロ難τιáo~ 提交于 2021-01-28 04:21:19

问题


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 handles aFile.
  • make sure aFile is valid so check_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 right default.

  • 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!