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 projeto Python perfeito deve:

  1. Ter boa estrutura de arquivos e diretórios para organizar todo o projeto, separando código da aplicação, testes, documentação e configuração do projeto.
  2. Usar ambientes virtuais para que o projeto seja desenvolvido isoladamente, sem interferência externa.
  3. Usar ferramentas de linting para que a análise estática do código identifique defeitos, problemas de formatação, otimização, segurança etc. em um estágio inicial do desenvolvimento
  4. Usar testes automatizados com relatórios sobre a cobertura dos testes.
  5. Usar integração contínua para verificar a qualidade do código assim que chega ao servidor.
  6. Usar um controle de versão devidamente ajustado para ignorar arquivos que não devem ser versionados.

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 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:

  • blue: formatador de código baseado no black
  • flake8: ferramenta de análise estática de código Python
  • flake8-debugger: plugin do flake8 para verificar comandos de depuração esquecidos no código
  • flake8-pytest-style: plugin do flake8 para verificar padrões de estilo de testes do pytest.
  • isort: ferramenta para ordenar imports do Python
  • mypy: ferramenta de análise estática de tipos
  • pep8-naming: plugin para flake8 que verifica convenção de nomes conforme PEP-8
  • pyupgrade: ferramenta para atualizar sintaxe para versões mais novas do Python

E algumas ferramentas adicionais específicas para segurança:

  • bandit: ferramenta para encontrar falhas comuns de segurança no código Python
  • pip-audit: ferramenta para escanear ambientes Python em busca de pacotes com vulnerabilidade conhecida

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=="*" \
                   blue==0.5.2 flake8=="*" flake8-debugger=="*" \
                   flake8-pytest-style=="*" isort=="*" mypy=="*" \
                   pep8-naming=="*" pyupgrade=="*" \
                   bandit=="*" pip-audit=="*"

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.isort]
profile = "black"
line_length = 100
[tool.blue]
line-length = 100
[tool.pytest.ini_options]
filterwarnings = ["ignore::DeprecationWarning"]
[tool.mypy]
ignore_missing_imports = true
disallow_untyped_defs = true

Algumas observações:

  1. PEP8 estabelece um guia de estilo para código Python mas deixa espaço para algumas variações de formatação. Ao estabelecer profile = "black" na linha 2, a formatação do isort é ajustada para ficar mais compatível com a produzida pelo black/blue.
  2. Linhas 3 e 6 alteram o tamanho da linha do valor padrão 79 para 100.
  3. 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).

Ao contrário das demais ferramentas adotadas, flake8 não pode ser configurado no pyproject.toml. Vamos usar um arquivo de nome .flake8 então:

[flake8]
max_line_length = 100
exclude = .venv,.mypy_cache,.pytest_cache
ignore = PT013,PT018,W503

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:
    @echo
    isort --diff -c .
    @echo
    blue --check --diff --color .
    @echo
    flake8 .
    @echo
    mypy .
    @echo
    bandit -r projeto_x/
    @echo
    pip-audit

format:
    isort .
    blue .
    pyupgrade --py310-plus **/*.py

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 isort, blue e pyupgrade.

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@v2
          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: 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 (linha 33)
  • Executar a tarefa de teste (linha 35)

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. 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.

Essa verificação local será automatizada através de hooks do controle de versão para disparar ações de acordo com algum evento. Vamos precisar de dois eventos:

  1. pre-commit executará make lint
  2. pre-push executará make test

Em ambos os casos, se alguma falha for encontrada, a operação do controle de versão é cancelada.

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:

install_hooks:
    scripts/install_hooks.sh

O script install_hooks.sh localizado no subdiretório scripts:

#!/usr/bin/env bash
GIT_PRE_COMMIT='#!/bin/bash
cd $(git rev-parse --show-toplevel)
poetry run make lint
'
GIT_PRE_PUSH='#!/bin/bash
cd $(git rev-parse --show-toplevel)
poetry run make test
'
HG_HOOKS='[hooks]
precommit.lint = (cd `hg root`; poetry run make lint)
pre-push.test = (cd `hg root`; poetry run make test)
'
if [ -d '.git' ]; then
    echo "$GIT_PRE_COMMIT" > .git/hooks/pre-commit
    echo "$GIT_PRE_PUSH" > .git/hooks/pre-push
    chmod +x .git/hooks/pre-*
elif ! grep -s -q 'precommit.lint' '.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, 8-11) quanto os comandos usados no Mercurial (linhas 13-16) 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 18-21). Caso contrário, os hooks do Mercurial são criados no diretório .hg/hgrc (linhas 22-23).
  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

Comentários

Comments powered by Disqus