Pular para o conteúdo

    Capítulo 49, Avançado

    Empacotamento e estrutura de projeto

    Transformar código em algo instalável, versionável e publicável. Mesmo que você nunca publique no PyPI, a estrutura de pacote resolve problemas de importação e de testes.

    Código deste capítulo: avancado/cap49_empacotamento.py

    Por que usar o layout src

    Colocar o pacote dentro de uma pasta src/ evita um erro sutil: com o código na raiz, os testes importam a pasta local por acidente e passam mesmo que o pacote instalado esteja quebrado. Com src/, a única forma de importar é instalando o pacote, e então você testa o que o usuário vai receber.

    Estrutura do pacote de exemplo
    exemplos/pacote/
      pyproject.toml
      README.md
      src/
        calc_notes/
          __init__.py
          operacoes.py
          cli.py
      tests/
        test_operacoes.py
    

    O pyproject.toml

    Tudo vive em um arquivo. A seção [build-system] diz como construir. A seção [project] descreve o pacote. E [project.scripts] cria um comando de terminal ligado a uma função:

    exemplos/pacote/pyproject.toml
    [build-system]
    requires = ["hatchling"]
    build-backend = "hatchling.build"
    
    [project]
    name = "calc-notes"
    version = "0.1.0"
    description = "Calculadora de exemplo do livro Python na Prática"
    readme = "README.md"
    requires-python = ">=3.12"
    dependencies = []
    
    [project.scripts]
    calc-notes = "calc_notes.cli:main"
    
    [dependency-groups]
    dev = ["pytest>=8"]
    

    O código do pacote

    exemplos/pacote/src/calc_notes/operacoes.py
    def somar(a: float, b: float) -> float:
        return a + b
    
    
    def dividir(a: float, b: float) -> float:
        if b == 0:
            raise ZeroDivisionError("divisor não pode ser zero")
        return a / b
    
    exemplos/pacote/src/calc_notes/__init__.py
    """Pacote de exemplo do livro Python na Prática."""
    
    from importlib.metadata import PackageNotFoundError, version
    
    from calc_notes.operacoes import dividir, somar
    
    try:
        __version__ = version("calc-notes")
    except PackageNotFoundError:
        __version__ = "0+local"
    
    __all__ = ["__version__", "dividir", "somar"]
    

    Eu leio a versão dos metadados do pacote instalado em vez de repeti-la no código. Assim existe uma só fonte da verdade, o pyproject.toml.

    exemplos/pacote/src/calc_notes/cli.py
    import argparse
    
    from calc_notes.operacoes import somar
    
    
    def main(argv: list[str] | None = None) -> int:
        parser = argparse.ArgumentParser(prog="calc-notes")
        parser.add_argument("a", type=float)
        parser.add_argument("b", type=float)
        args = parser.parse_args(argv)
        print(somar(args.a, args.b))
        return 0
    
    exemplos/pacote/tests/test_operacoes.py
    import pytest
    
    from calc_notes import dividir, somar
    
    
    def test_somar():
        assert somar(2, 3) == 5
    
    
    def test_dividir_por_zero():
        with pytest.raises(ZeroDivisionError):
            dividir(1, 0)
    
    exemplos/pacote/README.md
    calc-notes
    
    Calculadora de exemplo do livro Python na Prática.
    

    Instalar em modo editável e usar

    O modo editável instala o pacote apontando para o seu código-fonte, então cada alteração vale na hora, sem reinstalar:

    Terminal
    cd exemplos/pacote
    uv pip install -e .
    
    Terminal
    calc-notes 2 3
    
    Saída
    5.0
    

    Sem o uv, dentro de um ambiente virtual: python -m pip install -e .. Para testar, rode o pytest dentro da pasta do pacote.

    O módulo importlib.metadata consulta os metadados de qualquer pacote instalado, e levanta PackageNotFoundError se ele não existir:

    avancado/cap49_empacotamento.pylinhas 10 a 15
    from importlib.metadata import PackageNotFoundError, version
    
    try:
        print(version("pacote-que-nao-existe"))
    except PackageNotFoundError as erro:
        print("não instalado:", erro)
    
    Saída
    não instalado: No package metadata was found for pacote-que-nao-existe
    

    Construir e publicar

    Um pacote é distribuído em dois formatos: o sdist (código-fonte, .tar.gz) e o wheel (já pronto para instalar, .whl). Qualquer um dos comandos abaixo gera os dois na pasta dist/:

    Terminal
    uv build
    
    Saída
    Building source distribution...
    Building wheel from source distribution...
    Successfully built dist/calc_notes-0.1.0.tar.gz
    Successfully built dist/calc_notes-0.1.0-py3-none-any.whl
    

    Alternativas equivalentes: python -m build (com o pacote build) e poetry build. Para publicar, treine primeiro no TestPyPI, um índice de ensaio:

    Terminal
    uv publish --publish-url https://test.pypi.org/legacy/ --token "$TOKEN"
    

    Também funciona o twine upload --repository testpypi dist/*. Em projetos reais, eu prefiro a publicação confiável (trusted publishing): o GitHub Actions prova a sua identidade ao PyPI por um token temporário, e você nunca guarda uma senha de longa duração.

    Escolher o backend de construção

    BackendQuando usar
    hatchlingEscolha padrão para pacotes em Python puro, simples e configurável
    uv_buildMesmo cenário, muito rápido, integrado ao uv
    setuptoolsProjetos antigos, ou os que têm extensões em C
    poetry-coreProjetos gerenciados pelo Poetry
    maturin, scikit-build-coreExtensões em Rust ou C++

    Bibliotecas e aplicações pensam diferente sobre versões

    Uma aplicação é instalada em um ambiente que você controla. Faça commit do lockfile e fixe as versões. Uma biblioteca é instalada no ambiente de outras pessoas, junto com outras bibliotecas. Declare intervalos amplos (requests>=2.32,<3), porque versões exatas causam conflito para quem usa a sua. O lockfile da biblioteca serve só para o seu desenvolvimento e para os seus testes.

    Exercício 1

    Executar com python -m

    Faça o pacote rodar com python -m calc_notes 2 3, adicionando um arquivo ao pacote.