Qual é a melhor maneira de permitir que as opções de configuração sejam substituídas na linha de comando em Python?

87

Eu tenho um aplicativo Python que precisa de alguns parâmetros de configuração (~ 30). Até agora, usei a classe OptionParser para definir os valores padrão no próprio aplicativo, com a possibilidade de alterar parâmetros individuais na linha de comando ao invocar o aplicativo.

Agora eu gostaria de usar arquivos de configuração 'apropriados', por exemplo, da classe ConfigParser. Ao mesmo tempo, os usuários ainda devem ser capazes de alterar parâmetros individuais na linha de comando.

Eu queria saber se existe alguma maneira de combinar as duas etapas, por exemplo, usar optparse (ou o argparse mais recente) para lidar com as opções de linha de comando, mas lendo os valores padrão de um arquivo de configuração na sintaxe ConfigParse.

Alguma ideia de como fazer isso de maneira fácil? Eu realmente não gosto de invocar manualmente o ConfigParse e, em seguida, definir manualmente todos os padrões de todas as opções para os valores apropriados ...

andreas-h
fonte
6
Atualização : o pacote ConfigArgParse é uma substituição drop-in para argparse que permite que as opções também sejam definidas por meio de arquivos de configuração e / ou variáveis ​​de ambiente. Veja a resposta abaixo por @ user553965
nealmcb

Respostas:

88

Acabei de descobrir que você pode fazer isso com argparse.ArgumentParser.parse_known_args(). Comece usando parse_known_args()para analisar um arquivo de configuração da linha de comando, depois leia com ConfigParser e defina os padrões, e então analise o resto das opções com parse_args(). Isso permitirá que você tenha um valor padrão, substitua-o por um arquivo de configuração e, em seguida, substitua-o por uma opção de linha de comando. Por exemplo:

Padrão sem entrada do usuário:

$ ./argparse-partial.py
Option is "default"

Padrão do arquivo de configuração:

$ cat argparse-partial.config 
[Defaults]
option=Hello world!
$ ./argparse-partial.py -c argparse-partial.config 
Option is "Hello world!"

Padrão do arquivo de configuração, substituído pela linha de comando:

$ ./argparse-partial.py -c argparse-partial.config --option override
Option is "override"

argprase-partial.py a seguir. É um pouco complicado de manusear -hpara obter ajuda de forma adequada.

import argparse
import ConfigParser
import sys

def main(argv=None):
    # Do argv default this way, as doing it in the functional
    # declaration sets it at compile time.
    if argv is None:
        argv = sys.argv

    # Parse any conf_file specification
    # We make this parser with add_help=False so that
    # it doesn't parse -h and print help.
    conf_parser = argparse.ArgumentParser(
        description=__doc__, # printed with -h/--help
        # Don't mess with format of description
        formatter_class=argparse.RawDescriptionHelpFormatter,
        # Turn off help, so we print all options in response to -h
        add_help=False
        )
    conf_parser.add_argument("-c", "--conf_file",
                        help="Specify config file", metavar="FILE")
    args, remaining_argv = conf_parser.parse_known_args()

    defaults = { "option":"default" }

    if args.conf_file:
        config = ConfigParser.SafeConfigParser()
        config.read([args.conf_file])
        defaults.update(dict(config.items("Defaults")))

    # Parse rest of arguments
    # Don't suppress add_help here so it will handle -h
    parser = argparse.ArgumentParser(
        # Inherit options from config_parser
        parents=[conf_parser]
        )
    parser.set_defaults(**defaults)
    parser.add_argument("--option")
    args = parser.parse_args(remaining_argv)
    print "Option is \"{}\"".format(args.option)
    return(0)

if __name__ == "__main__":
    sys.exit(main())
Von
fonte
20
Pediram-me acima para reutilizar o código acima e, por meio deste, coloco-o no domínio público.
Von
22
'domínio púbico' me fez rir. Eu sou apenas uma criança estúpida.
SylvainD
1
argh! este é um código muito legal, mas a interpolação SafeConfigParser de propriedades substituídas pela linha de comando não funciona . Por exemplo, se você adicionar a seguinte linha a argparse-partial.config, another=%(option)s you are cruelentão anothersempre resolverá até Hello world you are cruelmesmo se optionfor substituído por outra linha na linha de comando .. argghh-parser!
ihadanny
Observe que set_defaults só funciona se os nomes dos argumentos não contiverem travessões ou sublinhados. Portanto, pode-se optar por --myVar em vez de --my-var (que é, infelizmente, muito feio). Para habilitar a distinção entre maiúsculas e minúsculas para o arquivo de configuração, use config.optionxform = str antes de analisar o arquivo, para que myVar não seja transformado em myvar.
Kevin Bader
1
Observe que se você deseja adicionar uma --versionopção ao seu aplicativo, é melhor adicioná-la ao conf_parserinvés de parsersair do aplicativo após imprimir a ajuda. Se você adicionar --versiona parsere você começar a aplicação com --versionbandeira, do que a sua aplicação desnecessariamente tentar abrir e analisar args.conf_filearquivo de configuração (que pode ser mal formado ou mesmo inexistente, o que leva a exceção).
patryk.beza
20

Confira ConfigArgParse - é um novo pacote PyPI ( código aberto ) que serve como um substituto para argparse com suporte adicionado para arquivos de configuração e variáveis ​​de ambiente.

user553965
fonte
2
apenas tentei e a inteligência funciona muito bem :) Obrigado por apontar isso.
red_tiger
1
Obrigado - parece bom! Essa página da web também compara ConfigArgParse com outras opções, incluindo argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt e configurati
nealmcb
9

Estou usando o ConfigParser e argparse com subcomandos para lidar com essas tarefas. A linha importante no código abaixo é:

subp.set_defaults(**dict(conffile.items(subn)))

Isso definirá os padrões do subcomando (de argparse) para os valores na seção do arquivo de configuração.

Um exemplo mais completo está abaixo:

####### content of example.cfg:
# [sub1]
# verbosity=10
# gggg=3.5
# [sub2]
# host=localhost

import ConfigParser
import argparse

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser_sub1 = subparsers.add_parser('sub1')
parser_sub1.add_argument('-V','--verbosity', type=int, dest='verbosity')
parser_sub1.add_argument('-G', type=float, dest='gggg')

parser_sub2 = subparsers.add_parser('sub2')
parser_sub2.add_argument('-H','--host', dest='host')

conffile = ConfigParser.SafeConfigParser()
conffile.read('example.cfg')

for subp, subn in ((parser_sub1, "sub1"), (parser_sub2, "sub2")):
    subp.set_defaults(**dict(conffile.items(subn)))

print parser.parse_args(['sub1',])
# Namespace(gggg=3.5, verbosity=10)
print parser.parse_args(['sub1', '-V', '20'])
# Namespace(gggg=3.5, verbosity=20)
print parser.parse_args(['sub1', '-V', '20', '-G','42'])
# Namespace(gggg=42.0, verbosity=20)
print parser.parse_args(['sub2', '-H', 'www.example.com'])
# Namespace(host='www.example.com')
print parser.parse_args(['sub2',])
# Namespace(host='localhost')
xubuntix
fonte
meu problema é que argparse define o caminho do arquivo de configuração e o arquivo de configuração define os padrões do argparse ... problema idiota do ovo de galinha
olivervbk
4

Não posso dizer que é a melhor maneira, mas tenho uma classe OptionParser que fiz que faz exatamente isso - atua como optparse.OptionParser com padrões provenientes de uma seção de arquivo de configuração. Você pode ficar com ele ...

class OptionParser(optparse.OptionParser):
    def __init__(self, **kwargs):
        import sys
        import os
        config_file = kwargs.pop('config_file',
                                 os.path.splitext(os.path.basename(sys.argv[0]))[0] + '.config')
        self.config_section = kwargs.pop('config_section', 'OPTIONS')

        self.configParser = ConfigParser()
        self.configParser.read(config_file)

        optparse.OptionParser.__init__(self, **kwargs)

    def add_option(self, *args, **kwargs):
        option = optparse.OptionParser.add_option(self, *args, **kwargs)
        name = option.get_opt_string()
        if name.startswith('--'):
            name = name[2:]
            if self.configParser.has_option(self.config_section, name):
                self.set_default(name, self.configParser.get(self.config_section, name))

Sinta-se à vontade para navegar na fonte . Os testes estão em um diretório irmão.

Blair Conrad
fonte
3

Você pode usar o ChainMap

A ChainMap groups multiple dicts or other mappings together to create a single, updateable view. If no maps are specified, a single empty dictionary is provided so that a new chain always has at least one mapping.

Você pode combinar valores da linha de comando, variáveis ​​de ambiente, arquivo de configuração e, caso o valor não esteja lá, defina um valor padrão.

import os
from collections import ChainMap, defaultdict

options = ChainMap(command_line_options, os.environ, config_file_options,
               defaultdict(lambda: 'default-value'))
value = options['optname']
value2 = options['other-option']


print(value, value2)
'optvalue', 'default-value'
Vlad Bezden
fonte
Qual é a vantagem de um ChainMap sobre, digamos, uma cadeia de dictsatualizações na ordem de precedência desejada? Com defaultdict, possivelmente, há uma vantagem, pois opções novas ou não suportadas podem ser definidas, mas isso é diferente ChainMap. Presumo que esteja faltando alguma coisa.
Dan
2

Atualização: Esta resposta ainda tem problemas; por exemplo, ele não pode lidar com requiredargumentos e requer uma sintaxe de configuração estranha. Em vez disso, ConfigArgParse parece ser exatamente o que esta pergunta pede e é uma substituição transparente e instantânea .

Um problema com o atual é que não haverá erro se os argumentos no arquivo de configuração forem inválidos. Esta é uma versão com uma desvantagem diferente: você precisará incluir o prefixo --ou -nas chaves.

Aqui está o código Python ( link Gist com licença MIT):

# Filename: main.py
import argparse

import configparser

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--config_file', help='config file')
    args, left_argv = parser.parse_known_args()
    if args.config_file:
        with open(args.config_file, 'r') as f:
            config = configparser.SafeConfigParser()
            config.read([args.config_file])

    parser.add_argument('--arg1', help='argument 1')
    parser.add_argument('--arg2', type=int, help='argument 2')

    for k, v in config.items("Defaults"):
        parser.parse_args([str(k), str(v)], args)

    parser.parse_args(left_argv, args)
print(args)

Aqui está um exemplo de arquivo de configuração:

# Filename: config_correct.conf
[Defaults]
--arg1=Hello!
--arg2=3

Agora correndo

> python main.py --config_file config_correct.conf --arg1 override
Namespace(arg1='override', arg2=3, config_file='test_argparse.conf')

No entanto, se nosso arquivo de configuração tiver um erro:

# config_invalid.conf
--arg1=Hello!
--arg2='not an integer!'

Executar o script produzirá um erro, conforme desejado:

> python main.py --config_file config_invalid.conf --arg1 override
usage: test_argparse_conf.py [-h] [--config_file CONFIG_FILE] [--arg1 ARG1]
                             [--arg2 ARG2]
main.py: error: argument --arg2: invalid int value: 'not an integer!'

A principal desvantagem é que isso usa parser.parse_argsum pouco hackly para obter a verificação de erros do ArgumentParser, mas não estou ciente de quaisquer alternativas para isso.

Achal Dave
fonte
1

Tente desta forma

# encoding: utf-8
import imp
import argparse


class LoadConfigAction(argparse._StoreAction):
    NIL = object()

    def __init__(self, option_strings, dest, **kwargs):
        super(self.__class__, self).__init__(option_strings, dest)
        self.help = "Load configuration from file"

    def __call__(self, parser, namespace, values, option_string=None):
        super(LoadConfigAction, self).__call__(parser, namespace, values, option_string)

        config = imp.load_source('config', values)

        for key in (set(map(lambda x: x.dest, parser._actions)) & set(dir(config))):
            setattr(namespace, key, getattr(config, key))

Use-o:

parser.add_argument("-C", "--config", action=LoadConfigAction)
parser.add_argument("-H", "--host", dest="host")

E crie um exemplo de configuração:

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")
mosquito
fonte
1

fromfile_prefix_chars

Talvez não seja a API perfeita, mas vale a pena conhecer. main.py:

#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
parser.add_argument('-a', default=13)
parser.add_argument('-b', default=42)
print(parser.parse_args())

Então:

$ printf -- '-a\n1\n-b\n2\n' > opts.txt
$ ./main.py
Namespace(a=13, b=42)
$ ./main.py @opts.txt
Namespace(a='1', b='2')
$ ./main.py @opts.txt -a 3 -b 4
Namespace(a='3', b='4')
$ ./main.py -a 3 -b 4 @opts.txt
Namespace(a='1', b='2')

Documentação: https://docs.python.org/3.6/library/argparse.html#fromfile-prefix-chars

Testado em Python 3.6.5, Ubuntu 18.04.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte