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 ...
fonte
Respostas:
Acabei de descobrir que você pode fazer isso com
argparse.ArgumentParser.parse_known_args()
. Comece usandoparse_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 comparse_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
-h
para 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())
fonte
another=%(option)s you are cruel
entãoanother
sempre resolverá atéHello world you are cruel
mesmo seoption
for substituído por outra linha na linha de comando .. argghh-parser!--version
opção ao seu aplicativo, é melhor adicioná-la aoconf_parser
invés deparser
sair do aplicativo após imprimir a ajuda. Se você adicionar--version
aparser
e você começar a aplicação com--version
bandeira, do que a sua aplicação desnecessariamente tentar abrir e analisarargs.conf_file
arquivo de configuração (que pode ser mal formado ou mesmo inexistente, o que leva a exceção).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.
fonte
Estou usando o ConfigParser e argparse com subcomandos para lidar com essas tarefas. A linha importante no código abaixo é:
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')
fonte
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.
fonte
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'
fonte
dicts
atualizações na ordem de precedência desejada? Comdefaultdict
, possivelmente, há uma vantagem, pois opções novas ou não suportadas podem ser definidas, mas isso é diferenteChainMap
. Presumo que esteja faltando alguma coisa.Atualização: Esta resposta ainda tem problemas; por exemplo, ele não pode lidar com
required
argumentos 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_args
um pouco hackly para obter a verificação de erros do ArgumentParser, mas não estou ciente de quaisquer alternativas para isso.fonte
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")
fonte
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.
fonte