
Passo 1 de 8
Definir o contrato e a ordem das fontes
Defina quais valores a aplicação aceita e qual fonte vence quando mais de uma fornece a mesma chave.
Trilha de aprendizado · Nível 15 · Tutorial 4
Resolver a configuração da aplicação a partir de padrões, arquivo TOML, variáveis de ambiente e opções da CLI, com regras claras para conflitos e valores inválidos.
Definir o contrato e a ordem das fontes
Defina quais valores a aplicação aceita e qual fonte vence quando mais de uma fornece a mesma chave. 2 min
Ler a estrutura de um arquivo TOML
Leia um arquivo TOML com segurança estrutural e extraia a contribuição da tabela da aplicação. 2 min
Estabelecer a política de arquivos e caminhos
Defina regras previsíveis para localizar o arquivo de configuração e interpretar caminhos relativos conforme sua fonte. 2 min
Normalizar variáveis de ambiente
Converta variáveis de ambiente presentes em uma contribuição parcial com chaves internas e tipos corretos. 2 min
Preservar opções omitidas na CLI
Configure o parser para que a CLI contribua somente com valores realmente informados, sem perder 0 nem False. 2 min
Combinar as fontes sem perder suas regras
Organize o carregamento em etapas, normalize cada fonte antes da sobreposição e preserve erros de fontes presentes. 3 min
Validar antes de montar a aplicação
Valide o resultado consolidado antes de criar componentes da aplicação, garantindo que apenas uma configuração tipada e válida chegue à raiz de composição. 2 min
Aplicar e verificar o contrato completo
Monte um exemplo autocontido, execute testes locais e confirme o contrato de precedência, caminhos e falhas de configuração. 4 min

Passo 1 de 8
Defina quais valores a aplicação aceita e qual fonte vence quando mais de uma fornece a mesma chave.
Neste exemplo, a camada de configuração entrega três valores para a aplicação:
limite: inteiro não negativo. O valor 0 significa sem limite.detalhado: booleano que ativa ou desativa uma saída mais detalhada.arquivo_dados: caminho do arquivo de dados usado pela aplicação.Antes de decidir de onde vem cada valor, defina esse contrato. Todas as fontes devem contribuir apenas com essas mesmas chaves, nos tipos esperados. A conversão e a validação serão tratadas nas próximas etapas.
Exemplo
A camada de configuração começa com um único conjunto de padrões da aplicação:
PADROES = {
"limite": 100,
"detalhado": False,
"arquivo_dados": "dados.csv",
}Esses padrões não pertencem ao arquivo, ao ambiente nem ao parser da CLI. Eles existem uma vez, na configuração, para que cada fonte externa possa informar somente o que deseja alterar.
Quando uma mesma chave aparece em mais de uma fonte, a fonte de maior prioridade vence. A ordem adotada é:
A CLI fica no topo porque representa uma decisão específica daquela execução. Essa ordem deve ser documentada como parte do contrato da aplicação.
Leia o diagrama de baixo para cima: cada camada só altera as chaves que ela fornece.

A prioridade sobe dos padrões para a CLI; chaves ausentes continuam com o valor já resolvido.
Considere contribuições já normalizadas para o formato interno:
limite=100, detalhado=False, arquivo_dados="dados.csv"limite=50detalhado=Truelimite=0O resultado é limite=0, detalhado=True e arquivo_dados="dados.csv".
A CLI não apaga detalhado nem arquivo_dados, porque ela forneceu somente limite. Em especial, 0 é um valor informado e válido no contrato: não significa ausência.
Dica
Pergunte chave por chave: “qual foi a fonte de maior prioridade que forneceu este valor?”. Não pergunte qual fonte venceu a configuração inteira.
Organize as fontes na ordem em que seus valores devem ser sobrepostos.
Com padrões {"limite": 100, "detalhado": False, "arquivo_dados": "dados.csv"}, TOML {"arquivo_dados": "local.csv"}, ambiente {"detalhado": True} e CLI {"limite": 0}, qual resultado respeita o contrato?

Passo 2 de 8
Leia um arquivo TOML com segurança estrutural e extraia a contribuição da tabela da aplicação.
A partir do Python 3.11, use o módulo padrão tomllib para ler TOML. Ele é apropriado para a configuração de uso da aplicação — não confunda esse arquivo com o pyproject.toml, que terá outra finalidade.
Neste exemplo, a tabela [aplicacao] reúne as três chaves reconhecidas: limite, detalhado e arquivo_dados. TOML preserva tipos básicos: inteiros viram int, booleanos viram bool e textos viram str.
A tabela TOML se torna um dicionário aninhado; a chave aplicacao aponta para outro dicionário.

[aplicacao] cria o nível aninhado dados["aplicacao"]; ainda não existe mesclagem entre fontes.
Exemplo
[aplicacao]
limite = 50
detalhado = true
arquivo_dados = "dados/itens.csv"Após a leitura, a estrutura relevante é equivalente a:
{
"aplicacao": {
"limite": 50,
"detalhado": True,
"arquivo_dados": "dados/itens.csv",
}
}A tabela [aplicacao] é opcional: se ela não aparecer, este arquivo contribui com {}. Se aparecer, precisa ser uma tabela e só pode conter as chaves reconhecidas.
Essa função apenas lê e verifica a estrutura. A conversão e a validação dos valores serão etapas posteriores.
O arquivo é aberto em modo binário ("rb"), exigência de tomllib.load.
from pathlib import Path
import tomllib
CHAVES_CONFIG = {"limite", "detalhado", "arquivo_dados"}
class EstruturaConfigInvalida(ValueError):
pass
def carregar_toml(config_file: Path) -> dict[str, object]:
with config_file.open("rb") as arquivo:
dados = tomllib.load(arquivo)
aplicacao = dados.get("aplicacao")
if aplicacao is None:
return {}
if not isinstance(aplicacao, dict):
raise EstruturaConfigInvalida(
"A seção 'aplicacao' deve ser uma tabela TOML."
)
desconhecidas = set(aplicacao) - CHAVES_CONFIG
if desconhecidas:
raise EstruturaConfigInvalida(
"A seção 'aplicacao' contém chaves não reconhecidas."
)
return dict(aplicacao)
Dica
tomllib.TOMLDecodeError indica sintaxe TOML inválida, como um valor mal escrito. Já uma falha para abrir ou ler o arquivo é uma falha de acesso do sistema, e uma EstruturaConfigInvalida indica que o TOML foi lido, mas não segue o contrato da aplicação. Não esconda essas falhas retornando {}.
Relacione cada situação ao comportamento correto do carregador.
Toque em um item e depois no par correspondente.

Passo 3 de 8
Defina regras previsíveis para localizar o arquivo de configuração e interpretar caminhos relativos conforme sua fonte.
Quando nenhuma opção solicitar um arquivo específico, a aplicação procura config.toml no diretório de trabalho atual. Esse arquivo convencional é opcional: se não existir, sua contribuição é simplesmente {} e a resolução segue com as outras fontes.
Já um arquivo indicado explicitamente por --config é uma solicitação do usuário. Se ele não existir, o carregamento deve falhar; não é correto trocar silenciosamente esse pedido por padrões.
Exemplo
Diretório de trabalho: /projetos/relatorios
/projetos/relatorios/config.toml não existe → continuar com {}.--config perfis/producao.toml; o caminho não existe → falhar.config.toml existe, mas não pode ser lido ou contém TOML inválido → falhar.A tolerância vale somente para a ausência do arquivo convencional. Permissão negada, sintaxe inválida e estrutura inválida são erros de uma fonte que foi encontrada.
Atenção
Não capture qualquer falha e retorne {}. Isso converteria um problema real — por exemplo, arquivo sem permissão ou TOML malformado — em uma execução com valores possivelmente inesperados.
Não busque nem grave configuração de uso ao lado de __file__: o pacote instalado não é a referência do usuário.
No exemplo, caminhos relativos recebidos por padrões internos, ambiente e CLI usam o diretório de trabalho. Um caminho relativo passado a --config também parte dele. Em contraste, um caminho declarado dentro do TOML usa a pasta que contém aquele arquivo TOML. Caminhos absolutos são preservados.
Compare a base usada por um valor da CLI com a base usada por um valor escrito no TOML.

Resolva o caminho vindo do TOML enquanto ainda conhece a pasta do próprio TOML; depois da mesclagem, essa origem pode não estar disponível.
Exemplo
Diretório de trabalho: /projetos/app
Arquivo selecionado: perfis/config.toml → /projetos/app/perfis/config.toml
No TOML: arquivo_dados = "dados/clientes.csv" → /projetos/app/perfis/dados/clientes.csv
Na CLI: --arquivo-dados dados/clientes.csv → /projetos/app/dados/clientes.csv
O texto é igual, mas a fonte define sua referência.
A aplicação foi iniciada em /trabalho, sem solicitar arquivo específico. O caminho /trabalho/config.toml não existe. Qual é a decisão correta?
O diretório de trabalho é /home/ana/app. O arquivo lido é /home/ana/config/perfil.toml e nele consta arquivo_dados = "dados/vendas.csv". Qual caminho deve resultar?

Passo 4 de 8
Converta variáveis de ambiente presentes em uma contribuição parcial com chaves internas e tipos corretos.
Variáveis de ambiente são consultadas como um mapeamento, normalmente os.environ. Seus valores sempre chegam como strings: até "0", "false" e "42" são textos.
Mapeie somente as chaves previstas pelo contrato:
APP_LIMITE → limite (int)APP_DETALHADO → detalhado (bool)APP_ARQUIVO_DADOS → arquivo_dados (Path)A contribuição do ambiente é parcial: se uma chave não existe no mapeamento, ela não entra no dicionário resultante.

A presença da chave decide se ela contribui. A conversão decide o tipo do valor contribuído.
Dica
Use if "APP_LIMITE" in ambiente, e não um teste de verdade do valor. Uma chave presente com "" continua presente; ela deve passar pela normalização apropriada. Em especial, um caminho vazio deve ser rejeitado, pois Path("") poderia representar o diretório atual sem que essa fosse a intenção.
Não use bool(texto) para interpretar a variável booleana: toda string não vazia é verdadeira em Python. Portanto, bool("false") resulta em True.
Normalize espaços e maiúsculas/minúsculas, depois aceite explicitamente apenas true e false. Caminhos recebidos do ambiente usam o diretório de trabalho como referência.
import os
from collections.abc import Mapping
from pathlib import Path
def converter_booleano(texto: str) -> bool:
normalizado = texto.strip().lower()
if normalizado == "true":
return True
if normalizado == "false":
return False
raise ValueError("APP_DETALHADO deve ser true ou false")
def normalizar_ambiente(
ambiente: Mapping[str, str],
*,
diretorio_trabalho: Path,
) -> dict[str, object]:
contribuicao: dict[str, object] = {}
if "APP_LIMITE" in ambiente:
contribuicao["limite"] = int(ambiente["APP_LIMITE"])
if "APP_DETALHADO" in ambiente:
contribuicao["detalhado"] = converter_booleano(
ambiente["APP_DETALHADO"]
)
if "APP_ARQUIVO_DADOS" in ambiente:
texto_caminho = ambiente["APP_ARQUIVO_DADOS"].strip()
if not texto_caminho:
raise ValueError("APP_ARQUIVO_DADOS não pode ser vazio")
caminho = Path(texto_caminho)
contribuicao["arquivo_dados"] = (
caminho if caminho.is_absolute() else diretorio_trabalho / caminho
)
return contribuicao
# Na execução normal:
config_do_ambiente = normalizar_ambiente(
os.environ,
diretorio_trabalho=Path.cwd(),
)A expressão bool("false") produz False em Python.
Considere diretorio_trabalho = Path("/projeto") e o mapeamento abaixo:
ambiente = {
"APP_DETALHADO": " FALSE ",
"APP_ARQUIVO_DADOS": "dados/itens.csv",
}Qual contribuição normalizada está correta?

Passo 5 de 8
Configure o parser para que a CLI contribua somente com valores realmente informados, sem perder 0 nem False.
Na resolução por precedência, uma opção omitida na linha de comando não deve gerar um valor artificial. Caso contrário, esse valor poderia encobrir uma configuração vinda do arquivo ou do ambiente.
Use default=argparse.SUPPRESS nas opções que correspondem a chaves de configuração. Quando a opção não aparece, o atributo nem sequer é criado no Namespace.
Compare uma chamada sem opções configuráveis com outra que informa valores explícitos.

A ausência produz {}; valores explícitos, inclusive 0 e False, permanecem na contribuição da CLI.
Cada opção configurável usa SUPPRESS. O sinalizador negativo cria False somente quando é usado.
import argparse
def criar_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument(
"--limite",
type=int,
default=argparse.SUPPRESS,
)
parser.add_argument(
"--detalhado",
action="store_true",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--sem-detalhes",
dest="detalhado",
action="store_false",
default=argparse.SUPPRESS,
)
parser.add_argument("--arquivo-dados", default=argparse.SUPPRESS)
# Seleciona o arquivo; não é entregue à configuração da aplicação.
parser.add_argument("--config")
return parser
parser = criar_parser()
vazio = parser.parse_args([])
explicito = parser.parse_args(["--limite", "0", "--sem-detalhes"])
print(vars(vazio)) # {}
print(vars(explicito)) # {'limite': 0, 'detalhado': False}Dica
valor or outro_valor descartaria 0 e False. Ao combinar fontes, a regra é a presença da chave: se "limite" está no dicionário da CLI, seu 0 é uma contribuição válida.
--config escolhe qual arquivo TOML será consultado. Por isso, ele pode existir no Namespace, mas não pertence ao dicionário de chaves entregue aos componentes da aplicação.
Depois de interpretar os argumentos, use vars() e selecione explicitamente as chaves configuráveis presentes. Assim, padrões de negócio continuam centralizados na camada de configuração, não no parser.
A seleção por chave preserva exatamente os atributos que o usuário informou.
CHAVES_CONFIG = {"limite", "detalhado", "arquivo_dados"}
def contribuicao_cli(args: argparse.Namespace) -> dict[str, object]:
valores = vars(args)
return {
chave: valores[chave]
for chave in CHAVES_CONFIG
if chave in valores
}
args = criar_parser().parse_args(
["--config", "minha-config.toml", "--limite", "0", "--sem-detalhes"]
)
print(args.config) # minha-config.toml
print(contribuicao_cli(args)) # {'limite': 0, 'detalhado': False}Complete o valor de default para que --limite só apareça no Namespace quando for informado:
parser.add_argument("--limite", type=int, default=____)Após executar parse_args(["--limite", "0", "--sem-detalhes"]), qual dicionário contribuicao_cli(args) deve retornar?

Passo 6 de 8
Organize o carregamento em etapas, normalize cada fonte antes da sobreposição e preserve erros de fontes presentes.
A resolução segue uma ordem fixa: interpretar os argumentos, selecionar o arquivo, coletar as contribuições, normalizar cada uma e só então sobrepor os dicionários.
Cada coletor deve entregar as mesmas chaves internas e os mesmos tipos: limite como int, detalhado como bool e arquivo_dados como Path. Resolva um caminho relativo enquanto ainda conhece sua origem: o diretório do TOML para valores do arquivo; o diretório de trabalho para padrões, ambiente e CLI.
Assim, a etapa de mesclagem não precisa saber se um valor veio de texto, TOML ou argumento.
A imagem mostra que a normalização acontece antes da sobreposição e que cada chave pode ter uma fonte vencedora diferente.

A prioridade é aplicada por chave, depois que cada fonte já foi interpretada.
Dica
Uma chave presente com 0 ou False deve substituir um valor inferior. Portanto, não use valor_da_cli or valor_do_ambiente: essa expressão descartaria justamente valores explícitos válidos.
Depois que os coletores concluírem sem erro, use dict.update na ordem crescente de prioridade. Isso cria uma configuração em trabalho e mantém PADROES intacto para outras execuções.
As funções coletoras abaixo já normalizaram e validaram a representação de cada fonte.
from pathlib import Path
from typing import Any
PADROES: dict[str, Any] = {
"limite": 100,
"detalhado": True,
"arquivo_dados": Path("dados.csv"),
}
def combinar(
arquivo: dict[str, Any],
ambiente: dict[str, Any],
cli: dict[str, Any],
) -> dict[str, Any]:
"""Sobrepõe contribuições já normalizadas, sem alterar PADROES."""
configuracao = PADROES.copy()
configuracao.update(arquivo)
configuracao.update(ambiente)
configuracao.update(cli)
return configuracao
arquivo = {"limite": 20, "detalhado": False}
ambiente = {"limite": 0}
cli = {"arquivo_dados": Path("entrada.csv")}
resultado = combinar(arquivo, ambiente, cli)
# {"limite": 0, "detalhado": False,
# "arquivo_dados": Path("entrada.csv")}Uma fonte presente precisa ser lida, estruturada e convertida corretamente antes de poder contribuir. Se o TOML contém limite = true, há um erro de representação: pelo contrato, limite deve ser inteiro, e bool não é aceito mesmo sendo subclasse de int em Python.
Não continue esperando que ambiente ou CLI substituam esse valor. A política é interromper a resolução em falhas de leitura, sintaxe, estrutura ou conversão de uma fonte presente. Restrições semânticas, como verificar se o limite vencedor é não negativo, ficam para a próxima etapa.
Esta verificação pertence ao normalizador do TOML, antes de chamar combinar.
def ler_limite_toml(valor: object) -> int:
# bool é subclasse de int; por isso type(...) é int, não isinstance.
if type(valor) is not int:
raise ValueError("configuração: limite do TOML deve ser inteiro")
return valor
ler_limite_toml(25) # 25
# ler_limite_toml(True) # ValueErrorConsidere: padrões limite=100, detalhado=True; TOML limite=20, detalhado=False; ambiente APP_LIMITE=0; CLI --arquivo-dados entrada.csv.
Quais valores vencem? Em seguida, explique o que muda se o TOML presente contiver limite = true, mesmo com APP_LIMITE=0 no ambiente.
Escreva pelo menos 120 caracteres (0/120).

Passo 7 de 8
Valide o resultado consolidado antes de criar componentes da aplicação, garantindo que apenas uma configuração tipada e válida chegue à raiz de composição.
As fontes já foram normalizadas e sobrepostas. Agora, valide o dicionário consolidado: ele precisa conter todas as chaves exigidas, com os tipos corretos e com regras semânticas atendidas.
Neste contrato, limite deve ser um inteiro maior ou igual a zero; 0 significa “sem limite”. detalhado deve ser booleano, e arquivo_dados deve ser um Path já resolvido conforme a referência de sua fonte.
A ordem importa: primeiro resolva e valide; só depois construa os componentes.

Um valor negativo no arquivo pode ser substituído por um valor válido de maior prioridade. Mas, se o valor vencedor for inválido, o fluxo para antes da composição.
Atenção
Se o valor final de limite for -1, a configuração falha. Não volte silenciosamente ao padrão e não procure um valor de prioridade menor que seja válido. A precedência já decidiu qual valor venceu; a validação decide se esse resultado pode ser usado.
Receba aqui o dicionário já normalizado e mesclado pelas fontes.
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping
class ErroConfiguracao(ValueError):
"""Indica uma configuração ausente, malformada ou inválida."""
@dataclass(frozen=True)
class Config:
limite: int
detalhado: bool
arquivo_dados: Path
def validar_config(valores: Mapping[str, Any]) -> Config:
obrigatorias = {"limite", "detalhado", "arquivo_dados"}
ausentes = obrigatorias - valores.keys()
if ausentes:
chave = sorted(ausentes)[0]
raise ErroConfiguracao(
f"configuração inválida: chave '{chave}' está ausente"
)
limite = valores["limite"]
detalhado = valores["detalhado"]
arquivo_dados = valores["arquivo_dados"]
# bool é subclasse de int; por isso, teste-o explicitamente.
if isinstance(limite, bool) or not isinstance(limite, int):
raise ErroConfiguracao(
"configuração inválida: chave 'limite' deve ser inteira"
)
if not isinstance(detalhado, bool):
raise ErroConfiguracao(
"configuração inválida: chave 'detalhado' deve ser booleana"
)
if not isinstance(arquivo_dados, Path):
raise ErroConfiguracao(
"configuração inválida: chave 'arquivo_dados' deve ser um caminho"
)
if limite < 0:
raise ErroConfiguracao(
"configuração inválida: chave 'limite' deve ser maior ou igual a zero"
)
return Config(
limite=limite,
detalhado=detalhado,
arquivo_dados=arquivo_dados,
)Dica
A exceção informa a chave, a etapa e o motivo seguro da falha. Evite incluir o valor bruto recebido: ele pode conter caminhos sensíveis ou outros dados que não devem aparecer em diagnósticos. A forma de apresentar essa exceção na CLI será definida depois.
Exemplo
A raiz de composição recebe uma Config pronta:
valores = resolver_configuracao(argv, ambiente)
config = validar_config(valores)
repositorio = criar_repositorio(config.arquivo_dados)
servico = criar_servico(repositorio, detalhado=config.detalhado)Se validar_config() lançar ErroConfiguracao, criar_repositorio() e criar_servico() não são chamados. Ler e validar configuração não é iniciar operações de negócio.
Coloque o fluxo na ordem correta para impedir que componentes sejam inicializados com configuração inválida.

Passo 8 de 8
Monte um exemplo autocontido, execute testes locais e confirme o contrato de precedência, caminhos e falhas de configuração.
Crie uma pasta vazia para este experimento. Nela, salve o primeiro bloco como config.py. O módulo recebe argumentos, ambiente e diretório de trabalho como dependências: isso permite executar normalmente e testar sem depender da configuração real do seu computador.
A ordem de aplicação é sempre: padrões → TOML → ambiente → CLI. Cada fonte já é normalizada antes da sobreposição; portanto, um valor inválido em uma fonte presente falha mesmo que uma fonte superior pudesse substituí-lo.

Os caminhos são resolvidos dentro de cada fonte, antes de os dicionários serem combinados.
Salve este módulo completo como config.py. Requer Python 3.11 ou superior, pois usa tomllib.
from __future__ import annotations
import argparse
import os
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Mapping
class ConfigError(Exception):
"""Falha segura ao carregar ou validar a configuração."""
@dataclass(frozen=True)
class Config:
limite: int
detalhado: bool
arquivo_dados: Path
PADROES = {
"limite": 10,
"detalhado": False,
"arquivo_dados": Path("dados.json"),
}
CHAVES = set(PADROES)
def caminho(valor: str | Path, base: Path) -> Path:
if str(valor).strip() == "":
raise ConfigError("arquivo_dados: caminho vazio")
resultado = Path(valor)
return resultado if resultado.is_absolute() else base / resultado
def normalizar_bruto(dados: Mapping[str, object], base: Path) -> dict[str, object]:
desconhecidas = set(dados) - CHAVES
if desconhecidas:
raise ConfigError("estrutura: chave não reconhecida")
resultado: dict[str, object] = {}
if "limite" in dados:
valor = dados["limite"]
if type(valor) is not int: # bool não é um limite aceito
raise ConfigError("limite: inteiro esperado")
resultado["limite"] = valor
if "detalhado" in dados:
valor = dados["detalhado"]
if type(valor) is not bool:
raise ConfigError("detalhado: booleano esperado")
resultado["detalhado"] = valor
if "arquivo_dados" in dados:
valor = dados["arquivo_dados"]
if not isinstance(valor, (str, Path)):
raise ConfigError("arquivo_dados: caminho esperado")
resultado["arquivo_dados"] = caminho(valor, base)
return resultado
def carregar_toml(config: Path | None, cwd: Path) -> dict[str, object]:
caminho_config = config if config is not None else cwd / "config.toml"
if not caminho_config.exists():
if config is None:
return {}
raise ConfigError("arquivo de configuração solicitado não foi encontrado")
try:
with caminho_config.open("rb") as arquivo:
documento = tomllib.load(arquivo)
except tomllib.TOMLDecodeError as erro:
raise ConfigError("arquivo TOML: sintaxe inválida") from erro
except OSError as erro:
raise ConfigError("arquivo TOML: não foi possível ler") from erro
tabela = documento.get("aplicacao", {})
if not isinstance(tabela, dict):
raise ConfigError("arquivo TOML: [aplicacao] deve ser uma tabela")
return normalizar_bruto(tabela, caminho_config.parent)
def booleano(texto: str, chave: str) -> bool:
normalizado = texto.strip().lower()
if normalizado == "true":
return True
if normalizado == "false":
return False
raise ConfigError(f"{chave}: use true ou false")
def carregar_ambiente(ambiente: Mapping[str, str], cwd: Path) -> dict[str, object]:
resultado: dict[str, object] = {}
if "APP_LIMITE" in ambiente:
try:
resultado["limite"] = int(ambiente["APP_LIMITE"])
except ValueError as erro:
raise ConfigError("APP_LIMITE: inteiro esperado") from erro
if "APP_DETALHADO" in ambiente:
resultado["detalhado"] = booleano(ambiente["APP_DETALHADO"], "APP_DETALHADO")
if "APP_ARQUIVO_DADOS" in ambiente:
resultado["arquivo_dados"] = caminho(ambiente["APP_ARQUIVO_DADOS"], cwd)
return resultado
def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("--config", type=Path, default=argparse.SUPPRESS)
p.add_argument("--limite", type=int, default=argparse.SUPPRESS)
p.add_argument("--arquivo-dados", dest="arquivo_dados", default=argparse.SUPPRESS)
grupo = p.add_mutually_exclusive_group()
grupo.add_argument("--detalhado", action="store_true", default=argparse.SUPPRESS)
grupo.add_argument("--sem-detalhes", dest="detalhado", action="store_false", default=argparse.SUPPRESS)
return p
def resolver(argv: list[str], ambiente: Mapping[str, str] | None = None, cwd: Path | None = None) -> Config:
diretorio = (cwd or Path.cwd()).resolve()
argumentos = vars(parser().parse_args(argv))
selecionado = argumentos.pop("config", None)
arquivo = caminho(selecionado, diretorio) if selecionado is not None else None
cli = normalizar_bruto(argumentos, diretorio)
ambiente_normalizado = carregar_ambiente(os.environ if ambiente is None else ambiente, diretorio)
toml = carregar_toml(arquivo, diretorio)
combinada = dict(PADROES)
combinada.update(toml)
combinada.update(ambiente_normalizado)
combinada.update(cli)
if combinada["limite"] < 0:
raise ConfigError("limite: deve ser maior ou igual a zero")
return Config(**combinada)
def iniciar(argv: list[str], criar_componentes: Callable[[Config], object]) -> object:
"""A composição só ocorre depois de resolver e validar a configuração."""
return criar_componentes(resolver(argv))
Para observar um caminho relativo ao arquivo, crie a pasta exemplo e salve este conteúdo em exemplo/config.toml. O caminho final de arquivo_dados será exemplo/armazenamento/registros.json, mesmo se você executar Python a partir da pasta acima.
[aplicacao]
limite = 25
detalhado = true
arquivo_dados = "armazenamento/registros.json"
Exemplo
No diretório que contém a pasta exemplo, execute no terminal:
python -c "from config import resolver; print(resolver(['--config', 'exemplo/config.toml', '--limite', '0', '--sem-detalhes'], {'APP_LIMITE': '40', 'APP_DETALHADO': 'true'}))"
O resultado contém limite=0 e detalhado=False: os dois valores foram fornecidos explicitamente na CLI e vencem o ambiente e o TOML. Já arquivo_dados vem do TOML e fica ancorado na pasta exemplo.
Não há instalação nem empacotamento nesta prática: você está executando o módulo local diretamente.
Dica
0 e False não podem ser removidos por testes de “verdade” como if valor ou por expressões com or. Aqui eles permanecem porque a sobreposição consulta a presença da chave no dicionário de argumentos.
Salve o bloco a seguir como test_config.py, na mesma pasta de config.py. Os testes controlam o ambiente passando um dicionário e usam tmp_path para isolar arquivos. Eles cobrem padrões, conflito entre fontes, omissão versus valores falsos, ausência de arquivo, representação inválida, regra semântica e o bloqueio da composição.
from pathlib import Path
import pytest
from config import ConfigError, iniciar, resolver
def escrever_toml(pasta: Path, texto: str) -> Path:
arquivo = pasta / "config.toml"
arquivo.write_text(texto, encoding="utf-8")
return arquivo
def test_usa_padroes_quando_config_convencional_nao_existe(tmp_path: Path) -> None:
config = resolver([], {}, tmp_path)
assert config.limite == 10
assert config.detalhado is False
assert config.arquivo_dados == tmp_path / "dados.json"
def test_cli_vence_arquivo_ambiente_e_padroes(tmp_path: Path) -> None:
arquivo = escrever_toml(
tmp_path,
'[aplicacao]\nlimite = 20\ndetalhado = true\narquivo_dados = "toml.json"\n',
)
config = resolver(
["--config", str(arquivo), "--limite", "0", "--sem-detalhes", "--arquivo-dados", "cli.json"],
{"APP_LIMITE": "30", "APP_DETALHADO": "true", "APP_ARQUIVO_DADOS": "ambiente.json"},
tmp_path,
)
assert (config.limite, config.detalhado) == (0, False)
assert config.arquivo_dados == tmp_path / "cli.json"
def test_caminho_do_toml_e_relativo_ao_proprio_arquivo(tmp_path: Path) -> None:
pasta_config = tmp_path / "configs"
pasta_config.mkdir()
arquivo = escrever_toml(pasta_config, '[aplicacao]\narquivo_dados = "dados/base.json"\n')
config = resolver(["--config", str(arquivo)], {}, tmp_path)
assert config.arquivo_dados == pasta_config / "dados/base.json"
def test_arquivo_convencional_ausente_e_aceito_mas_solicitado_nao(tmp_path: Path) -> None:
assert resolver([], {}, tmp_path).limite == 10
with pytest.raises(ConfigError, match="solicitado"):
resolver(["--config", "nao-existe.toml"], {}, tmp_path)
def test_erro_de_representacao_falha_mesmo_com_cli_superior(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="APP_LIMITE"):
resolver(["--limite", "3"], {"APP_LIMITE": "muitos"}, tmp_path)
def test_restricao_semantica_incide_sobre_o_valor_vencedor(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="maior ou igual"):
resolver(["--limite", "-1"], {}, tmp_path)
assert resolver(["--limite", "2"], {"APP_LIMITE": "-1"}, tmp_path).limite == 2
def test_falha_impede_a_composicao(tmp_path: Path) -> None:
chamadas: list[object] = []
def sentinela(config: object) -> object:
chamadas.append(config)
return object()
with pytest.raises(ConfigError):
iniciar(["--limite", "-1"], sentinela)
assert chamadas == []
Exemplo
Com pytest disponível no seu ambiente, execute:
python -m pytest -q
A expectativa é ver sete testes aprovados. Se algum falhar, leia a asserção: ela aponta qual parte do contrato — precedência, referência de caminho, ausência ou validação — deixou de ser respeitada.
Execute os testes localmente ou analise o código com atenção. Em poucas frases, explique: (1) qual fonte vence em um conflito; (2) como o exemplo preserva 0 ou False explícitos; e (3) por que a sentinela não é chamada quando a configuração falha.
Escreva pelo menos 180 caracteres (0/180).
Resumo
Parabéns! Você concluiu: Carregar configurações externas com precedência explícita
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks