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

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

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

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

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


person Bryan Oakley    schedule 11.12.2014    source источник
comment
Что в вашей версии вызовет специальную обработку аргумента (вместо символа префикса)?   -  person martineau    schedule 12.12.2014
comment
@martineau: опция -A говорит, что следующий аргумент - это файл для чтения. Мне нужно иметь возможность написать свою собственную функцию для чтения этого файла и возврата аргументов (это не формат с одним аргументом в строке)   -  person Bryan Oakley    schedule 12.12.2014
comment
Глядя на источник _parse_known_args, он выглядит так: argparse любит знать все аргументы заранее. Если у вас установлен @fromfile_prefix_chars, он просматривает аргументы и строит совершенно новый список для анализа (_read_args_from_files). Я думаю, что ваш лучший вариант — заранее проанализировать sys.argv, чтобы получить список аргументов.   -  person mgilson    schedule 12.12.2014
comment
Как вы сообщаете анализатору аргументов, что у вас есть аргумент, который следует обрабатывать таким образом? Я думаю, возможно, вы могли бы создать подкласс argparse.ArgumentParser.   -  person martineau    schedule 12.12.2014
comment
@mgilson: я никогда не говорил, что считаю это неразумным ... просто пытаюсь понять, что имеет в виду ОП.   -  person martineau    schedule 12.12.2014


Ответы (3)


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

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

class LoadFromFile (argparse.Action):
    def __call__ (self, parser, namespace, values, option_string = None):
        with values as f:
            # parse arguments in the file and store them in the target namespace
            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 будет также содержать любую конфигурацию, которая также была загружена из файла, где файл содержал аргументы с тем же синтаксисом, что и в командной строке (например, --foo 1 --bar 2).

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

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

    # parse arguments in the file and store them in a blank namespace
    data = parser.parse_args(contents.split(), namespace=None)
    for k, v in vars(data).items():
        # set arguments in the target namespace if they haven’t been set yet
        if getattr(namespace, k, None) is not None:
            setattr(namespace, k, v)

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

person poke    schedule 11.12.2014
comment
Ах-ха! Суть в том, что анализатор передается пользовательскому действию, и вы можете использовать этот анализатор для обработки содержимого файла. Спасибо. - person Bryan Oakley; 12.12.2014
comment
Но если бы парсер имел другое имя в глобальном окружении, он все равно был бы доступен здесь. Так что здесь можно использовать любой парсер. В параметре parser нет ничего особенного. - person hpaulj; 15.10.2016
comment
Увы, это не удается, если для какого-либо из ваших параметров установлено значение Required. - person Hack Saw; 06.06.2017
comment
Какой должен быть формат файла? - person Rovshan Musayev; 07.12.2020
comment
@RovshanMusayev Формат файла будет идентичен тому, что вы указываете в командной строке. Например. --foo 1 --bar 2. Но вы можете настроить логику своего действия по своему вкусу, чтобы поддерживать любой формат. - person poke; 07.12.2020
comment
Большое спасибо. Это было полезно. - person Rovshan Musayev; 07.12.2020
comment
если вы измените строку на это: parser.add_argument('--file', type=open, action=LoadFromFile, default=argparse.SUPPRESS) тогда вам не придется иметь дело с аргументом пустого файла - person kdubs; 28.02.2021

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

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

В существующем обработчике файла префикса есть возможность изменить способ чтения файла. Файл читается «приватным» методом 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 есть тестовый пример для этой альтернативы.


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

Однако вы могли бы написать свою собственную функцию, которая обрабатывает 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.

person hpaulj    schedule 12.12.2014

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)
person ivan_pozdeev    schedule 11.12.2014
comment
Я считаю, что OP уже знает об этой функции, как указано в заявлении, которое я знаю об argparse fromfile_prefix_chars, но это не совсем то, что я хочу - person mgilson; 12.12.2014