Как получить argparse для чтения аргументов из файла с опцией, а не с префиксом

Я хотел бы знать, как использовать модуль python argparse для чтения аргументов как из командной строки, так и, возможно, из текстовых файлов. Я знаю об argparse fromfile_prefix_chars, но это не совсем то, что я хочу. Я хочу поведение, но я не хочу синтаксис. Мне нужен интерфейс, который выглядит следующим образом:

$ python myprogram.py --foo 1 -A somefile.txt --bar 2

Когда argparse видит -A, он должен прекратить чтение из sys.argv или того, что я ему даю, и вызвать вызываемую мной функцию, которая прочитает somefile.text и вернет список аргументов. Когда файл исчерпан, он должен возобновить синтаксический анализ sys.argv или чего-либо еще. Важно, чтобы обработка аргументов в файле происходила по порядку (то есть: -foo следует обработать, затем аргументы в файле, затем -bar, чтобы аргументы в файле могли переопределить - -foo, и - -bar может переопределить то, что в файле).

Это возможно? Могу ли я написать пользовательскую функцию, которая помещает новые аргументы в стек argparse, или что-то в этом роде?

Ответ 1

Вы можете решить эту проблему с помощью пользовательского argparse.Action, который открывает файл, анализирует содержимое файла и затем добавляет аргументы.

Например, это будет очень простое действие:

class LoadFromFile (argparse.Action):
    def __call__ (self, parser, namespace, values, option_string = None):
        with values as f:
            parser.parse_args(f.read().split(), namespace)

Что вы можете использовать, как это:

parser = argparse.ArgumentParser()
# other arguments
parser.add_argument('--file', type=open, action=LoadFromFile)
args = parser.parse_args()

Полученное пространство имен в args также будет содержать любую конфигурацию, которая также была загружена из файла.

Если вам нужен более сложный синтаксический анализ, вы также можете сначала проанализировать конфигурацию в файле отдельно, а затем выборочно выбрать, какие значения следует принять. Например, может иметь смысл запретить указывать другой файл в файле конфигурации:

def __call__ (self, parser, namespace, values, option_string=None):
    with values as f:
        contents = f.read()

    data = parser.parse_args(contents.split(), namespace=namespace)
    for k, v in vars(data).items():
        if v and k != option_string.lstrip('-'):
            setattr(namespace, k, v)

Конечно, вы также можете сделать чтение файла более сложным, например, сначала прочитать из JSON.

Ответ 2

Вы прокомментировали, что

Мне нужно иметь возможность написать собственную функцию для чтения этого файла и возврата аргументов (не в формате с одним аргументом на строку) -

В существующем обработчике префиксного файла есть положение, позволяющее изменить способ чтения файла. Файл читается "закрытым" методом, parser._read_args_from_files, но он вызывает простой открытый метод, который преобразует строку в строки, действие по умолчанию с одним аргументом на строку:

def convert_arg_line_to_args(self, arg_line):
    return [arg_line]

Это было написано так, чтобы вы могли легко настроить его. https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.convert_arg_line_to_args

Полезным переопределением этого метода является то, которое обрабатывает каждое разделенное пробелами слово как аргумент:

def convert_arg_line_to_args(self, arg_line):
    for arg in arg_line.split():
        if not arg.strip():
            continue
        yield arg

В файле юнит-тестирования test_argparse.py есть тестовый пример для этой альтернативы.


Но если вы все еще хотите инициировать это чтение с параметром аргумента, а не с префиксным символом, тогда подход Custom Action является хорошим.

Вы могли бы написать свою собственную функцию, которая обрабатывает argv, прежде чем она будет передана в parser. Это может быть смоделировано на parser._read_args_from_files.

Таким образом, вы можете написать такую функцию:

def read_my_file(argv):
    # if there is a '-A' string in argv, replace it, and the following filename
    # with the contents of the file (as strings)
    # you can adapt code from _read_args_from_files
    new_argv = []
    for a in argv:
        ....
        # details left to user
    return new_argv

Затем вызовите парсер с помощью:

parser.parse_args(read_my_file(sys.argv[1:]))

И да, это может быть заключено в подкласс ArgumentParser.

Ответ 3

Action, когда вызывается, получает parser и namespace среди своих аргументов.

Таким образом, вы можете поместить свой файл через первый, чтобы обновить последний:

class ArgfileAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        extra_args = <parse_the_file>(values)
        #'namespace' is updated in-place when specified
        parser.parse_args(extra_args,namespace)