Guia do pyproject.toml: empacotamento, dependências e ferramentas

Tradução automática

Este artigo foi traduzido automaticamente a partir da versão original em inglês.

Os projetos Python mais antigos costumam espalhar a configuração por setup.py, setup.cfg, ficheiros de requisitos, MANIFEST.in e ficheiros separados para as ferramentas de desenvolvimento. O pyproject.toml dá a todas estas áreas um local comum, embora não substitua todos os ficheiros de um projeto nem faça com que todas as secções sigam um único padrão.

Este guia destina-se a programadores Python que mantêm um pacote ou uma aplicação. Vai ficar a saber o que contém cada uma das quatro tabelas principais ([build-system], [project], [dependency-groups], [tool.*]), em que diferem de um lockfile e do requirements.txt, que backend de build escolher, e como migrar um projeto antigo e configurar um novo com o uv.

Um ficheiro, quatro tabelas

Três padrões de empacotamento estabeleceram a estrutura base:

  • O PEP 518 define os requisitos do sistema de build.
  • O PEP 621 define os metadados do projeto.
  • O PEP 735 define os grupos de dependências não publicados.

Os autores de ferramentas também podem reservar um espaço de nomes dentro de [tool]. Nada normaliza o que lá se coloca: cada ferramenta define as suas próprias chaves e comportamento.

A figura divide o ficheiro de exemplo da secção seguinte nas suas quatro tabelas e mostra que programa lê cada uma. Só o [project] chega aos utilizadores, através dos metadados do wheel construído.

O pyproject.toml de exemplo dividido nas suas quatro tabelas: o build frontend lê build-system, o hatchling transforma project em metadados do wheel que o pip instala para o utilizador, o uv sync instala o grupo test no .venv, e o pytest e o ruff leem as suas próprias tabelasO pyproject.toml de exemplo dividido nas suas quatro tabelas: o build frontend lê build-system, o hatchling transforma project em metadados do wheel que o pip instala para o utilizador, o uv sync instala o grupo test no .venv, e o pytest e o ruff leem as suas próprias tabelas

Aqui está uma pequena biblioteca empacotada:

[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"

[project]
name = "weather-client"
version = "0.1.0"
description = "A small weather API client"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
  "httpx>=0.27",
]

[project.optional-dependencies]
cli = ["rich>=13"]

[project.scripts]
weather = "weather_client.cli:main"

[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 100
target-version = "py311"

O ficheiro tem quatro listas de dependências. dependencies indica o que o pacote precisa em runtime, optional-dependencies indica o que os utilizadores podem escolher instalar, [dependency-groups] indica o que os contribuidores instalam localmente e [build-system].requires indica o que o backend precisa para construir.

[build-system]: como o código-fonte se torna uma distribuição

Um frontend de build, como python -m build, o pip ou o uv, invoca um backend de build. O backend decide como a árvore de código-fonte se torna um sdist ou um wheel e que ficheiros entram nesses artefactos. No exemplo, build-backend indica o hatchling e requires lista o que o frontend instala para o executar.

Um sdist é um arquivo de código-fonte que o instalador constrói antes da instalação; um wheel contém os ficheiros prontos a instalar.

O build decorre por esta ordem. O frontend lê [build-system], instala requires num ambiente de build temporário e executa o backend. O backend lê [project] e escreve o sdist e o wheel.

requires contém as dependências necessárias para executar o backend no seu ambiente de build isolado. Não é a lista de dependências de runtime do seu pacote.

Declare um sistema de build quando o projeto produz uma distribuição ou precisa de ter o próprio código instalado como pacote. Um projeto que não é um pacote também pode usar o pyproject.toml; o PEP 735 permite até um ficheiro com apenas grupos de dependências. Sem sistema de build, o uv não instala o projeto no seu próprio ambiente: importar o seu código só funciona a partir da raiz do repositório, e o uv não cria comandos de [project.scripts]. Defina tool.uv.package = true ou adicione um sistema de build quando precisar de qualquer um dos dois (empacotamento de projetos no uv).

Escolher um backend

Escolha o backend consoante o que o build tem de fazer:

BackendUse-o quando
uv_buildO projeto é Python puro, gerido com o uv, e não precisa de plugins de build. O uv init escreve-o.
hatchlingO projeto é Python puro e precisa de plugins de build ou de uma versão obtida do git (hatch-vcs).
flit-coreO projeto é o pacote Python puro mais simples e não precisa de plugins de build.
setuptoolsUm setup.py existente, extensões em C ou um passo de build personalizado têm de continuar a funcionar.
poetry-coreA equipa gere o projeto com o Poetry.
maturinA extensão está escrita em Rust.
scikit-build-coreA extensão é em C ou C++ e é construída com o CMake.
meson-pythonO projeto é construído com o Meson, como o NumPy e o SciPy.

A figura coloca os backends desde os builds que só copiam ficheiros Python até aos builds que compilam código. Para uma versão obtida a partir de tags do Git, use um backend que o suporte, como hatchling com hatch-vcs ou setuptools com setuptools-scm.

Backends de build em quatro colunas, de Python puro sem plugins (uv_build, flit-core) a Python puro com plugins (hatchling), um build antigo com setup.py (setuptools) e extensões compiladas (maturin, scikit-build-core, meson-python)Backends de build em quatro colunas, de Python puro sem plugins (uv_build, flit-core) a Python puro com plugins (hatchling), um build antigo com setup.py (setuptools) e extensões compiladas (maturin, scikit-build-core, meson-python)

Copie da documentação do backend a tabela que ele recomenda atualmente. Não adicione wheel aos requisitos de build por hábito; o backend declara o que precisa.

[project]: os metadados que os consumidores recebem

A tabela [project] descreve a distribuição: o nome, a versão, a compatibilidade com Python, as dependências de runtime, os pontos de entrada e outros metadados do índice.

Os especificadores de dependências, como httpx>=0.27 no exemplo, são restrições para um resolvedor, não um retrato de um único ambiente. Passam a fazer parte dos metadados do wheel e do sdist, pelo que os instaladores a jusante podem combiná-los com os requisitos de outros pacotes.

Os extras são uma interface de instalação pública

[project.optional-dependencies] define extras que os consumidores podem pedir. Com o extra cli do exemplo, um consumidor pode instalar weather-client[cli]. Como os nomes e os requisitos dos extras são publicados, trate-os como capacidades do produto e dê-lhes nomes nesse sentido, como cli ou postgres. Não use um extra chamado dev apenas para guardar todas as ferramentas dos contribuidores.

Os pontos de entrada ligam os comandos instalados ao Python

A tabela [project.scripts] do exemplo tem uma entrada, weather = "weather_client.cli:main". Depois de a distribuição ser instalada, o ambiente expõe um comando weather que importa weather_client.cli e chama main. O comando weather é instalado mesmo sem o extra cli, por isso o cli.py tem de funcionar sem o rich ou indicar ao utilizador que instale weather-client[cli].

Teste o comando a partir de um wheel construído, e não apenas a partir da raiz do repositório; o wheel é o que os utilizadores recebem. A secção de migração mostra os comandos.

[dependency-groups]: ambientes locais e não publicados

Os grupos de dependências do PEP 735 descrevem ambientes de desenvolvimento ou que não são pacotes, sem os publicar como metadados do pacote.

[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
  { include-group = "test" },
  { include-group = "lint" },
]

É o sítio certo para testes, linters, geradores de documentação e ferramentas semelhantes dos contribuidores. Também é útil para aplicações ou notebooks que não constroem uma distribuição. O include-group permite que um grupo reutilize outro; os extras não têm equivalente.

O PEP 735 não define uma interface de linha de comandos, por isso cada instalador tem as suas próprias opções. Os grupos e os extras instalam-se assim:

# Dependency groups (not published)
pip install --group test        # pip 25.1 and later
uv sync --group test
uv sync --all-groups

# Extras (published)
pip install "weather-client[cli]"
uv sync --extra cli

O pyproject.toml declara dependências; não cria um ambiente. Os comandos que criam um ambiente e instalam o projeto com um grupo diferem consoante a ferramenta:

Fluxo de trabalhoCriar o ambienteInstalar o projeto e o grupo test
pippython -m venv .venv, e depois ativá-lopip install -e . --group test
uvuv sync cria .venv quando este não existeuv sync --group test (também instala o projeto e o dev)

[tool.*]: configuração que pertence a uma só ferramenta

As tabelas de ferramentas não partilham um esquema. No exemplo, [tool.pytest.ini_options] e [tool.ruff] usam cada uma chaves que só a respetiva ferramenta lê. Use uma tabela [tool.*] apenas se a ferramenta a documentar. Estas ferramentas comuns leem as suas definições do pyproject.toml:

TarefaFerramentaTabela
Formatação e lintingRuff[tool.ruff]
Testespytest[tool.pytest.ini_options] ou [tool.pytest]
Verificação de tiposmypy[tool.mypy]
Coberturacoverage.py[tool.coverage.run], [tool.coverage.report]

O pytest 9.0 acrescentou [tool.pytest], que usa tipos TOML nativos, como uma lista para addopts; as versões mais antigas leem apenas [tool.pytest.ini_options].

Algumas definições continuam a pertencer a ficheiros próprios, porque outro programa as lê ou porque o ficheiro precisa de um formato diferente. O flake8 não lê o pyproject.toml; lê .flake8, setup.cfg ou tox.ini. O pre-commit lê .pre-commit-config.yaml.

Declarações, locks e ficheiros de requisitos resolvem problemas diferentes

A figura segue os pacotes do ficheiro de exemplo. Cada lista chega a um ambiente diferente, e os utilizadores recebem apenas as duas listas de [project].

Uma grelha das quatro listas de dependências contra três ambientes: hatchling apenas no ambiente de build temporário, httpx para utilizadores e contribuidores, rich só para utilizadores com o extra cli, e pytest e pytest-cov apenas no .venv do contribuidorUma grelha das quatro listas de dependências contra três ambientes: hatchling apenas no ambiente de build temporário, httpx para utilizadores e contribuidores, rich só para utilizadores com o extra cli, e pytest e pytest-cov apenas no .venv do contribuidor

Cinco locais guardam informação sobre dependências. Cada um tem uma só função:

ArtefactoFinalidade principalConteúdo típico
[project.dependencies]Contrato de runtime publicadoRequisitos diretos e intervalos compatíveis
[project.optional-dependencies]Capacidades opcionais publicadasExtras destinados aos consumidores
[dependency-groups]Ambientes locais não publicadosGrupos de testes, lint, documentação ou aplicação
LockfileReproduzir um ambiente resolvidoVersões exatas, origens e metadados da resolução
requirements.txtEntrada de instalação compatível com o pipRequisitos mais opções, restrições, URLs ou hashes específicos do pip

Uma biblioteca publica normalmente restrições compatíveis e testa contra um intervalo. Uma aplicação costuma fazer commit do lockfile do gestor de ambientes. As versões fixas exatas pertencem ao artefacto de deployment resolvido, e não, às cegas, aos metadados públicos de uma biblioteca. Por exemplo, uma biblioteca declara httpx>=0.27 e o resolvedor do utilizador escolhe a versão. Uma aplicação faz commit do uv.lock e instala-o com uv sync --locked.

Mantenha um ficheiro de requisitos quando uma integração exigir o formato ou as funcionalidades do pip. Se um projeto gerido pelo uv precisar de um, exporte-o a partir do lock em vez de manter dois conjuntos de dependências independentes:

uv export --format requirements.txt --output-file requirements.txt

A exportação é um resultado de compatibilidade derivado. O lock continua a ser a fonte resolvida para esse fluxo de trabalho.

Uma migração que preserva o comportamento

Não comece por apagar o setup.py ou o requirements.txt. Primeiro, classifique a função de cada ficheiro existente. A figura mostra para onde vai cada parte de uma configuração antiga no pyproject.toml e que ficheiros ficam.

Ficheiros antigos mapeados para os novos destinos: metadados do setup.py, install_requires, extras_require e pontos de entrada para tabelas de project, requisitos de desenvolvimento para dependency-groups, pytest.ini para uma tabela tool, enquanto um setup.py pequeno, MANIFEST.in, .flake8 e um requirements.txt exportado do uv.lock ficam de foraFicheiros antigos mapeados para os novos destinos: metadados do setup.py, install_requires, extras_require e pontos de entrada para tabelas de project, requisitos de desenvolvimento para dependency-groups, pytest.ini para uma tabela tool, enquanto um setup.py pequeno, MANIFEST.in, .flake8 e um requirements.txt exportado do uv.lock ficam de fora

  1. Faça o inventário da lógica de build, dos metadados, dos requisitos de runtime, dos extras, dos ambientes de desenvolvimento, das definições de ferramentas, dos dados do pacote e dos pontos de entrada.
  2. Escolha um backend capaz de reproduzir a inclusão atual de ficheiros, os artefactos compilados e as instalações editáveis.
  3. Mova os metadados publicados estáticos para [project]; mantenha explícitos os campos genuinamente dinâmicos.
  4. Mova os requisitos exclusivos dos contribuidores para [dependency-groups], e não para extras públicos.
  5. Mova as definições das ferramentas apenas quando a ferramenta suportar uma semântica equivalente.
  6. Construa um sdist e um wheel, inspecione o conteúdo de ambos e instale o wheel num ambiente limpo.
  7. Execute os pontos de entrada, os testes, as verificações de importação e o caminho de deployment real.
  8. Apague a configuração antiga só depois de os artefactos e o comportamento coincidirem.

Os passos 6 e 7 ficam assim para a biblioteca do exemplo:

uv build                        # or: python -m build
unzip -l dist/*.whl             # files in the wheel
tar -tzf dist/*.tar.gz          # files in the sdist

uv venv /tmp/check
uv pip install --python /tmp/check dist/*.whl
/tmp/check/bin/weather --help

Compare as duas listas de ficheiros com o build antigo antes de apagar o que quer que seja. Um ficheiro de dados ou um módulo em falta aparece aqui, e não no checkout do repositório.

O MANIFEST.in pode continuar a ser necessário com alguns layouts do setuptools, e um pequeno setup.py pode continuar a ser válido para um comportamento de build programático.

Um fluxo de trabalho atual com o uv

O uv init cria um layout diferente para cada opção. Escolha uma destas alternativas:

# Packaged application: src/weather_app/, uv_build, and a weather-app command
uv init weather-app

# Single main.py and no build system
uv init --no-package weather-app

# Library: src layout, uv_build, and a py.typed marker
uv init --lib weather-client

O uv 0.12.0 tornou o layout empacotado o predefinido. As versões anteriores criavam por predefinição o layout com main.py.

O uv init cria os ficheiros do projeto. O uv lock cria ou atualiza uv.lock sem criar .venv. O uv sync ou o uv run atualiza o lockfile e cria e sincroniza .venv conforme necessário.

Continue com a biblioteca criada por uv init --lib weather-client. Contém uma função hello() gerada, por isso adicione um pequeno teste antes de executar o pytest:

cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
mkdir -p tests
cat > tests/test_weather_client.py <<'PY'
from weather_client import hello

def test_hello():
    assert hello() == "Hello from weather-client!"
PY
uv run --group test pytest
uv build

O teste deve indicar 1 passed. --group test seleciona as dependências de teste mesmo depois de um uv sync com as opções predefinidas as ter removido.

Inspecione os arquivos e instale o wheel num ambiente limpo antes de publicar, como indicado na secção de migração. Esta biblioteca gerada não tem um comando weather, por isso verifique antes import weather_client. Quando estiver tudo pronto, envie os ficheiros de dist/ para o PyPI:

uv publish

Fora do uv, python -m build seguido de twine upload dist/* faz o mesmo.

O uv coloca os requisitos de desenvolvimento locais em grupos de dependências normalizados. O seu backend nativo uv_build suporta apenas projetos em Python puro; as extensões compiladas precisam de outro backend da tabela acima, como o maturin ou o scikit-build-core. Consulte a documentação de configuração de projetos do uv para ver as definições atuais do backend.

Vários pacotes num só repositório

Um repositório com vários pacotes pode transformá-los num único workspace do uv. O pyproject.toml da raiz lista os membros:

[tool.uv.workspace]
members = ["packages/*"]

Os membros partilham um único uv.lock e um único .venv. uv run --package <name> executa um comando no contexto de um dos membros.

Referências