Budowanie paczek Pythonowych

Do konfiguracji projektu i jego budowania używamy pliku pyproject.toml, ponieważ staje się on de facto standardem dla projektów Pythonowych. Jest o tym trochę w PEP-518, PEP-621, oraz PEP-660. Sporo narzędzi preferuje konfigurację w pyproject.toml (black, isort, ruff, pytest…).

Podstawowy plik pyproject.toml

Konfiguracje w tym formacie zawierają kilka istotnych sekcji:

  • Sekcja [build-system] określa jaki backend zostanie użyty do budowania paczki jak setuptools czy hatch.
  • Sekcja [project] zawiera podstawowe metadane o projekcie, nazwę, wersję, linki, zależności, autorów i opis.
  • Sekcja [tool] często występuje wielokrotnie z dodaną nazwą narzędzia, którego dotyczy. Jest to coś w rodzaju konfiguracji per biblioteka. Przykładem jest chociażby [tool.hatch], [tool.black] czy [tool.mypy]. Co w tych sekcjach umieszczać zależy od konkretnych narzędzi i powinno być opisane w ich dokumentacji 😉.

Poniżej przykładowa konfiguracja projektu oparta o setuptools. Uwaga, wymagana wersja to 61.0 ze względu na wprowadzone wsparcie plików .toml -> notka z setuptools.

Przykładowy plik pyproject.toml dla fikcyjnego projektu snakes.

[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "snakes"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "requests",
    "Django>=4.2",
    "pydantic>=2.5",
]
dynamic = ["version"]

[tool.setuptools.dynamic]
version = {attr = "snakes.VERSION"}

W wyżej wylistowanym pliku widzimy kilka podstawowych sekcji, jak nazwa, wymagana wersja interpretera oraz zależności. Więcej informacji o dostepnych sekcjach na https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#setuptools-specific-configuration.

Wpis dynamic pozwala nam na pobieranie wersji projektu dynamicznie, to jest w czasie budowania paczki. Wymaga to podania w sekcji [tool.setuptools.dynamic] skąd pobrac informację. W przykładowym wpisie "my_package.VERSION" będzie oznaczało odczyt zmiennej w kontekście pakietu snakes. Czyli jeśli do snakes/__init__.py wrzucimy VERSION = "0.1.0", to setuptoolsy zaciągną tę zmienną i ustawią wersję naszej budowanej paczki na 0.1.0. Jednocześnie w innych miejscach projektu możemy VERSION zaimportować, żeby zachować spójność, np gdy wyświetlamy wersję w stopce strony czy panelu admina.

Zależności opcjonalne

Na podstawie dokumentacji zależności opcjonalnych można dopisać do naszego pliku następującą sekcję:

...

[project.optional-dependencies]
cli = [
  "rich",
  "click",
]

Dla takiej konfiguracji, gdy wykonamy pip install snakes[cli] podane paczki rich oraz click zostaną doinstalowane jako zależności. Jest to przydatne gdy nie chcemy zawsze instalować pewnych elementów, jak na przykład biblioteki obsługi różnych baz danych lub jak elementy GUI/CLI.

Niestandardowa struktura projektu

Domyślnie setuptools wykonują “automatyczne skanowanie” projektu. W praktyce wygląda to jednak tak, że o ile nie mamy jednego katalogu jak src, to automat może nie zadziałać. W innym przypadku mamy katalogi ze skryptami, których nie chcemy “pakować” do paczki (w rozumieniu dystrybucji z pakietem np na PyPi).

Ze względu na powyższe przydatne może być podanie dokładnie co pakować, a czego nie. W niektórych przypadkach polecenie budowanie zakończy się błędem bez takiej konfiguracji.

...
[tool.setuptools.packages.find]
where = ["snakes"]  # root katalog projektu, w którym są wszystkie exportowalne elementy
include = ["pkg*"]  # lub: `exclude = ["additional*"]`

taki plik załączy snakes/pkg* do dystrybuowanej paczki, czyli w poniższym przypadki snakes/pkg1 oraz snakes/pkg2.

include oraz exclude używają stringów w formacie glob pattern. Oznacza to, że przykładowy wpis util zaimportuje paczkę o tej nazwie, ale już nie moduły w niej umieszczone. Jeśli chcemy wszystko z paczki o jakiejś nazwie, trzeba podać pełne ścieżki, bądź wildcard paczka*.

├── pyproject.toml  # AND/OR setup.cfg, setup.py
└── snakes
    ├── pkg1
    │   └── __init__.py
    ├── pkg2
    │   └── __init__.py
    ├── additional
    │   └── __init__.py
    └── tests
        └── __init__.py

Budujemy 🔨

Zalecanym przez dokumentację setuptools narzędziem jest build, który pod spodem wykorzystuje setuptools. https://setuptools.pypa.io/en/latest/userguide/quickstart.html

pip install --upgrade build

Jeśli nasza konfiguracja jest poprawna, to polecenie

python -m build

powinno utworzyć paczki .tar.gz oraz .wheel w katalogu dist. Te paczki można wgrać czy to na PyPi, cyz do naszego lokalnego repozytorium paczek Pythonowych 😄.

Źródła

🐍