Analise arquivos de configuração, ambiente e argumentos de linha de comando para obter um único conjunto de opções

110

A biblioteca padrão do Python tem módulos para análise de arquivo de configuração ( configparser ), leitura de variável de ambiente ( os.environ ) e análise de argumento de linha de comando ( argparse ). Quero escrever um programa que faça tudo isso e também:

  • Tem uma cascata de valores de opção :

    • valores de opção padrão, substituídos por
    • opções do arquivo de configuração, substituídas por
    • variáveis ​​de ambiente, substituídas por
    • opções de linha de comando.
  • Permite uma ou mais localizações de arquivo de configuração especificadas na linha de comando com por exemplo --config-file foo.conf, e lê isso (em vez de, ou adicional ao arquivo de configuração usual). Isso ainda deve obedecer à cascata acima.

  • Permite definições de opções em um único lugar para determinar o comportamento de análise para arquivos de configuração e a linha de comandos.

  • Unifica as opções analisadas em uma única coleção de valores de opção para o resto do programa acessar sem se importar de onde vieram.

Tudo que eu preciso está aparentemente na biblioteca padrão do Python, mas eles não funcionam juntos perfeitamente.

Como posso conseguir isso com desvio mínimo da biblioteca padrão do Python?

nariz grande
fonte
6
Eu realmente gosto dessa pergunta. Há muito tempo que estou pensando em fazer algo assim ... Estou feliz por ter jterracedado uma recompensa aqui para me levar ao limite o suficiente para tentar fazer algo assim :)
mgilson
4
Excelente pergunta! É incrível que isso não tenha sido resolvido por um pacote popular (ou pela própria biblioteca padrão) há muito tempo.
Zearin

Respostas:

33

O módulo argparse torna isso maluco, contanto que você esteja satisfeito com um arquivo de configuração que se pareça com uma linha de comando. (Eu acho que isso é uma vantagem, porque os usuários só terão que aprender uma sintaxe.) Definir fromfile_prefix_chars como, por exemplo @, faz com que,

my_prog --foo=bar

é equivalente a

my_prog @baz.conf

se @baz.confé,

--foo
bar

Você pode até mesmo fazer com que seu código procure foo.confautomaticamente, modificandoargv

if os.path.exists('foo.conf'):
    argv = ['@foo.conf'] + argv
args = argparser.parse_args(argv)

O formato desses arquivos de configuração é modificável criando uma subclasse de ArgumentParser e adicionando um método convert_arg_line_to_args .

Alex Szatmary
fonte
Até que alguém forneça uma alternativa melhor, esta é a resposta certa. Tenho usado argparse e nem olhei para esse recurso. Agradável!
Lemur
mas isso não tem uma resposta para variáveis ​​de ambiente?
jterrace
1
@jterrace: Esta resposta SO pode funcionar para você: stackoverflow.com/a/10551190/400793
Alex Szatmary
27

ATUALIZAÇÃO: finalmente consegui colocar isso no pypi. Instale a versão mais recente via:

   pip install configargparser

Ajuda e instruções completas estão aqui .

Postagem original

Aqui está uma coisinha que eu criei juntos. Sinta-se à vontade para sugerir melhorias / relatórios de bugs nos comentários:

import argparse
import ConfigParser
import os

def _identity(x):
    return x

_SENTINEL = object()


class AddConfigFile(argparse.Action):
    def __call__(self,parser,namespace,values,option_string=None):
        # I can never remember if `values` is a list all the time or if it
        # can be a scalar string; this takes care of both.
        if isinstance(values,basestring):
            parser.config_files.append(values)
        else:
            parser.config_files.extend(values)


class ArgumentConfigEnvParser(argparse.ArgumentParser):
    def __init__(self,*args,**kwargs):
        """
        Added 2 new keyword arguments to the ArgumentParser constructor:

           config --> List of filenames to parse for config goodness
           default_section --> name of the default section in the config file
        """
        self.config_files = kwargs.pop('config',[])  #Must be a list
        self.default_section = kwargs.pop('default_section','MAIN')
        self._action_defaults = {}
        argparse.ArgumentParser.__init__(self,*args,**kwargs)


    def add_argument(self,*args,**kwargs):
        """
        Works like `ArgumentParser.add_argument`, except that we've added an action:

           config: add a config file to the parser

        This also adds the ability to specify which section of the config file to pull the 
        data from, via the `section` keyword.  This relies on the (undocumented) fact that
        `ArgumentParser.add_argument` actually returns the `Action` object that it creates.
        We need this to reliably get `dest` (although we could probably write a simple
        function to do this for us).
        """

        if 'action' in kwargs and kwargs['action'] == 'config':
            kwargs['action'] = AddConfigFile
            kwargs['default'] = argparse.SUPPRESS

        # argparse won't know what to do with the section, so 
        # we'll pop it out and add it back in later.
        #
        # We also have to prevent argparse from doing any type conversion,
        # which is done explicitly in parse_known_args.  
        #
        # This way, we can reliably check whether argparse has replaced the default.
        #
        section = kwargs.pop('section', self.default_section)
        type = kwargs.pop('type', _identity)
        default = kwargs.pop('default', _SENTINEL)

        if default is not argparse.SUPPRESS:
            kwargs.update(default=_SENTINEL)
        else:  
            kwargs.update(default=argparse.SUPPRESS)

        action = argparse.ArgumentParser.add_argument(self,*args,**kwargs)
        kwargs.update(section=section, type=type, default=default)
        self._action_defaults[action.dest] = (args,kwargs)
        return action

    def parse_known_args(self,args=None, namespace=None):
        # `parse_args` calls `parse_known_args`, so we should be okay with this...
        ns, argv = argparse.ArgumentParser.parse_known_args(self, args=args, namespace=namespace)
        config_parser = ConfigParser.SafeConfigParser()
        config_files = [os.path.expanduser(os.path.expandvars(x)) for x in self.config_files]
        config_parser.read(config_files)

        for dest,(args,init_dict) in self._action_defaults.items():
            type_converter = init_dict['type']
            default = init_dict['default']
            obj = default

            if getattr(ns,dest,_SENTINEL) is not _SENTINEL: # found on command line
                obj = getattr(ns,dest)
            else: # not found on commandline
                try:  # get from config file
                    obj = config_parser.get(init_dict['section'],dest)
                except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): # Nope, not in config file
                    try: # get from environment
                        obj = os.environ[dest.upper()]
                    except KeyError:
                        pass

            if obj is _SENTINEL:
                setattr(ns,dest,None)
            elif obj is argparse.SUPPRESS:
                pass
            else:
                setattr(ns,dest,type_converter(obj))

        return ns, argv


if __name__ == '__main__':
    fake_config = """
[MAIN]
foo:bar
bar:1
"""
    with open('_config.file','w') as fout:
        fout.write(fake_config)

    parser = ArgumentConfigEnvParser()
    parser.add_argument('--config-file', action='config', help="location of config file")
    parser.add_argument('--foo', type=str, action='store', default="grape", help="don't know what foo does ...")
    parser.add_argument('--bar', type=int, default=7, action='store', help="This is an integer (I hope)")
    parser.add_argument('--baz', type=float, action='store', help="This is an float(I hope)")
    parser.add_argument('--qux', type=int, default='6', action='store', help="this is another int")
    ns = parser.parse_args([])

    parser_defaults = {'foo':"grape",'bar':7,'baz':None,'qux':6}
    config_defaults = {'foo':'bar','bar':1}
    env_defaults = {"baz":3.14159}

    # This should be the defaults we gave the parser
    print ns
    assert ns.__dict__ == parser_defaults

    # This should be the defaults we gave the parser + config defaults
    d = parser_defaults.copy()
    d.update(config_defaults)
    ns = parser.parse_args(['--config-file','_config.file'])
    print ns
    assert ns.__dict__ == d

    os.environ['BAZ'] = "3.14159"

    # This should be the parser defaults + config defaults + env_defaults
    d = parser_defaults.copy()
    d.update(config_defaults)
    d.update(env_defaults)
    ns = parser.parse_args(['--config-file','_config.file'])
    print ns
    assert ns.__dict__ == d

    # This should be the parser defaults + config defaults + env_defaults + commandline
    commandline = {'foo':'3','qux':4} 
    d = parser_defaults.copy()
    d.update(config_defaults)
    d.update(env_defaults)
    d.update(commandline)
    ns = parser.parse_args(['--config-file','_config.file','--foo=3','--qux=4'])
    print ns
    assert ns.__dict__ == d

    os.remove('_config.file')

FAÇAM

Esta implementação ainda está incompleta. Aqui está uma lista parcial de TODO:

Conformidade com o comportamento documentado

  • (fácil) Escreva uma função que saia destde argsdentro add_argument, em vez de depender do Actionobjeto
  • (trivial) Escreva uma parse_argsfunção que use parse_known_args. (por exemplo, cópia parse_argsda cpythonimplementação para garantir que ela chama parse_known_args.)

Coisas menos fáceis ...

Eu não tentei nada disso ainda. É improvável - mas ainda possível! - que funcione ...

  • (difícil?) Exclusão mútua
  • (difícil?) Grupos de argumentos (se implementados, esses grupos devem obter um sectionno arquivo de configuração).
  • (difícil?) Subcomandos (os subcomandos também devem ter um sectionno arquivo de configuração).
mgilson
fonte
você se importaria de jogar isso em um repositório github para que todos possam melhorar?
brent.payne
1
@ brent.payne - github.com/mgilson/configargparser - Se vou lançar isso como código real, decidi dedicar um pouco de tempo esta noite para limpá-lo um pouco. :-)
mgilson
3
FWIW, finalmente consegui colocar isso no pypi - Você deve conseguir instalá-lo viapip install configargparser
mgilson
@mgilson - Eu atualizei sua postagem. Este pacote merece mais uso!
ErichBSchulz,
12

Existe uma biblioteca que faz exatamente isso chamada configglue .

configglue é uma biblioteca que une optparse.OptionParser e ConfigParser.ConfigParser do python, para que você não precise se repetir quando quiser exportar as mesmas opções para um arquivo de configuração e uma interface de linha de comando.

Ele também oferece suporte a variáveis ​​de ambiente.

Há também outra biblioteca chamada ConfigArgParse que é

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.

Você pode se interessar pela palestra do PyCon sobre configuração de Łukasz Langa - Deixe-os configurar!

Piotr Dobrogost
fonte
Eu perguntei se há planos para oferecer suporte ao módulo argparse.
Piotr Dobrogost
10

Embora eu não tenha experimentado sozinho, existe a biblioteca ConfigArgParse , que afirma que faz a maioria das coisas que você deseja:

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.

Rutsky
fonte
1
Eu tentei, o ConfigArgParse é muito conveniente e, na verdade, um substituto imediato.
maxschlepzig,
7

Parece que a biblioteca padrão não resolver isso, deixando cada programador para remendar configparsere argparsee os.environtodos juntos em formas desajeitadas.

nariz grande
fonte
5

A biblioteca padrão do Python não fornece isso, até onde eu sei. Resolvi isso sozinho escrevendo código para usar optparsee ConfigParseranalisar a linha de comando e os arquivos de configuração, e fornecer uma camada de abstração sobre eles. No entanto, você precisaria disso como uma dependência separada, o que em seu comentário anterior parece intragável.

Se você quiser ver o código que escrevi, está em http://liw.fi/cliapp/ . Ele está integrado à minha biblioteca de "estrutura de aplicativo de linha de comando", uma vez que é uma grande parte do que a estrutura precisa fazer.


fonte
4

Tentei algo parecido recentemente, usando "optparse".

Eu o configurei como uma subclasse de OptonParser, com um comando '--Store' e um '--Check'.

O código a seguir deve ajudar você. Você só precisa definir seus próprios métodos de 'carregar' e 'armazenar' que aceitam / retornam dicionários e você está pronto.


class SmartParse(optparse.OptionParser):
    def __init__(self,defaults,*args,**kwargs):
        self.smartDefaults=defaults
        optparse.OptionParser.__init__(self,*args,**kwargs)
        fileGroup = optparse.OptionGroup(self,'handle stored defaults')
        fileGroup.add_option(
            '-S','--Store',
            dest='Action',
            action='store_const',const='Store',
            help='store command line settings'
        )
        fileGroup.add_option(
            '-C','--Check',
            dest='Action',
            action='store_const',const='Check',
            help ='check stored settings'
        )
        self.add_option_group(fileGroup)
    def parse_args(self,*args,**kwargs):
        (options,arguments) = optparse.OptionParser.parse_args(self,*args,**kwargs)
        action = options.__dict__.pop('Action')
        if action == 'Check':
            assert all(
                value is None 
                for (key,value) in options.__dict__.iteritems() 
            )
            print 'defaults:',self.smartDefaults
            print 'config:',self.load()
            sys.exit()
        elif action == 'Store':
            self.store(options.__dict__)
            sys.exit()
        else:
            config=self.load()
            commandline=dict(
                [key,val] 
                for (key,val) in options.__dict__.iteritems() 
                if val is not None
            )
            result = {}
            result.update(self.defaults)
            result.update(config)
            result.update(commandline)
            return result,arguments
    def load(self):
        return {}
    def store(self,optionDict):
        print 'Storing:',optionDict
Suki
fonte
mas ainda é útil se você deseja permanecer compatível com versões anteriores do Python
MarioVilas
3

Para atender a todos esses requisitos, eu recomendaria escrever sua própria biblioteca que usa parse [opt | arg] e configparser para a funcionalidade subjacente.

Dados os dois primeiros e o último requisito, eu diria que você deseja:

Passo um: Faça uma passagem do analisador de linha de comando que apenas procure a opção --config-file.

Etapa dois: analisar o arquivo de configuração.

Etapa três: configurar uma segunda passagem do analisador de linha de comando usando a saída da passagem do arquivo de configuração como padrão.

O terceiro requisito provavelmente significa que você deve projetar seu próprio sistema de definição de opções para expor todas as funcionalidades do optparse e configparser de seu interesse, e escrever algumas dicas para fazer as conversões entre eles.

Russell Borogove
fonte
Isso está bem mais longe do “desvio mínimo da biblioteca padrão do Python” do que eu esperava.
bignose
2

Aqui está um módulo que hackeado que lê argumentos de linha de comando, configurações de ambiente, arquivos ini e valores de chaveiro também. Também está disponível em uma essência .

"""
Configuration Parser

Configurable parser that will parse config files, environment variables,
keyring, and command-line arguments.



Example test.ini file:

    [defaults]
    gini=10

    [app]
    xini = 50

Example test.arg file:

    --xfarg=30

Example test.py file:

    import os
    import sys

    import config


    def main(argv):
        '''Test.'''
        options = [
            config.Option("xpos",
                          help="positional argument",
                          nargs='?',
                          default="all",
                          env="APP_XPOS"),
            config.Option("--xarg",
                          help="optional argument",
                          default=1,
                          type=int,
                          env="APP_XARG"),
            config.Option("--xenv",
                          help="environment argument",
                          default=1,
                          type=int,
                          env="APP_XENV"),
            config.Option("--xfarg",
                          help="@file argument",
                          default=1,
                          type=int,
                          env="APP_XFARG"),
            config.Option("--xini",
                          help="ini argument",
                          default=1,
                          type=int,
                          ini_section="app",
                          env="APP_XINI"),
            config.Option("--gini",
                          help="global ini argument",
                          default=1,
                          type=int,
                          env="APP_GINI"),
            config.Option("--karg",
                          help="secret keyring arg",
                          default=-1,
                          type=int),
        ]
        ini_file_paths = [
            '/etc/default/app.ini',
            os.path.join(os.path.dirname(os.path.abspath(__file__)),
                         'test.ini')
        ]

        # default usage
        conf = config.Config(prog='app', options=options,
                             ini_paths=ini_file_paths)
        conf.parse()
        print conf

        # advanced usage
        cli_args = conf.parse_cli(argv=argv)
        env = conf.parse_env()
        secrets = conf.parse_keyring(namespace="app")
        ini = conf.parse_ini(ini_file_paths)
        sources = {}
        if ini:
            for key, value in ini.iteritems():
                conf[key] = value
                sources[key] = "ini-file"
        if secrets:
            for key, value in secrets.iteritems():
                conf[key] = value
                sources[key] = "keyring"
        if env:
            for key, value in env.iteritems():
                conf[key] = value
                sources[key] = "environment"
        if cli_args:
            for key, value in cli_args.iteritems():
                conf[key] = value
                sources[key] = "command-line"
        print '\n'.join(['%s:\t%s' % (k, v) for k, v in sources.items()])


    if __name__ == "__main__":
        if config.keyring:
            config.keyring.set_password("app", "karg", "13")
        main(sys.argv)

Example results:

    $APP_XENV=10 python test.py api --xarg=2 @test.arg
    <Config xpos=api, gini=1, xenv=10, xini=50, karg=13, xarg=2, xfarg=30>
    xpos:   command-line
    xenv:   environment
    xini:   ini-file
    karg:   keyring
    xarg:   command-line
    xfarg:  command-line


"""
import argparse
import ConfigParser
import copy
import os
import sys

try:
    import keyring
except ImportError:
    keyring = None


class Option(object):
    """Holds a configuration option and the names and locations for it.

    Instantiate options using the same arguments as you would for an
    add_arguments call in argparse. However, you have two additional kwargs
    available:

        env: the name of the environment variable to use for this option
        ini_section: the ini file section to look this value up from
    """

    def __init__(self, *args, **kwargs):
        self.args = args or []
        self.kwargs = kwargs or {}

    def add_argument(self, parser, **override_kwargs):
        """Add an option to a an argparse parser."""
        kwargs = {}
        if self.kwargs:
            kwargs = copy.copy(self.kwargs)
            try:
                del kwargs['env']
            except KeyError:
                pass
            try:
                del kwargs['ini_section']
            except KeyError:
                pass
        kwargs.update(override_kwargs)
        parser.add_argument(*self.args, **kwargs)

    @property
    def type(self):
        """The type of the option.

        Should be a callable to parse options.
        """
        return self.kwargs.get("type", str)

    @property
    def name(self):
        """The name of the option as determined from the args."""
        for arg in self.args:
            if arg.startswith("--"):
                return arg[2:].replace("-", "_")
            elif arg.startswith("-"):
                continue
            else:
                return arg.replace("-", "_")

    @property
    def default(self):
        """The default for the option."""
        return self.kwargs.get("default")


class Config(object):
    """Parses configuration sources."""

    def __init__(self, options=None, ini_paths=None, **parser_kwargs):
        """Initialize with list of options.

        :param ini_paths: optional paths to ini files to look up values from
        :param parser_kwargs: kwargs used to init argparse parsers.
        """
        self._parser_kwargs = parser_kwargs or {}
        self._ini_paths = ini_paths or []
        self._options = copy.copy(options) or []
        self._values = {option.name: option.default
                        for option in self._options}
        self._parser = argparse.ArgumentParser(**parser_kwargs)
        self.pass_thru_args = []

    @property
    def prog(self):
        """Program name."""
        return self._parser.prog

    def __getitem__(self, key):
        return self._values[key]

    def __setitem__(self, key, value):
        self._values[key] = value

    def __delitem__(self, key):
        del self._values[key]

    def __contains__(self, key):
        return key in self._values

    def __iter__(self):
        return iter(self._values)

    def __len__(self):
        return len(self._values)

    def get(self, key, *args):
        """
        Return the value for key if it exists otherwise the default.
        """
        return self._values.get(key, *args)

    def __getattr__(self, attr):
        if attr in self._values:
            return self._values[attr]
        else:
            raise AttributeError("'config' object has no attribute '%s'"
                                 % attr)

    def build_parser(self, options, **override_kwargs):
        """."""
        kwargs = copy.copy(self._parser_kwargs)
        kwargs.update(override_kwargs)
        if 'fromfile_prefix_chars' not in kwargs:
            kwargs['fromfile_prefix_chars'] = '@'
        parser = argparse.ArgumentParser(**kwargs)
        if options:
            for option in options:
                option.add_argument(parser)
        return parser

    def parse_cli(self, argv=None):
        """Parse command-line arguments into values."""
        if not argv:
            argv = sys.argv
        options = []
        for option in self._options:
            temp = Option(*option.args, **option.kwargs)
            temp.kwargs['default'] = argparse.SUPPRESS
            options.append(temp)
        parser = self.build_parser(options=options)
        parsed, extras = parser.parse_known_args(argv[1:])
        if extras:
            valid, pass_thru = self.parse_passthru_args(argv[1:])
            parsed, extras = parser.parse_known_args(valid)
            if extras:
                raise AttributeError("Unrecognized arguments: %s" %
                                     ' ,'.join(extras))
            self.pass_thru_args = pass_thru + extras
        return vars(parsed)

    def parse_env(self):
        results = {}
        for option in self._options:
            env_var = option.kwargs.get('env')
            if env_var and env_var in os.environ:
                value = os.environ[env_var]
                results[option.name] = option.type(value)
        return results

    def get_defaults(self):
        """Use argparse to determine and return dict of defaults."""
        parser = self.build_parser(options=self._options)
        parsed, _ = parser.parse_known_args([])
        return vars(parsed)

    def parse_ini(self, paths=None):
        """Parse config files and return configuration options.

        Expects array of files that are in ini format.
        :param paths: list of paths to files to parse (uses ConfigParse logic).
                      If not supplied, uses the ini_paths value supplied on
                      initialization.
        """
        results = {}
        config = ConfigParser.SafeConfigParser()
        config.read(paths or self._ini_paths)
        for option in self._options:
            ini_section = option.kwargs.get('ini_section')
            if ini_section:
                try:
                    value = config.get(ini_section, option.name)
                    results[option.name] = option.type(value)
                except ConfigParser.NoSectionError:
                    pass
        return results

    def parse_keyring(self, namespace=None):
        """."""
        results = {}
        if not keyring:
            return results
        if not namespace:
            namespace = self.prog
        for option in self._options:
            secret = keyring.get_password(namespace, option.name)
            if secret:
                results[option.name] = option.type(secret)
        return results

    def parse(self, argv=None):
        """."""
        defaults = self.get_defaults()
        args = self.parse_cli(argv=argv)
        env = self.parse_env()
        secrets = self.parse_keyring()
        ini = self.parse_ini()

        results = defaults
        results.update(ini)
        results.update(secrets)
        results.update(env)
        results.update(args)

        self._values = results
        return self

    @staticmethod
    def parse_passthru_args(argv):
        """Handles arguments to be passed thru to a subprocess using '--'.

        :returns: tuple of two lists; args and pass-thru-args
        """
        if '--' in argv:
            dashdash = argv.index("--")
            if dashdash == 0:
                return argv[1:], []
            elif dashdash > 0:
                return argv[0:dashdash], argv[dashdash + 1:]
        return argv, []

    def __repr__(self):
        return "<Config %s>" % ', '.join([
            '%s=%s' % (k, v) for k, v in self._values.iteritems()])


def comma_separated_strings(value):
    """Handles comma-separated arguments passed in command-line."""
    return map(str, value.split(","))


def comma_separated_pairs(value):
    """Handles comma-separated key/values passed in command-line."""
    pairs = value.split(",")
    results = {}
    for pair in pairs:
        key, pair_value = pair.split('=')
        results[key] = pair_value
    return results
Ziad Sawalha
fonte
0

Você pode usar o ChainMap para isso. Dê uma olhada no meu exemplo que forneci em "Qual é a melhor maneira de permitir que as opções de configuração sejam substituídas na linha de comando em Python?" ASSIM pergunta.

Vlad Bezden
fonte
-1

A biblioteca confect eu construí é justamente para atender à maioria das suas necessidades.

  • Ele pode carregar o arquivo de configuração várias vezes por meio de determinados caminhos de arquivo ou nome de módulo.
  • Ele carrega configurações de variáveis ​​de ambiente com um determinado prefixo.
  • Ele pode anexar opções de linha de comando a alguns comandos de clique

    (desculpe, não é argparse, mas o clique é melhor e muito mais avançado.confect pode suportar argparse na versão futura).

  • Mais importante ainda, confectcarrega arquivos de configuração Python, não JSON / YMAL / TOML / INI. Assim como o arquivo de perfil IPython ou o arquivo de configurações DJANGO, o arquivo de configuração Python é flexível e fácil de manter.

Para obter mais informações, verifique o README.rst no repositório do projeto . Esteja ciente de que ele suporta apenas Python3.6 ou superior.

Exemplos

Anexando opções de linha de comando

import click
from proj_X.core import conf

@click.command()
@conf.click_options
def cli():
    click.echo(f'cache_expire = {conf.api.cache_expire}')

if __name__ == '__main__':
    cli()

Ele cria automaticamente uma mensagem de ajuda abrangente com todas as propriedades e valores padrão declarados.

$ python -m proj_X.cli --help
Usage: cli.py [OPTIONS]

Options:
  --api-cache_expire INTEGER  [default: 86400]
  --api-cache_prefix TEXT     [default: proj_X_cache]
  --api-url_base_path TEXT    [default: api/v2/]
  --db-db_name TEXT           [default: proj_x]
  --db-username TEXT          [default: proj_x_admin]
  --db-password TEXT          [default: your_password]
  --db-host TEXT              [default: 127.0.0.1]
  --help                      Show this message and exit.

Carregando variáveis ​​de ambiente

Só precisa de uma linha para carregar as variáveis ​​de ambiente

conf.load_envvars('proj_X')
d2207197
fonte
> desculpe, não é argparse, mas click é melhor e muito mais avançado [...] Independentemente dos méritos de uma biblioteca de terceiros, isso faz com que isso não seja uma resposta à questão.
bignose