Przewodnik po pyproject.toml: pakowanie, zależności i narzędzia
Tłumaczenie automatyczne
Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.
Starsze projekty w Pythonie często rozpraszają konfigurację po setup.py, setup.cfg, plikach z wymaganiami, MANIFEST.in i osobnych plikach narzędzi deweloperskich. pyproject.toml daje tym zagadnieniom wspólne miejsce, choć nie zastępuje każdego pliku projektu i nie sprawia, że każda sekcja należy do jednego standardu.
Ten przewodnik jest dla programistów Pythona, którzy utrzymują pakiet lub aplikację. Dowiesz się, co zawiera każda z czterech głównych tabel ([build-system], [project], [dependency-groups], [tool.*]), czym różnią się od pliku blokady (lockfile) i requirements.txt, jak wybrać backend budowania oraz jak zmigrować starszy projekt i założyć nowy za pomocą uv.
Jeden plik, cztery tabele
Podstawową strukturę ustaliły trzy standardy pakowania:
- PEP 518 definiuje wymagania systemu budowania.
- PEP 621 definiuje metadane projektu.
- PEP 735 definiuje nieopublikowane grupy zależności.
Autorzy narzędzi mogą też zająć przestrzeń nazw pod [tool]. Nic nie standaryzuje jej zawartości: każde narzędzie samo definiuje swoje klucze i zachowanie.
Rysunek dzieli przykładowy plik z następnej sekcji na cztery tabele i pokazuje, który program czyta każdą z nich. Do użytkowników trafia tylko [project], przez metadane w zbudowanym wheelu.
Oto mała pakowana biblioteka:
[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"
Plik zawiera cztery listy zależności. dependencies wymienia to, czego pakiet potrzebuje w czasie działania, optional-dependencies wymienia to, co użytkownicy mogą włączyć opcjonalnie, [dependency-groups] wymienia to, co współtwórcy instalują lokalnie, a [build-system].requires wymienia to, czego backend potrzebuje do zbudowania pakietu.
[build-system]: jak źródła stają się dystrybucją
Frontend budowania, taki jak python -m build, pip lub uv, wywołuje backend budowania. Backend decyduje, jak drzewo źródeł staje się sdistem lub wheelem i które pliki trafiają do tych artefaktów. W przykładzie build-backend wskazuje hatchling, a requires wymienia to, co frontend instaluje, aby go uruchomić.
sdist to archiwum źródeł, które instalator buduje przed instalacją; wheel zawiera pliki gotowe do zainstalowania.
Budowanie przebiega w tej kolejności. Frontend czyta [build-system], instaluje requires w tymczasowym środowisku budowania i uruchamia backend. Backend czyta [project] i zapisuje sdist oraz wheel.
requires zawiera zależności potrzebne do uruchomienia backendu w jego izolowanym środowisku budowania. To nie jest lista zależności uruchomieniowych Twojego pakietu.
Zadeklaruj system budowania, gdy projekt tworzy dystrybucję albo jego własny kod ma być zainstalowany jako pakiet. Projekt niebędący pakietem też może używać pyproject.toml; PEP 735 dopuszcza nawet plik zawierający wyłącznie grupy zależności. Bez systemu budowania uv nie instaluje projektu we własnym środowisku: importowanie własnego kodu działa tylko z katalogu głównego repozytorium, a uv nie tworzy poleceń z [project.scripts]. Ustaw tool.uv.package = true albo dodaj system budowania, gdy potrzebujesz jednego z tych dwóch (pakowanie projektu w uv).
Wybór backendu
Wybierz backend według tego, co ma zrobić budowanie:
| Backend | Użyj, gdy |
|---|---|
uv_build | Projekt jest w czystym Pythonie, zarządzany przez uv i nie potrzebuje wtyczek budowania. Zapisuje go uv init. |
hatchling | Projekt jest w czystym Pythonie i potrzebuje wtyczek budowania albo wersji z gita (hatch-vcs). |
flit-core | Projekt to najprostszy pakiet w czystym Pythonie i nie potrzebuje wtyczek budowania. |
setuptools | Istniejący setup.py, rozszerzenia w C lub własny krok budowania muszą nadal działać. |
poetry-core | Zespół zarządza projektem za pomocą Poetry. |
maturin | Rozszerzenie jest napisane w Rust. |
scikit-build-core | Rozszerzenie jest w C lub C++ i budowane przez CMake. |
meson-python | Projekt buduje się za pomocą Meson, jak NumPy i SciPy. |
Rysunek ustawia backendy od buildów, które tylko kopiują pliki Pythona, do buildów kompilujących kod. Dla wersji wyznaczanej z tagów Git użyj backendu, który to obsługuje, na przykład hatchling z hatch-vcs albo setuptools z setuptools-scm.
Skopiuj aktualnie zalecaną tabelę backendu z jego dokumentacji. Nie dodawaj wheel do wymagań budowania z przyzwyczajenia; backend sam deklaruje, czego potrzebuje.
[project]: metadane, które otrzymują odbiorcy
Tabela [project] opisuje dystrybucję: jej nazwę, wersję, zgodność z wersjami Pythona, zależności uruchomieniowe, punkty wejścia i pozostałe metadane indeksu.
Specyfikatory zależności, takie jak httpx>=0.27 w przykładzie, to ograniczenia dla resolvera, a nie zrzut jednego środowiska. Stają się częścią metadanych wheela i sdista, więc instalatory po stronie odbiorcy mogą połączyć je z wymaganiami innych pakietów.
Extras to publiczny interfejs instalacji
[project.optional-dependencies] definiuje extras, które mogą zażądać odbiorcy. Dzięki extra cli z przykładu odbiorca może zainstalować weather-client[cli]. Ponieważ nazwy extras i ich wymagania są publikowane, traktuj je jak możliwości produktu i tak je nazywaj, na przykład cli albo postgres. Nie używaj extra o nazwie dev tylko po to, by zebrać w nim wszystkie narzędzia współtwórców.
Punkty wejścia łączą zainstalowane polecenia z Pythonem
Tabela [project.scripts] w przykładzie ma jeden wpis: weather = "weather_client.cli:main". Po zainstalowaniu dystrybucji środowisko udostępnia polecenie weather, które importuje weather_client.cli i wywołuje main. Polecenie weather jest instalowane nawet bez extra cli, więc cli.py musi działać bez rich albo poinformować użytkownika, że ma zainstalować weather-client[cli].
Testuj polecenie ze zbudowanego wheela, nie tylko z katalogu głównego repozytorium; wheel jest tym, co dostają użytkownicy. Sekcja o migracji pokazuje odpowiednie polecenia.
[dependency-groups]: lokalne, nieopublikowane środowiska
Grupy zależności z PEP 735 opisują środowiska deweloperskie lub niepakietowe bez publikowania ich jako metadanych pakietu.
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
]
To właściwe miejsce na testy, lintery, generatory dokumentacji i podobne narzędzia współtwórców. Przydaje się też w aplikacjach i notebookach, które nie budują dystrybucji. include-group pozwala jednej grupie użyć innej; extras nie mają odpowiednika.
PEP 735 nie definiuje interfejsu wiersza poleceń, więc każdy instalator ma własne flagi. Grupy i extras instaluje się tak:
# 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
pyproject.toml deklaruje zależności; nie tworzy środowiska. Polecenia, które tworzą środowisko i instalują projekt wraz z grupą, różnią się zależnie od narzędzia:
| Przepływ pracy | Utwórz środowisko | Zainstaluj projekt i grupę test |
|---|---|---|
| pip | python -m venv .venv, a potem je aktywuj | pip install -e . --group test |
| uv | uv sync tworzy .venv, gdy go brakuje | uv sync --group test (instaluje też projekt i grupę dev) |
[tool.*]: konfiguracja należąca do jednego narzędzia
Tabele narzędzi nie mają wspólnego schematu. W przykładzie [tool.pytest.ini_options] i [tool.ruff] używają kluczy, które czyta tylko ich własne narzędzie. Używaj tabeli [tool.*] tylko wtedy, gdy narzędzie ją dokumentuje. Te popularne narzędzia czytają ustawienia z pyproject.toml:
| Zadanie | Narzędzie | Tabela |
|---|---|---|
| Formatowanie i linting | Ruff | [tool.ruff] |
| Testy | pytest | [tool.pytest.ini_options] lub [tool.pytest] |
| Sprawdzanie typów | mypy | [tool.mypy] |
| Pokrycie kodu | coverage.py | [tool.coverage.run], [tool.coverage.report] |
pytest 9.0 dodał [tool.pytest], który używa natywnych typów TOML, takich jak lista dla addopts; starsze wersje czytają tylko [tool.pytest.ini_options].
Niektóre ustawienia nadal należą do własnych plików, ponieważ czyta je inny program albo plik wymaga innego formatu. flake8 nie czyta pyproject.toml; czyta .flake8, setup.cfg lub tox.ini. pre-commit czyta .pre-commit-config.yaml.
Deklaracje, pliki blokady i pliki requirements rozwiązują różne problemy
Rysunek śledzi pakiety z przykładowego pliku. Każda lista trafia do innego środowiska, a użytkownicy dostają tylko dwie listy z [project].
Informacje o zależnościach mieszczą się w pięciu miejscach. Każde ma jedno zadanie:
| Artefakt | Główny cel | Typowa zawartość |
|---|---|---|
[project.dependencies] | Publikowany kontrakt uruchomieniowy | Bezpośrednie wymagania i zgodne zakresy |
[project.optional-dependencies] | Publikowane opcjonalne możliwości | Extras przeznaczone dla odbiorców |
[dependency-groups] | Nieopublikowane środowiska lokalne | Grupy testowe, lintingu, dokumentacji lub aplikacji |
| Plik blokady | Odtworzenie rozwiązanego środowiska | Dokładne wersje, źródła i metadane rozwiązywania |
requirements.txt | Dane wejściowe instalacji zgodne z pip | Wymagania oraz opcje pip, ograniczenia, adresy URL lub hashe |
Biblioteka zwykle publikuje zgodne ograniczenia i jest testowana na zakresie wersji. Aplikacja zwykle commituje plik blokady menedżera środowiska. Dokładne przypięcia wersji należą do rozwiązanego artefaktu wdrożeniowego, a nie bezmyślnie do publicznych metadanych biblioteki. Na przykład biblioteka deklaruje httpx>=0.27, a wersję wybiera resolver użytkownika. Aplikacja commituje uv.lock i instaluje go przez uv sync --locked.
Zachowaj plik requirements, gdy integracja wymaga formatu lub funkcji pip. Jeśli projekt zarządzany przez uv go potrzebuje, wyeksportuj go z pliku blokady zamiast utrzymywać dwa niezależne zestawy zależności:
uv export --format requirements.txt --output-file requirements.txt
Eksport to wygenerowane dane zgodności. Plik blokady pozostaje źródłem rozwiązanych wersji dla tego przepływu pracy.
Migracja, która zachowuje działanie
Nie zaczynaj od usuwania setup.py ani requirements.txt. Najpierw ustal, co robi każdy istniejący plik. Rysunek pokazuje, dokąd w pyproject.toml trafia każda część starej konfiguracji i które pliki zostają.
- Zinwentaryzuj logikę budowania, metadane, wymagania uruchomieniowe, extras, środowiska deweloperskie, ustawienia narzędzi, dane pakietu i punkty wejścia.
- Wybierz backend, który odtworzy obecne dołączanie plików, skompilowane artefakty i instalacje edytowalne.
- Przenieś statyczne publikowane metadane do
[project]; pola naprawdę dynamiczne zostaw jawnie zadeklarowane. - Przenieś wymagania wyłącznie dla współtwórców do
[dependency-groups], a nie do publicznych extras. - Przenieś ustawienia narzędzi tylko tam, gdzie narzędzie obsługuje równoważną semantykę.
- Zbuduj zarówno sdist, jak i wheel, sprawdź ich zawartość i zainstaluj wheel w czystym środowisku.
- Uruchom punkty wejścia, testy, sprawdzenia importów i rzeczywistą ścieżkę wdrożenia.
- Usuń starą konfigurację dopiero wtedy, gdy artefakty i zachowanie się zgadzają.
Kroki 6 i 7 wyglądają tak dla przykładowej biblioteki:
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
Porównaj obie listy plików ze starym buildem, zanim cokolwiek usuniesz. Brakujący plik danych lub moduł wychodzi na jaw tutaj, a nie w kopii repozytorium.
MANIFEST.in może nadal być potrzebny przy niektórych układach setuptools, a mały setup.py może pozostać poprawny dla programowego zachowania budowania.
Aktualny przepływ pracy z uv
uv init tworzy inny układ dla każdej flagi. Wybierz jeden z tych wariantów:
# 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
uv 0.12.0 uczynił układ pakietowy domyślnym. Wcześniejsze wersje domyślnie tworzyły układ z main.py.
uv init tworzy pliki projektu. uv lock tworzy lub aktualizuje uv.lock bez tworzenia .venv. uv sync lub uv run aktualizuje plik blokady oraz w razie potrzeby tworzy i synchronizuje .venv.
Kontynuuj z biblioteką utworzoną przez uv init --lib weather-client. Zawiera wygenerowaną funkcję hello(), więc dodaj mały test przed uruchomieniem 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
Test powinien zgłosić 1 passed. --group test wybiera zależności testowe nawet po tym, jak domyślne uv sync je usunie.
Przed publikacją sprawdź archiwa i zainstaluj wheel w czystym środowisku, jak pokazano w sekcji o migracji. Ta wygenerowana biblioteka nie ma polecenia weather, więc sprawdź zamiast niego import weather_client. Gdy wszystko będzie gotowe, prześlij pliki z dist/ do PyPI:
uv publish
Poza uv to samo robi python -m build, a potem twine upload dist/*.
uv umieszcza lokalne wymagania deweloperskie w standardowych grupach zależności. Jego natywny backend uv_build obsługuje tylko projekty w czystym Pythonie; rozszerzenia kompilowane wymagają innego backendu z tabeli powyżej, na przykład maturin lub scikit-build-core. Aktualne ustawienia backendu opisuje dokumentacja konfiguracji projektu uv.
Kilka pakietów w jednym repozytorium
Repozytorium z kilkoma pakietami może uczynić je jednym workspace uv. Główny pyproject.toml wymienia członków:
[tool.uv.workspace]
members = ["packages/*"]
Członkowie współdzielą jeden uv.lock i jedno .venv. uv run --package <name> uruchamia polecenie w kontekście jednego członka.