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.
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:
| Backend | Use-o quando |
|---|---|
uv_build | O projeto é Python puro, gerido com o uv, e não precisa de plugins de build. O uv init escreve-o. |
hatchling | O projeto é Python puro e precisa de plugins de build ou de uma versão obtida do git (hatch-vcs). |
flit-core | O projeto é o pacote Python puro mais simples e não precisa de plugins de build. |
setuptools | Um setup.py existente, extensões em C ou um passo de build personalizado têm de continuar a funcionar. |
poetry-core | A equipa gere o projeto com o Poetry. |
maturin | A extensão está escrita em Rust. |
scikit-build-core | A extensão é em C ou C++ e é construída com o CMake. |
meson-python | O 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.
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 trabalho | Criar o ambiente | Instalar o projeto e o grupo test |
|---|---|---|
| pip | python -m venv .venv, e depois ativá-lo | pip install -e . --group test |
| uv | uv sync cria .venv quando este não existe | uv 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:
| Tarefa | Ferramenta | Tabela |
|---|---|---|
| Formatação e linting | Ruff | [tool.ruff] |
| Testes | pytest | [tool.pytest.ini_options] ou [tool.pytest] |
| Verificação de tipos | mypy | [tool.mypy] |
| Cobertura | coverage.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].
Cinco locais guardam informação sobre dependências. Cada um tem uma só função:
| Artefacto | Finalidade principal | Conteúdo típico |
|---|---|---|
[project.dependencies] | Contrato de runtime publicado | Requisitos diretos e intervalos compatíveis |
[project.optional-dependencies] | Capacidades opcionais publicadas | Extras destinados aos consumidores |
[dependency-groups] | Ambientes locais não publicados | Grupos de testes, lint, documentação ou aplicação |
| Lockfile | Reproduzir um ambiente resolvido | Versões exatas, origens e metadados da resolução |
requirements.txt | Entrada de instalação compatível com o pip | Requisitos 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.
- 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.
- Escolha um backend capaz de reproduzir a inclusão atual de ficheiros, os artefactos compilados e as instalações editáveis.
- Mova os metadados publicados estáticos para
[project]; mantenha explícitos os campos genuinamente dinâmicos. - Mova os requisitos exclusivos dos contribuidores para
[dependency-groups], e não para extras públicos. - Mova as definições das ferramentas apenas quando a ferramenta suportar uma semântica equivalente.
- Construa um sdist e um wheel, inspecione o conteúdo de ambos e instale o wheel num ambiente limpo.
- Execute os pontos de entrada, os testes, as verificações de importação e o caminho de deployment real.
- 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.