“`html
Prática e confiável
generate_files.py
Um guia técnico (e bem direto ao ponto) para você criar e revisar um script generate_files.py
que gera arquivos de forma previsível, segura e fácil de manter — com boas decisões de caminho, escrita, validação e idempotência.
1) Defina o “contrato” do script: entradas, saídas e idempotência
Antes de escrever qualquer linha, eu defino o que generate_files.py realmente entrega.
Isso evita scripts “mágicos” que mudam o repositório sem controle.
Um contrato bem definido normalmente cobre:
- Entradas: diretório base, templates/strings, lista de arquivos, modo (
dry-run,force). - Saídas: quais caminhos serão criados/atualizados (por exemplo:
src/,tests/,docs/). - Idempotência: rodar duas vezes não deve causar diffs desnecessários (conteúdo igual → arquivo igual).
- Falha segura: se der erro no meio, você não quer um estado “meio gerado” sem aviso.
Dica: se o conteúdo do arquivo é derivado de dados determinísticos (templates fixos, nomes conhecidos), você consegue comparar e só escrever quando muda.
2) Caminhos robustos: use pathlib e restrinja o “root”
Problemas clássicos em geradores surgem quando o script monta caminhos com str e concatenação.
Eu prefiro pathlib e sempre trabalho com um “root” (diretório base) explícito.
Regras que eu sigo:
- Normalize: converta para
Path.resolve()e compare se o destino ainda está dentro do root. - Crie diretórios: use
mkdir(parents=True, exist_ok=True)apenas no nível necessário. - Evite path traversal: se algum nome vem de entrada (mesmo que “confiável”), valide para não sair do diretório base.
- Compatibilidade: todo caminho deve ser construído com
root / "subdir" / arquivo.
3) Escrita com controle: compare conteúdo antes de gravar
Para manter o repositório limpo, eu imponho uma regra: se o conteúdo não mudou, não escreva.
Isso reduz diffs, melhora o comportamento em CI e evita “touch” acidental.
O fluxo típico é:
- gerar o conteúdo em memória (string) →
- ler o arquivo existente (se existir) →
- comparar (mesmo encoding, mesma string) →
- escrever somente quando necessário.
Além disso, eu prefiro abrir arquivos com encoding explícito (ex.: utf-8) e usar newline="\n"
para manter consistência.
Resultado prático: o script fica idempotente e “barulhento” apenas quando realmente há mudança.
4) Fluxo de execução: validação, logs úteis e modo dry-run
Um gerador bom não é só “funciona”; ele também é diagnosticável.
Eu adiciono logs objetivos e um modo dry-run para visualizar o que seria feito.
Um padrão útil de validação antes de escrever:
- verifique se os dados de entrada (nomes, templates, listas) não estão vazios;
- valide extensões permitidas (ex.:
.py,.md); - valide se a lista de arquivos é única (sem duplicatas que causem sobrescrita acidental);
- em caso de erro, falhe cedo com mensagens claras sobre qual arquivo/etapa quebrou.
E para logs, eu costumo diferenciar:
gerado/atualizado
mantido (sem mudança)
dry-run (simulado)
Exemplo: escrita idempotente com segurança de paths e dry-run
Aqui vai um núcleo de generate_files.py que eu uso como base:
ele garante que o destino está dentro do root, cria diretórios, compara conteúdo e respeita dry-run.
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class WritePlan:
rel_path: str
content: str
def safe_join(root: Path, rel_path: str) -> Path:
root = root.resolve()
target = (root / rel_path).resolve()
# Evita sair do diretório base (path traversal)
if root not in target.parents and target != root:
raise ValueError(f"Destino fora do root: {rel_path}")
return target
def write_if_changed(root: Path, rel_path: str, content: str, *, dry_run: bool = False) -> str:
target = safe_join(root, rel_path)
if not target.parent.exists():
if dry_run:
# Em dry-run, eu só simulo
return "dry-run:create-dir"
target.parent.mkdir(parents=True, exist_ok=True)
encoding = "utf-8"
existing = None
if target.exists():
existing = target.read_text(encoding=encoding)
if existing == content:
return "kept:no-change"
if dry_run:
return "dry-run:write"
target.write_text(content, encoding=encoding, newline="
")
return "updated:write"
def main() -> None:
project_root = Path(__file__).resolve().parent
output_root = project_root / "generated"
plans = [
WritePlan(
rel_path="__init__.py",
content="\"\"\"Arquivos gerados.\"\"\"\n",
),
WritePlan(
rel_path="example.py",
content=(
"def hello():\n"
" return 'ok'\n"
),
),
]
# Troque para True quando quiser só visualizar
dry_run = False
for plan in plans:
status = write_if_changed(
output_root,
plan.rel_path,
plan.content,
dry_run=dry_run,
)
print(f"{status:>18} {plan.rel_path}")
if __name__ == "__main__":
main()
Esse esqueleto resolve os pontos mais comuns:
root seguro, comparação antes de gravar, dry-run e criação de diretórios.
Quer evoluir seu gerador e sua base de código?
Se esse tema te ajudou, lê mais posts por aqui. Eu vou te mostrando padrões práticos para
manter projetos previsíveis, com menos surpresas em CI e diffs mais limpos.
“`
Sou Apaixonado pela programação e estou trilhando o caminho de ter cada diz mais conhecimento e trazer toda minha experiência vinda do Design para a programação resultando em layouts incríveis e idéias inovadoras! Conecte-se Comigo!