Pular para o conteúdo principal

Como Começar um Projeto Python Perfeito

Começar um novo projeto Python não precisa ser um desafio porque as necessidades básicas são sempre iguais mesmo para tipos de projetos diferentes. Este artigo apresenta como criar uma base inicial perfeita que pode ser usada para qualquer projeto Python.

Definição da Base Inicial Perfeita

A base de um Python perfeito deve ter os seguintes recursos:

  1. Estrutura adequada de arquivos e diretórios para organizar o projeto, separando o código da aplicação, testes, documentação, configuração do projeto etc.
  2. Ambiente virtual para criar uma instalação isolada e independente do Python e das bibliotecas necessárias para o projeto, sem conflitos ou interferência do sistema ou de outros projetos.
  3. Ferramentas de linter para manter a qualidade e a consistência do projeto através da análise estática do código para identificar defeitos, problemas de formatação, otimização, segurança e outros problemas no início do estágio de desenvolvimento.
  4. Testes automatizados para garantir a qualidade do código e detectar defeitos precocemente. É importante ter uma suíte de testes que cubra todos os aspectos da aplicação, juntamente com relatórios que indiquem a porcentagem de código coberta pelos testes.
  5. Controle de versão para registrar as alterações no código-fonte e permitir a colaboração entre os desenvolvedores. É importante configurar devidamente o controle de versão para ignorar arquivos que não devem ser versionados, como arquivos temporários ou código compilado.
  6. Integração contínua (CI) para garantir a qualidade do código executando testes automatizados e realizando outras verificações no código sempre que ele é enviado para o servidor.

Tirando a estrutura de arquivos e diretórios que é única, todos os demais itens dependem de escolhas. E existem muitas opções. O gerenciamento de ambientes virtuais, por exemplo, podem ser feito com venv, pipenv, poetry ou conda. Há dezenas de ferramentas de linting tais como ruff, flake8, pylint, mypy etc. que são equivalentes ou complementares. No fim das contas, as escolhas que formam uma ou outra combinação dependem de decisões técnicas e pessoais.

Gerenciamento de Ambientes Virtuais

O gerenciamento de versões do Python, de ambiente virtuais e dependências será feito através da combinação pyenv + poetry (leia o artigo anterior).

Estrutura Inicial de Diretórios

Para criar a estrutura inicial do seu projeto, use poetry new <nome_projeto>:

$ poetry new projeto_x

O comando anterior cria a seguinte estrutura de diretórios:

projeto_x
├── projeto_x
│   └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
    ├── __init__.py
    └── test_projeto_x.py

Esta é uma estrutura mínima de arquivos e diretórios muito boa porque separa claramente o código específico do projeto no subdiretório projeto_x do código apenas relacionado com testes no diretório tests, e dos arquivos de configuração e documentação do projeto (pyproject.toml e README.rst). Contudo, alguns ajustes são necessários:

  1. O arquivo README.rst vem vazio e você precisa completá-lo. A criação desse tipo de arquivo foge do escopo deste artigo, mas você encontra boas dicas e mais informações em 1 e 2.
  2. Edite o arquivo pyproject.toml e mude as definições que foram criadas automaticamente para name, version, description e authors.
  3. Verifique se a versão do Python definida na seção [tool.poetry.dependencies] em pyproject.toml é a versão desejada. poetry new usa a versão do ambiente, mas você pode instalar e especificar outras versões do Python através do pyenv.

Ferramentas de Linting e Teste

O conjunto mínimo recomendado de ferramentas de teste é:

  • pytest: ferramenta de teste para Python
  • pytest-cov: plugin do pytest para medir cobertura de código

Para linting, recomendo o uso de:

  • mypy: ferramenta de análise estática de tipos
  • pip-audit: ferramenta para escanear ambientes Python em busca de pacotes com vulnerabilidade conhecida
  • ruff: Ferramenta de linting para projetos Python extremamente rápida (escrita em Rust). Substitui outras ferramentas tais como blue, black, flake8, isort, pep-naming, pyupgrade e bandit.

Instalação e Configuração

Todas as bibliotecas e ferramentas relacionadas a atividades de teste e linting são necessárias para o desenvolvimento do projeto, mas não para seu funcionamento em produção. Devem ser instaladas em uma seção separada em pyproject.toml para não se misturar com as dependências essenciais. A instalação dessas bibliotecas e ferramentas deve ser feita através do comando poetry add --dev:

$ poetry add --dev pytest=="*" pytest-cov=="*" \
                mypy=="*" pip-audit=="*" ruff=="*"

Configuração

A configuração da maioria das dependências pode ser mantida no arquivo pyproject.toml, em seções nomeadas seguindo o padrão [tool.<nome-da-ferramenta>]:

[tool.pytest.ini_options]
filterwarnings = ["ignore::DeprecationWarning"]
[tool.mypy]
ignore_missing_imports = true
disallow_untyped_defs = true
[tool.ruff]
line-length = 100
select = [
    "A",
    "ARG",
    "B",
    "C",
    "C4",
    "E",
    "ERA",
    "F",
    "I",
    "N",
    "PLC",
    "PLE",
    "PLR",
    "PLW",
    "RET",
    "S",
    "T10",
    "UP",
    "W",
]
ignore = ["A003"]
target-version = "py310"
[tool.ruff.format]
quote-style = "single"
[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"]
"tests/**" = ["ARG", "S"]

Algumas observações:

  1. Linhas 2 e 12 alteram o tamanho da linha do valor padrão 79 para 100.
  2. mypy possui diversas opções de configuração. A opção ignore_missing_imports suprime mensagens de erro sobre importações que não podem ser resolvidas (linha 12). A opção disallow_untyped_defs não permite a definição de funções sem anotações de tipo ou com anotações de tipo incompletas (linha 13).
  3. ruff é capaz de verificar as regras usadas por diversas outras ferramentas de linting.

Automação

Testes e linting devem ser fáceis de serem executados, sem a necessidade de lembrar cada comando e seus argumentos. Para isso, eu recomendo usar um Makefile com as tarefas necessárias:

test:
    pytest --cov-report term-missing --cov-report html --cov-branch \
           --cov projeto_x/


lint:
    ruff check --diff .
    @echo
    ruff format --diff .
    @echo
    mypy .


format:
    ruff check --silent --exit-zero --fix .
    @echo
    ruff format .


audit:
    pip-audit

E aí, é só usar o comando make para executar as tarefas:

  • make test executa os testes e gera relatórios de cobertura dos testes.
  • make lint executa o linting usando diversas ferramentas em sequência.
  • make format formata o código Python de acordo com os padrões usados por blue e ruff.
  • make audit verifica se existem pacotes com vulnerabilidades conhecidas.

Podemos usar esses mesmos comandos nos scritps de hook do controle de versão e na configuração do sistema de integração contínua. Dessa forma, mantemos um único arquivo com os comandos e os demais usando esse arquivo.

Configuração do Sistema de Integração Contínua

A maioria dos sistemas de integração contínua modernos mantém sua configuração junto com o código-fonte. GitHub Actions, por exemplo, mantém sua configuração em arquivos no formato yaml dentro do diretório .github/workflows, na raiz do projeto.

Crie o arquivo .github/workflows/continuous_integration.yml com o seguinte conteúdo:

name: Continuous Integration
on: [push]
jobs:
  lint_and_test:
    runs-on: ubuntu-latest
    steps:
        - name: Set up python
          uses: actions/setup-python@v3
          with:
              python-version: '3.10'
        - name: Check out repository
          uses: actions/checkout@v2
        - name: Install Poetry
          uses: snok/install-poetry@v1
          with:
              virtualenvs-in-project: true
        - name: Load cached venv
          id: cached-poetry-dependencies
          uses: actions/cache@v2
          with:
              path: .venv
              key: venv-${{ hashFiles('**/poetry.lock') }}
        - name: Install dependencies
          if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
          run: poetry install --no-interaction
        - name: Lint
          run: poetry run make lint
        - name: Auditing
          run: poetry run make audit
        - name: Run tests
          run: poetry run make test

Essa configuração funciona da seguinte forma:

  • Esse fluxo será executado toda vez que o repositório receber um push (linha 2).
  • O fluxo será executado em um sistema operacional Ubuntu, na última versão disponível (linha 5).
  • Use a versão 3.10 do Python (linha 11).
  • Em seguida, instale poetry (linha 16) e configure-o para usar ambientes virtuais em diretórios .venv (linha 19).
  • Para evitar ter de reinstalar as mesmas dependências toda vez, vamos criar uma política de cache para o diretório .venv (linha 25). A chave que identifica o cache é formado pela combinação da palavra venv e o hash do conteúdo de poetry.lock (linha 26).
  • As dependências são instaladas apenas se o cache não for encontrado (linhas 28 a 30). Caso contrário, o cache de venv é usado.
  • Executar a tarefa de lint, audit e teste (linhas 32 a 39)

Eventos de pre-commit e pre-push

É uma boa prática fazer a verificação da qualidade do código localmente mesmo que a integração contínua refaça o processo no servidor. Isso economiza tempo porque o resultado é imediato e as correções podem ser feitas sem ter de passar por um ciclo de integração contínua.

A verificação local deve acontecer antes do compartilhamento das alterações com outros desenvolvedores ou com o repositório oficial do projeto. Podemos automatizar esse processo através de hooks do controle de versão. Os mais adequados são pre-commit e pre-push.

Configuração pre-commit pre-push
1 make lint make test && make audit
2 make lint && make test && make audit

Na configuração 1, a análise estática é feita antes de cada commit. Os testes e a auditoria, por serem mais demorados, só são feitos antes do push. A vantagem dessa distribuição é que toda revisão local é verificada e formatada. Por outro lado, executar make lint antes de cada commit pode ser meio irritante dependendo do seu fluxo de trabalho.

Na configuração 2, o linting, os testes e a auditoria são feitos apenas antes do push. O fluxo de trabalho é mais fluído porém as revisões locais podem ser inconsistentes. É necessário criar uma revisão adicional com os ajustes necessários em caso de falha na execução do pre-push.

Para facilitar a vida do desenvolvedor, vamos adicionar a tarefa install_hooks ao Makefile, que chama o script scripts/install_hooks.sh para criar os hooks na configuração 2:

install_hooks:
    scripts/install_hooks.sh

scripts/install_hooks.sh contém:

#!/usr/bin/env bash
GIT_PRE_PUSH='#!/bin/bash
cd $(git rev-parse --show-toplevel)
poetry run make lint && poetry run make test && poetry run make audit
'
HG_HOOKS='[hooks]
pre-push.lint_test = (cd `hg root`; poetry run make lint && poetry run make test && poetry run make audit)
'
if [ -d '.git' ]; then
    echo "$GIT_PRE_PUSH" > .git/hooks/pre-push
    chmod +x .git/hooks/pre-*
elif ! grep -s -q 'pre-push.lint_test' '.hg/hgrc'; then
    echo "$HG_HOOKS" >> .hg/hgrc
fi

Algumas explicações:

  1. No Git, hooks são arquivos executáveis nomeados de acordo com o evento desejado, localizados em .git/hooks.
  2. No Mercurial, hooks são definidos na seção [hooks] no arquivo de configuração .hg/hgrc, em que cada hook pode ser comandos ou funções Python.
  3. Tanto os scripts em bash (linhas 3-6) quanto os comandos usados no Mercurial (linhas 8-10) fazem a mesma coisa: mudam o diretório corrente para a raiz do projeto, onde está localizado Makefile, e executa o comando poetry run make <tarefa>. Lembre-se que poetry run <comando> executa o comando dentro do contexto do ambiente virtual do projeto.
  4. Se existir um diretório .git, então os hooks do Git são criados no diretório .git/hooks (linhas 12-14). Caso contrário, os hooks do Mercurial são criados no diretório .hg/hgrc (linhas 15-16).
  5. O trecho de código apresentado atende tanto quem usa Mercurial (meu caso) quanto quem usa Git. Você pode retirar algumas partes se você e sua equipe usam apenas um ou outro.

Preparando o Controle de Versão

Para evitar que arquivos indesejados sejam adicionados por engano ao controle de versão, é preciso criar uma lista de filtros em um arquivo especial localizado na raiz do projeto, no mesmo nível do diretório .hg ou .git dependendo de qual ferramenta você usa. Se você usa o Mercurial, este arquivo deve-se chamar .hgignore e deve conter:

syntax: glob

.venv
.env
*~
*.py[cod]
*.orig

# Unit test / coverage reports
.coverage
htmlcov/

# cache
__pycache__
.mypy_cache
.pytest_cache

Se você usa Git, o nome do arquivo deve ser .gitignore e contém as mesmas linhas acima menos a primeira linha (syntax: glob), que deve ser removida.

Com os filtros definidos, podemos iniciar o controle de versão. Para o Mercurial, os comandos são:

$ hg init .
$ poetry run make install_hooks
$ hg commit -Am 'Estrutura inicial do projeto'

Para quem usa Git, execute:

$ git init .
$ poetry run make install_hooks
$ git add -A .
$ git commit -m 'Estrutura inicial do projeto'

E tudo está pronto para enviar ao repositório oficial do projeto no GitHub.

Gabarito Pronto Para Uso no GitHub

É importante saber os passos para criar o projeto Python perfeito. Mas ao invés de executar esses mesmos passos a cada novo projeto, você pode simplesmente usar o gabarito que eu disponibilizei no Github. As instruções de uso estão no README.rst.

Considerações Finais

A base de projeto apresentada neste artigo funciona muito bem e pode ser facilmente adaptada para outras ferramentas se você quiser tentar uma combinação diferente. O importante é manter a estrutura do projeto e as atividades de linting e teste automatizadas.

Referências

1 Make a README
2 READMEs on READMEs (and other README-related resources)
3 How to set up a perfect Python project

Próximo artigo: Projeto FastAPI Mínimo

Artigo anterior: Gerenciamento de Versões, Ambiente Virtuais e Dependências com pyenv e poetry

Comentários

Comments powered by Disqus