PDM: moderní správce balíčků a virtuálních prostředí Pythonu

19. 10. 2023
Doba čtení: 28 minut

Sdílet

 Autor: Depositphotos
Ukážeme si správce balíčků a virtuálních prostředí Pythonu jménem PDM. Umožňuje práci s virtuálním prostředím i s lokální instalací balíčků podle PEP-582. Pracuje také s projektovými soubory pyproject.toml definovanými v PEP-621.

Obsah

1. PDM: moderní správce balíčků a virtuálních prostředí Pythonu

2. Instalace nástroje PDM

3. První spuštění nástroje PDM

4. Vytvoření kostry projektu bez dalších závislostí

5. Struktura nově vytvořeného projektu, soubor pyproject.toml

6. Přidání dalšího balíčku do projektu, zobrazení grafu závislostí

7. Soubor pdm.lock

8. Malá odbočka: vliv zvolené minimální verze Pythonu

9. Projekt s více závislostmi

10. Nová podoba souboru pdm.lock

11. Explicitní specifikace verze balíčku

12. Strategie výběru verzí balíčku

13. Instalace balíčků používaných při vývoji, testování a ladění

14. Správa virtuálních prostředí z PDM

15. Využití adresářové struktury dle PEP-582 namísto virtuálního prostředí

16. Nová instalace všech balíčků (PEP-582)

17. Specifikace nových příkazů spouštěných přes pdm run

18. Spouštění jednotkových testů přes pdm run

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. PDM: moderní správce balíčků a virtuálních prostředí Pythonu

V současnosti pro ekosystém Pythonu existuje a je používáno hned několik správců balíčků a virtuálních prostředí. Pravděpodobně nejznámější je projekt Pipenv, který kombinuje možnosti pip a venv. Ovšem existují i další nástroje, například Poetry (ten používá pyproject.toml, ovšem není zcela kompatibilní s PEP-621) nebo Hatch (ten už je s PEP-621 kompatibilní). Dnes se ovšem seznámíme s jiným správcem, který se jmenuje PDM. Zajímavé je, že PDM umožňuje jak práci s virtuálním prostředím, tak i s lokální instalací balíčku podle PEP-582 (což je dnes oficiálně „zavržená“, ovšem stále užitečná vlastnost). Repositář projektu PDM naleznete na adrese https://github.com/pdm-project/pdm.

2. Instalace nástroje PDM

Existuje hned několik variant instalace nástroje PDM. Jedna z nabízených možností spočívá ve stažení instalačního skriptu (naprogramovaného v Pythonu) z webové stránky projektu PDM a v jeho následném spuštění. Tuto operaci lze provést jediným příkazem:

$ curl -sSL https://pdm.fming.dev/install-pdm.py | python3 -

Lepší je však si nejprve ověřit kontrolní součet stahovaného instalačního skriptu. Postup je snadný a nalezneme ho na stránkách samotného projektu PDM:

$ curl -sSLO https://pdm.fming.dev/install-pdm.py
$ curl -sSL https://pdm.fming.dev/install-pdm.py.sha256 | shasum -a 256 -c -
$ python3 install-pdm.py [options]

Jelikož je balíček PDM dostupný i na PyPi, je jeho instalace možná i přes nástroj pip, resp. pip3. Podívejme se, jak může tato forma instalace vypadat v praxi. Instalaci můžeme provést buď pro celý systém (PDM bude dostupný pro všechny uživatele) nebo pouze pro aktuálně přihlášeného uživatele:

$ pip3 install --user pdm

Jak je z následujícího výpisu prováděných operací patrné, závisí PDM na poměrně velkém množství dalších balíčků. Ovšem celková velikost nainstalovaných souborů nepřesáhne několik megabajtů:

Collecting pdm
  Downloading pdm-2.9.3-py3-none-any.whl (238 kB)
     |████████████████████████████████| 238 kB 2.1 MB/s
Collecting resolvelib>=1.0.1
  Downloading resolvelib-1.0.1-py2.py3-none-any.whl (17 kB)
Collecting pyproject-hooks
  Downloading pyproject_hooks-1.0.0-py3-none-any.whl (9.3 kB)
Collecting unearth>=0.10.0
  Downloading unearth-0.11.1-py3-none-any.whl (41 kB)
     |████████████████████████████████| 41 kB 585 kB/s
Collecting importlib-metadata>=3.6; python_version < "3.10"
  Downloading importlib_metadata-6.8.0-py3-none-any.whl (22 kB)
Collecting findpython<1.0.0a0,>=0.4.0
  Downloading findpython-0.4.0-py3-none-any.whl (20 kB)
Collecting rich>=12.3.0
  Downloading rich-13.6.0-py3-none-any.whl (239 kB)
     |████████████████████████████████| 239 kB 11.2 MB/s
Collecting requests-toolbelt
  Downloading requests_toolbelt-1.0.0-py2.py3-none-any.whl (54 kB)
     |████████████████████████████████| 54 kB 1.5 MB/s
Collecting cachecontrol[filecache]>=0.13.0
  Downloading cachecontrol-0.13.1-py3-none-any.whl (22 kB)
Collecting installer<0.8,>=0.7
  Downloading installer-0.7.0-py3-none-any.whl (453 kB)
     |████████████████████████████████| 453 kB 2.4 MB/s
Collecting python-dotenv>=0.15
  Downloading python_dotenv-1.0.0-py3-none-any.whl (19 kB)
Requirement already satisfied: blinker in /usr/lib/python3/dist-packages (from pdm) (1.4)
Requirement already satisfied: importlib-resources>=5; python_version < "3.9" in ./.local/lib/python3.8/site-packages (from pdm) (6.1.0)
Requirement already satisfied: tomli>=1.1.0; python_version < "3.11" in ./.local/lib/python3.8/site-packages (from pdm) (2.0.1)
Requirement already satisfied: certifi in /usr/lib/python3/dist-packages (from pdm) (2019.11.28)
Collecting shellingham>=1.3.2
  Downloading shellingham-1.5.3-py2.py3-none-any.whl (9.7 kB)
Collecting tomlkit<1,>=0.11.1
  Downloading tomlkit-0.12.1-py3-none-any.whl (37 kB)
Collecting platformdirs
  Downloading platformdirs-3.11.0-py3-none-any.whl (17 kB)
Collecting virtualenv>=20
  Downloading virtualenv-20.24.5-py3-none-any.whl (3.7 MB)
     |████████████████████████████████| 3.7 MB 6.6 MB/s
Collecting packaging!=22.0,>=20.9
  Downloading packaging-23.2-py3-none-any.whl (53 kB)
     |████████████████████████████████| 53 kB 1.9 MB/s
Collecting requests>=2.25
  Downloading requests-2.31.0-py3-none-any.whl (62 kB)
     |████████████████████████████████| 62 kB 1.2 MB/s
Requirement already satisfied: zipp>=0.5 in ./.local/lib/python3.8/site-packages (from importlib-metadata>=3.6; python_version < "3.10"->pdm) (3.17.0)
Collecting markdown-it-py>=2.2.0
  Downloading markdown_it_py-3.0.0-py3-none-any.whl (87 kB)
     |████████████████████████████████| 87 kB 2.6 MB/s
Requirement already satisfied: typing-extensions<5.0,>=4.0.0; python_version < "3.9" in ./.local/lib/python3.8/site-packages (from rich>=12.3.0->pdm) (4.7.1)
Collecting pygments<3.0.0,>=2.13.0
  Downloading Pygments-2.16.1-py3-none-any.whl (1.2 MB)
     |████████████████████████████████| 1.2 MB 8.9 MB/s
Collecting msgpack>=0.5.2
  Downloading msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (534 kB)
     |████████████████████████████████| 534 kB 6.7 MB/s
Collecting filelock>=3.8.0; extra == "filecache"
  Downloading filelock-3.12.4-py3-none-any.whl (11 kB)
Collecting distlib<1,>=0.3.7
  Downloading distlib-0.3.7-py2.py3-none-any.whl (468 kB)
     |████████████████████████████████| 468 kB 5.7 MB/s
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/lib/python3/dist-packages (from requests>=2.25->unearth>=0.10.0->pdm) (1.25.8)
Requirement already satisfied: idna<4,>=2.5 in /usr/lib/python3/dist-packages (from requests>=2.25->unearth>=0.10.0->pdm) (2.8)
Collecting charset-normalizer<4,>=2
  Downloading charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (137 kB)
     |████████████████████████████████| 137 kB 14.2 MB/s
Collecting mdurl~=0.1
  Downloading mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Installing collected packages: resolvelib, pyproject-hooks, packaging, charset-normalizer, requests, unearth, importlib-metadata, findpython, mdurl, markdown-it-py, pygments, rich, requests-toolbelt, msgpack, filelock, cachecontrol, installer, python-dotenv, shellingham, tomlkit, platformdirs, distlib, virtualenv, pdm
Successfully installed cachecontrol-0.13.1 charset-normalizer-3.3.0 distlib-0.3.7 filelock-3.12.4 findpython-0.4.0 importlib-metadata-6.8.0 installer-0.7.0 markdown-it-py-3.0.0 mdurl-0.1.2 msgpack-1.0.7 packaging-23.2 pdm-2.9.3 platformdirs-3.11.0 pygments-2.16.1 pyproject-hooks-1.0.0 python-dotenv-1.0.0 requests-2.31.0 requests-toolbelt-1.0.0 resolvelib-1.0.1 rich-13.6.0 shellingham-1.5.3 tomlkit-0.12.1 unearth-0.11.1 virtualenv-20.24.5

3. První spuštění nástroje PDM

Po doufejme že úspěšné instalaci by mělo být možné spustit příkaz pdm. Nejnovější a v době vydání článku aktuální verze PDM je 2.9.3, což si můžeme velmi snadno ověřit (ostatně tato verze se vypisovala již při instalaci balíčků provedené v rámci předchozí kapitoly):

$ pdm --version
 
PDM, version 2.9.3
 
Usage: pdm [-h] [-V] [-c CONFIG] [-v] [-I] [--pep582 [SHELL]] ...
 
    ____  ____  __  ___
   / __ \/ __ \/  |/  /
  / /_/ / / / / /|_/ /
 / ____/ /_/ / /  / /
/_/   /_____/_/  /_/
 
Options:
  -h, --help            Show this help message and exit.
  -V, --version         Show the version and exit
  -c CONFIG, --config CONFIG
                        Specify another config file path [env var:
                        PDM_CONFIG_FILE]
  -v, --verbose         Use `-v` for detailed output and `-vv` for more
                        detailed
  -I, --ignore-python   Ignore the Python path saved in .pdm-python. [env var:
                        PDM_IGNORE_SAVED_PYTHON]
  --pep582 [SHELL]      Print the command line to be eval'd by the shell
 
Commands:
  add                   Add package(s) to pyproject.toml and install them
  build                 Build artifacts for distribution
  cache                 Control the caches of PDM
  completion            Generate completion scripts for the given shell
  config                Display the current configuration
  export                Export the locked packages set to other formats
  fix                   Fix the project problems according to the latest
                        version of PDM
  import                Import project metadata from other formats
  info                  Show the project information
  init                  Initialize a pyproject.toml for PDM
  install               Install dependencies from lock file
  list                  List packages installed in the current working set
  lock                  Resolve and lock dependencies
  publish               Build and publish the project to PyPI
  remove                Remove packages from pyproject.toml
  run                   Run commands or scripts with local packages loaded
  search                Search for PyPI packages
  self (plugin)         Manage the PDM program itself (previously known as
                        plugin)
  show                  Show the package information
  sync                  Synchronize the current working set with lock file
  update                Update package(s) in pyproject.toml
  use                   Use the given python version or path as base
                        interpreter
  venv                  Virtualenv management

4. Vytvoření kostry projektu bez dalších závislostí

Podívejme se nyní, jakým způsobem můžeme s využitím nástroje PDM vytvořit kostru jednoduchého Pythonovského projektu. První projekt nebude mít žádné závislosti, ovšem jeho ostatní nastavení a metadata budou odpovídat skutečnému projektu.

Kostru projektu lze vytvořit interaktivně, tj. odpovědmi na několik otázek, které PDM položí. Pro tento účel v novém (prázdném) adresáři zavoláme následující příkaz:

$ pdm init

Nástroj PDM se v prvním kroku pokusí nalézt všechny nainstalované verze Pythonu a následně se zeptá, na které verzi bude projekt postaven (resp. na které bude závislý). Na mém nově nainstalovaném systému je nalezen pouze Python 3.11 (jediná verze), kterou ovšem PDM kvůli použití symlinků nabídne jako tři nezávislé verze:

Creating a pyproject.toml for PDM...
Please enter the Python interpreter to use
0. /usr/bin/python (3.11)
1. /usr/bin/python3.11 (3.11)
2. /usr/bin/python3 (3.11)
Please select (0): 2
Poznámka: skutečně se jedná o jedinou instalaci Pythonu, o čemž se ostatně můžeme snadno přesvědčit pohledem na všechny tři soubory zmíněné v prvním dotazu (jedná se o dvojici symlinků na stejný skript):
$ ls -l /usr/bin/python*
 
lrwxrwxrwx. 1 root root     9 Aug 28 02:00 /usr/bin/python -> ./python3
lrwxrwxrwx. 1 root root    10 Aug 28 02:00 /usr/bin/python3 -> python3.11
-rwxr-xr-x. 1 root root 16040 Aug 28 02:00 /usr/bin/python3.11

Mimochodem: na systému s Pythonem 2.x i Pythonem 3.x může výběr vypadat například následovně:

0. /usr/bin/python (3.8)
1. /usr/bin/python3.8 (3.8)
2. /usr/bin/python2.7 (2.7)

Dále se PDM zeptá, zda se má vytvořit virtuální prostředí Pythonu (virtualenv). Prozatím odpovíme, že ano, ovšem později se vrátíme k alternativě:

Would you like to create a virtualenv with /usr/bin/python3? [y/n] (y): y
Virtualenv is created successfully at /home/ptisnovs/project1/.venv

Další otázka se týká struktury projektového souboru. Odpovíme, že se prozatím nebude jednat o (instalovatelnou) knihovnu:

Is the project a library that is installable?
If yes, we will need to ask a few more questions to include the project name and
build backend [y/n] (n): n

Následují otázky týkající se licence a autora aplikace či knihovny:

License(SPDX name) (MIT): MIT
Author name (): Pavel
Author email (): pavel@nowhere.com

A konečně musíme odpovědět na otázku, jaká minimální verze Pythonu bude vyžadována. Poté již bude vytvořena kostra projektu:

Python requires('*' to allow any) (>=3.11): >=3.7
Project is initialized successfully
Poznámka: v případě, že používáte nástroj pyenv pro správu verzí Pythonu, budou i tyto verze nástrojem PDM detekovány a můžete si z nich vybrat konkrétní požadovanou verzi.

Obrázek 1: Vytvoření kostry projektu.

5. Struktura nově vytvořeného projektu, soubor pyproject.toml

Adresář s kostrou projektu, který byl vytvořen po upřesňujících dotazech, o nichž jsme se zmínili ve čtvrté kapitole, by měl vypadat následovně:

.
├── __pycache__
├── pyproject.toml
├── README.md
├── src
│   └── example_package
│       ├── __init__.py
│       └── __pycache__
└── tests
    ├── __init__.py
    └── __pycache__
 
7 directories, 4 files

Projekt obsahuje především soubor pyproject.toml, v němž jsou zapsána metadata o projektu, která odpovídají specifikaci PEP 621 – Storing project metadata in pyproject.toml. Pokud jste na otázky ve čtvrté kapitole odpovídali stejně jako autor článku, měl by projektový soubor mít následující obsah:

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel", email = "pavel@nowhere.com"},
]
dependencies = []
requires-python = ">=3.7"
readme = "README.md"
license = {text = "MIT"}
Poznámka: formát TOML (neboli Tom's Obvious, Minimal Language) sice zdánlivě (alespoň na první pohled) vychází ze souborů typu INI, ovšem ve skutečnosti se jedná o odlišný, v mnoha ohledech vylepšený a především promyšlený formát, v němž byly odstraněny prakticky všechny nevýhody INI a přitom byla zachována čitelnost a snadnost úprav. I proto se za posledních několik let stal velmi populární. Ve formátu TOML jsou kromě řetězců, celých čísel a seznamů podporovány i další datové typy – pravdivostní typ, čísla s plovoucí řádovou čárkou a zejména pak, což je v praxi velmi užitečné, typ „datum+čas“ neboli časové razítko.

Dále v projektu najdeme obligátní soubor .gitignore a taktéž soubor .pdm-python s odkazem na interpret Pythonu, který se bude používat:

/home/ptisnovs/pdb/project1/.venv/bin/python
Poznámka: tento soubor obsahuje cestu platnou pouze na konkrétním počítači a pro konkrétního uživatele, a tudíž by neměl být součástí repositáře (tedy neměl by se ukládat do Gitu atd.). I proto je ostatně soubor .pdm-python zmíněn v souboru .gitignore.

Další informace o vytvořeném projektu lze získat tímto příkazem:

$ pdm info

Výsledek by mohl vypadat následovně (cesty se pochopitelně budou lišit):

PDM version:
  2.9.3
Python Interpreter:
  /home/ptisnovs/project1/.venv/bin/python (3.11)
Project Root:
  /home/ptisnovs/project1
Local Packages:
Poznámka: právě popsaný projekt naleznete v GitHub repositáři na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project1/.

6. Přidání dalšího balíčku do projektu, zobrazení grafu závislostí

Nyní se pokusme do projektu přidat nějakou závislost, tj. knihovnu, kterou budeme v projektu používat. Tato operace je snadná, protože nám postačuje použít příkaz pdm add, kterému předáme název knihovny a popř. i požadovanou verzi. Pokud verzi neuvedeme, bude použita poslední dostupná validní verze (ovšem viz další text – ještě je totiž možné specifikovat takzvanou strategii):

$ pdm add sympy
Adding packages to default dependencies: sympy
🔒 Lock successful
Changes are written to pyproject.toml.
All packages are synced to date, nothing to do.
 
🎉 All complete!

Po provedení této operace se podívejme do projektového souboru pyproject.toml. Objevila se v něm nová sekce nazvaná příznačně dependencies:

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel", email = "pavel@nowhere.com"},
]
dependencies = [
    "sympy>=1.10.1",
]
requires-python = ">=3.7"
readme = "README.md"
license = {text = "MIT"}

Seznam závislostí ovšem můžeme získat i z příkazového řádku bez nutnosti znát strukturu projektového souboru:

$ pdm list

Výsledek bude obsahovat i tranzitivní závislost – knihovnu mpmath, kterou sympy pro svoji činnost vyžaduje:

╭────────┬─────────┬──────────╮
│ name   │ version │ location │
├────────┼─────────┼──────────┤
│ mpmath │ 1.3.0   │          │
│ sympy  │ 1.10.1  │          │
╰────────┴─────────┴──────────╯

Přímé i tranzitivní závislosti si můžeme nechat zobrazit i formou stromové struktury, což je ukázáno na dalším příkladu:

$ pdm list --graph

S výsledkem:

sympy 1.10.1 [ required: >=1.10.1 ]
└── mpmath 1.3.0 [ required: >=0.19 ]
Poznámka: právě popsaný projekt naleznete v GitHub repositáři na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project2/.

Obrázek 2: Výsledek příkazu pdm list na terminálu, který dokáže zobrazovat obarvený text.

7. Soubor pdm.lock

V průběhu přidání (či aktualizace nebo odebrání) nějakého balíčku, na kterém projekt závisí, se vytvoří i soubor pojmenovaný pdm.lock. V tomto souboru jsou uloženy přesné informace o konkrétních verzích přímých i tranzitivních závislostí, včetně otisků souborů s archivy těchto balíčků. Jedná se o velmi důležité informace, které mohou pomoci k tomu, aby se na jiném počítači (třeba na stage či produkčním serveru) nainstalovaly ty samé balíčky, jako na počítači vývojáře a/nebo testera. A v neposlední řadě to vede k větší bezpečnosti při sestavování projektů (omezí se útoky typu typosquatting atd.).

Zajímavé bude zjistit, jak tento soubor vypadá:

# This file is @generated by PDM.
# It is not intended for manual editing.
 
[metadata]
groups = ["default"]
cross_platform = true
static_urls = false
lock_version = "4.3"
content_hash = "sha256:c19bb0a4a0a179b5e92d875ea3704d5564d319483f9497918302f97aa96a7d83"
 
[[package]]
name = "mpmath"
version = "1.3.0"
summary = "Python library for arbitrary-precision floating-point arithmetic"
files = [
    {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
    {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
]
 
[[package]]
name = "sympy"
version = "1.10.1"
requires_python = ">=3.7"
summary = "Computer algebra system (CAS) in Python"
dependencies = [
    "mpmath>=0.19",
]
files = [
    {file = "sympy-1.10.1-py3-none-any.whl", hash = "sha256:df75d738930f6fe9ebe7034e59d56698f29e85f443f743e51e47df0caccc2130"},
    {file = "sympy-1.10.1.tar.gz", hash = "sha256:5939eeffdf9e152172601463626c022a2c27e75cf6278de8d401d50c9d58787b"},
]

8. Malá odbočka: vliv zvolené minimální verze Pythonu

Připomeňme si, že seznam přímých i tranzitivních závislostí vypadá u našeho projektu následovně:

╭────────┬─────────┬──────────╮
│ name   │ version │ location │
├────────┼─────────┼──────────┤
│ mpmath │ 1.3.0   │          │
│ sympy  │ 1.10.1  │          │
╰────────┴─────────┴──────────╯

To je zajímavé, protože při pohledu na vydání knihovny sympy (viz https://github.com/sympy/sym­py/releases) snadno zjistíme, že poslední stabilní verze je 1.12 a nikoli 1.10.1. Pokusme se tedy o explicitní instalaci novější verze:

$ pdm add sympy==1.12

V tomto případě se instalace (resp. ugrade) nepodaří, protože novější verze knihovny Sympy vyžadují i novější verzi samotného Pythonu:

Adding packages to default dependencies: sympy==1.12
🔒 Lock failed
Unable to find a resolution because the following dependencies don't work on all Python versions in the range of the project's `requires-python`: >=3.7.
  python>=3.8 (from <Candidate sympy@1.12 from https://pypi.org/simple/sympy/>)
A possible solution is to change the value of `requires-python` in pyproject.toml to >=3.8.
See /tmp/pdm-lock-ssanlsgo.log for detailed debug log.

To znamená, že i když používáme Python 3.11, projekt je stále nastaven tak, že minimální kompatibilní verze Pythonu je 3.7:

requires-python = ">=3.7"

Změna souboru pyproject je snadná – postačuje totiž výše uvedený řádek změnit na:

requires-python = ">=3.8"

Nyní již můžeme spustit příkaz pro instalaci konkrétní verze knihovny sympy:

$ pdm add sympy==1.12

Ve skutečnosti u nového projektu můžeme použít i:

$ pdm add sympy

Instalace by měla proběhnout bez problémů, a to s verzí Sympy 1.12:

Adding packages to default dependencies: sympy
🔒 Lock successful
Changes are written to pyproject.toml.
Synchronizing working set with resolved packages: 2 to add, 0 to update, 0 to
remove
 
  ✔ Install mpmath 1.3.0 successful
  ✔ Install sympy 1.12 successful
 
🎉 All complete!

9. Projekt s více závislostmi

Samozřejmě nám nic nebrání v tom, abychom si do projektu přidali další balíček, na kterém bude projekt záviset. Zvolit můžeme například známý balíček requests. Při jeho instalaci prozatím nebudeme specifikovat verzi:

$ pdm add requests

Průběh konfigurace projektu s přidáním nového balíčku:

Adding packages to default dependencies: requests
🔒 Lock successful
Changes are written to pyproject.toml.
Synchronizing working set with resolved packages: 5 to add, 0 to update, 0 to
remove
 
  ✔ Install idna 3.4 successful
  ✔ Install requests 2.31.0 successful
  ✔ Install certifi 2023.7.22 successful
  ✔ Install urllib3 2.0.6 successful
  ✔ Install charset-normalizer 3.3.0 successful
 
🎉 All complete!

V projektovém souboru pyproject.toml se objeví pouze dvě přímé závislosti (podle očekávání):

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel", email = "pavel@nowhere.com"},
]
dependencies = [
    "sympy>=1.10.1",
    "requests>=2.31.0",
]
requires-python = ">=3.7"
readme = "README.md"
license = {text = "MIT"}

Ve skutečnosti však naroste počet tranzitivních závislostí, což si opět můžeme ověřit jejich výpisem ve formě tabulky nebo zobrazením stromové struktury:

$ pdm list
 
╭────────────────────┬───────────┬──────────╮
│ name               │ version   │ location │
├────────────────────┼───────────┼──────────┤
│ charset-normalizer │ 3.3.0     │          │
│ mpmath             │ 1.3.0     │          │
│ sympy              │ 1.10.1    │          │
│ idna               │ 3.4       │          │
│ requests           │ 2.31.0    │          │
│ certifi            │ 2023.7.22 │          │
│ urllib3            │ 2.0.6     │          │
╰────────────────────┴───────────┴──────────╯

Zobrazení přímých i tranzitivních závislostí formou stromové struktury:

$ pdm list --graph
 
requests 2.31.0 [ required: >=2.31.0 ]
├── certifi 2023.7.22 [ required: >=2017.4.17 ]
├── charset-normalizer 3.3.0 [ required: <4,>=2 ]
├── idna 3.4 [ required: <4,>=2.5 ]
└── urllib3 2.0.6 [ required: <3,>=1.21.1 ]
sympy 1.10.1 [ required: >=1.10.1 ]
└── mpmath 1.3.0 [ required: >=0.19 ]
Poznámka: právě popsaný projekt naleznete v GitHub repositáři na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project3/.

10. Nová podoba souboru pdm.lock

Soubor pdm.lock se po přidání nového balíčku kvůli tranzitivním závislostem zvětšil až na 17kB. Z tohoto důvodu si uvedeme pouze jeho začátek a konec (s několika balíčky). Struktura souboru však zůstává stále stejná:

[metadata]
groups = ["default"]
cross_platform = true
static_urls = false
lock_version = "4.3"
content_hash = "sha256:286f1c206e4a496e9dc40642617bcc789fa52bcac8f2dc52b9090d276c4162e4"
 
[[package]]
name = "certifi"
version = "2023.7.22"
requires_python = ">=3.6"
summary = "Python package for providing Mozilla's CA Bundle."
files = [
    {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
    {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
]
 
[[package]]
name = "charset-normalizer"
version = "3.3.0"
requires_python = ">=3.7.0"
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
files = [
    {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"},
    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"},
    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"},
    ...
    ...
    ...
[[package]]
name = "urllib3"
version = "2.0.6"
requires_python = ">=3.7"
summary = "HTTP library with thread-safe connection pooling, file post, and more."
files = [
    {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"},
    {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"},
]

11. Explicitní specifikace verze balíčku

Při přidávání balíčku do projektu můžeme explicitně specifikovat požadovanou verzi tohoto balíčku. Formát zápisu verze je v ekosystému Pythonu již standardizován – konkrétní verze se zapisuje za znaky rovnosti:

$ pdm add requests==2.0.0
POZOR: jedná se pouze o příklad, který v praxi raději nepoužívejte. Ve skutečnosti má balíček requests ve verzi 2.0.0 minimálně tři známé bezpečnostní chyby.

Projektový soubor pyproject.toml bude nyní vypadat následovně:

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel", email = "pavel@nowhere.com"},
]
dependencies = [
    "sympy>=1.10.1",
    "requests==2.0.0",
]
requires-python = ">=3.7"
readme = "README.md"
license = {text = "MIT"}

Vzhledem k tomu, že requests nemá žádné tranzitivní závislosti (na rozdíl od novějších verzí tohoto balíčku), bude soubor pdm.lock stále velmi jednoduchý:

# This file is @generated by PDM.
# It is not intended for manual editing.
 
[metadata]
groups = ["default"]
cross_platform = true
static_urls = false
lock_version = "4.3"
content_hash = "sha256:b5aed5d117e640360335e67f9b9a815150e717d992ecfdf1663587226da6b6b7"
 
[[package]]
name = "mpmath"
version = "1.3.0"
summary = "Python library for arbitrary-precision floating-point arithmetic"
files = [
    {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
    {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
]
 
[[package]]
name = "requests"
version = "2.0.0"
summary = "Python HTTP for Humans."
files = [
    {file = "requests-2.0.0-py2.py3-none-any.whl", hash = "sha256:2ef65639cb9600443f85451df487818c31f993ab288f313d29cc9db4f3cbe6ed"},
    {file = "requests-2.0.0.tar.gz", hash = "sha256:78536038f54cff6ade3be6863403146665b5a3923dd61108c98d8b64141f9d70"},
]
 
[[package]]
name = "sympy"
version = "1.10.1"
requires_python = ">=3.7"
summary = "Computer algebra system (CAS) in Python"
dependencies = [
    "mpmath>=0.19",
]
files = [
    {file = "sympy-1.10.1-py3-none-any.whl", hash = "sha256:df75d738930f6fe9ebe7034e59d56698f29e85f443f743e51e47df0caccc2130"},
    {file = "sympy-1.10.1.tar.gz", hash = "sha256:5939eeffdf9e152172601463626c022a2c27e75cf6278de8d401d50c9d58787b"},
]
Poznámka: právě popsaný projekt naleznete v GitHub repositáři na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project4/.

Požadovanou verzi balíčku lze ovšem specifikovat i dalšími způsoby:

minimální: >2.21.0
kompatibilní: >2.21.0,<3.0.0

12. Strategie výběru verzí balíčku

Výběr verze balíčku, který se do projektu přidává, závisí na zvolené strategii. Ta se specifikuje následujícími čtyřmi volbami (přepínači):

--save-minimum
--save-compatible
--save-exact
--save-wildcard

Strategie „minimum“ ve k tomu, že se do projektového souboru uloží informace o tom, že bude vyžadována konkrétní stabilní verze či verze vyšší:

$ pdm add --save-minimum requests

Výsledek vypadá následovně:

dependencies = [
    "requests>=2.31.0",
]

Strategie „compatible“ umožňuje specifikovat konkrétní verzi, popř. verze s ní kompatibilní:

$ pdm add --save-compatible requests

Výsledek:

dependencies = [
    "requests~=2.31",
]

Nejjednodušší je strategie „exact“, která v důsledku vede k tomu, že bude specifikována jediná konkrétní verze balíčku:

$ pdm add --save-exact requests

Výsledek:

dependencies = [
    "requests==2.31.0",
]

A konečně v poslední variantě „wildcard“ nemusí být ve výsledku verze balíčku vůbec uvedená:

$ pdm add --save-wildcard requests

Výsledek:

dependencies = [
    "requests",
]

13. Instalace balíčků používaných při vývoji, testování a ladění

Mnoho balíčků, s nimiž se v projektu nějakým způsobem pracuje, je ve skutečnosti použito „pouze“ při vývoji, testování a ladění aplikace. Takové balíčky tedy není nutné (a ani to není vhodné) instalovat při nasazování aplikace na stage či produkční prostředí. Na druhou stranu by však projekt měl obsahovat informace o těchto balíčcích. V případě, že se používá nástroj PDM, je možné „vývojářské“ balíčky nainstalovat s použitím přepínače -d, kterým se tyto balíčky odliší od produkčních balíčků nutných pro běh aplikace.

Poměrně dobrým příkladem může být balíček pytest využívaný pro spouštění a analýzu jednotkových testů. Takový balíček nainstalujeme následujícím způsobem:

$ pdm add -d pytest

Výsledkem bude, že se v projektovém souboru pyproject.toml objeví nová sekce nazvaná tool.pdm.dev-dependencies (viz zvýrazněnou část):

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel Tisnovsky", email = "tisnik@nowhere.com"},
]
dependencies = [
    "sympy>=1.12",
]
requires-python = ">=3.11"
readme = "README.md"
license = {text = "MIT"}
 
[tool.pdm.dev-dependencies]
dev = [
    "pytest>=7.4.2",
]

Naproti tomu soubor pdm.lock informaci o tomto balíčku obsahovat nebude:

# This file is @generated by PDM.
# It is not intended for manual editing.
 
[metadata]
groups = ["default"]
cross_platform = true
static_urls = false
lock_version = "4.3"
content_hash = "sha256:5cd901a11186547468a967f40df61dd8cb8cf0b5bb3499bbf66125887548c0f7"
 
[[package]]
name = "mpmath"
version = "1.3.0"
summary = "Python library for arbitrary-precision floating-point arithmetic"
files = [
    {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
    {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
]
 
[[package]]
name = "sympy"
version = "1.12"
requires_python = ">=3.8"
summary = "Computer algebra system (CAS) in Python"
dependencies = [
    "mpmath>=0.19",
]
files = [
    {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"},
    {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"},
]
Poznámka: právě popsaný projekt naleznete v GitHub repositáři na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project8/.

14. Správa virtuálních prostředí z PDM

Ještě jednou se vraťme k příkazu pro zobrazení informací o projektu, zejména o jeho prostředí. Jedná se o příkaz:

$ pdm info

Výsledek by mohl vypadat následovně (cesty se pochopitelně budou lišit):

PDM version:
  2.9.3
Python Interpreter:
  /home/ptisnovs/project1/.venv/bin/python (3.11)
Project Root:
  /home/ptisnovs/project1
Local Packages:

V tuto chvíli je pro nás důležitý řádek s cestou k interpretru Pythonu. Ten nám totiž mj. říká, že při spouštění projektu (nebo jeho částí, popř. testů atd.) bude využito virtuální prostředí Pythonu. To je v našem případě umístěno v podadresáři .vent. PDM nám umožňuje nechat si vypsat virtuální prostředí, která jsou v daný okamžik dostupná, a to konkrétně příkazem:

$ pdm venv list
Virtualenvs created with this project:
 
*  in-project: /home/ptisnovs/x/project1/.venv

Virtuální prostředí je do stromové struktury s projektem zakomponováno následovně:

├── __pycache__
├── src
│   └── example_package
│       └── __pycache__
├── tests
│   └── __pycache__
└── .venv
    ├── bin
    ├── lib
    │   └── python3.11
    │       └── site-packages
    │           ├── mpmath
    │           ├── mpmath-1.3.0.dist-info
    │           ├── __pycache__
    │           ├── sympy
    │           └── sympy-1.10.1.dist-info
    ├── lib64
    │   └── python3.11
    │       └── site-packages
    └── share
        └── man
            └── man1

Možné je nechat si vytvořit i další virtuální prostředí. Pokud však již adresář .venv existuje, bude další virtuální prostředí vytvořeno v adresáři specifikovaném konfigurační volbou venv.location (tu jsme neměnili). Jméno nově vytvořeného podadresáře s virtuálním prostředím přitom bude mít tento formát: jméno_projektu-hash-verze_pythonu. Ostatně můžeme se pokusit o vytvoření ještě jednoho virtuálního prostředí pro náš projekt a pro zvolenou verzi Pythonu:

$ pdm venv create --name testing 3.11
 
Virtualenv /home/ptisnovs/.local/share/pdm/venvs/project1-MLVA-_ZK-testing is
created successfully

Struktura tohoto nového virtuální prostředí bude shodná se strukturou „lokálního“ virtuálního prostředí:

/home/ptisnovs/.local/share/pdm/venvs
└── project1-MLVA-_ZK-testing
    ├── bin
    ├── lib
    │   └── python3.11
    │       └── site-packages
    └── lib64
        └── python3.11
            └── site-packages

Virtuální prostředí je možné i smazat:

$ pdm venv remove in-project
 
Virtualenvs created with this project:
Will remove: /home/ptisnovs/x/project1/.venv, continue? [y/n] (y): y
Removed successfully!

15. Využití adresářové struktury dle PEP-582 namísto virtuálního prostředí

Správce PDM umožňuje namísto virtuálního prostředí (tedy vlastně typicky podadresáře .venv) pracovat s adresářovou strukturou definovanou v PEP-582. Toto PEP sice bylo oficiálně zamítnuto, což ale neznamená, že jím definovanou adresářovou strukturu nelze použít (ostatně PDM byl vytvořen právě proto, aby existovala implementace PEP-582).

Při vytváření nového projektu postačuje na otázku:

Would you like to create a virtualenv with /usr/bin/python? [y/n] (y): n

odpovědět záporně:

You are using the PEP 582 mode, no virtualenv is created.
For more info, please visit https://peps.python.org/pep-0582/
Poznámka: výsledkem bude prakticky stejný projekt, jako v předchozích případech, ovšem obsah souboru .pdm-python bude odlišný.

Následně do projektu přidáme nějakou závislost:

$ pdm add sympy
 
Adding packages to default dependencies: sympy
🔒 Lock successful
Changes are written to pyproject.toml.
Synchronizing working set with resolved packages: 2 to add, 0 to update, 0 to
remove
 
  ✔ Install mpmath 1.3.0 successful
  ✔ Install sympy 1.12 successful
 
🎉 All complete!

Výsledkem by měla být následující adresářová struktura, v níž se namísto .venv objevuje podadresář __pypackages__:

.
├── __pycache__
├── __pypackages__
│   └── 3.11
│       ├── bin
│       ├── include
│       ├── lib
│       │   ├── mpmath
│       │   ├── mpmath-1.3.0.dist-info
│       │   ├── sympy
│       │   └── sympy-1.12.dist-info
│       └── share
│           └── man
├── src
│   └── example_package
│       └── __pycache__
└── tests
    └── __pycache__

16. Nová instalace všech balíčků (PEP-582)

V případě potřeby, například pokud dojde k problémům s updaty balíčků (což se nastává příliš často), je možné jednoduše celý adresář __pypackages__ smazat:

$ rm -rf __pypackages__

Struktura projektu se nám zjednoduší na:

.
├── __pycache__
├── src
│   └── example_package
│       └── __pycache__
└── tests
    └── __pycache__

Nová instalace všech balíčků v požadovaných verzích se provede příkazem:

$ pdm install -v
 
STATUS: Resolving packages from lockfile...
STATUS: Fetching hashes for resolved packages...
Synchronizing working set with resolved packages: 2 to add, 0 to update, 0 to remove
 
unearth.preparer: Using cached <Link https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl (from https://pypi.org/simple/mpmath/)>
unearth.preparer: Using cached <Link https://files.pythonhosted.org/packages/d2/05/e6600db80270777c4a64238a98d442f0fd07cc8915be2a1c16da7f2b9e74/sympy-1.12-py3-none-any.whl (from https://pypi.org/simple/sympy/)>
  ✔ Install mpmath 1.3.0 successful
  ✔ Install sympy 1.12 successful

Což povede k obnovení původní struktury projektu:

.
├── __pycache__
├── __pypackages__
│   └── 3.11
│       ├── bin
│       ├── include
│       ├── lib
│       │   ├── mpmath
│       │   ├── mpmath-1.3.0.dist-info
│       │   ├── sympy
│       │   └── sympy-1.12.dist-info
│       └── share
│           └── man
├── src
│   └── example_package
│       └── __pycache__
└── tests
    └── __pycache__

17. Specifikace nových příkazů spouštěných přes pdm run

Do projektového souboru pyproject.toml můžeme přidat i specifikaci nových (prakticky libovolných) příkazů, které se spouští přes pdm run. Příkladem může být příkaz start pro spuštění aplikace (samozřejmě s nastaveným prostředím atd.):

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel", email = "pavel@nowhere.com"},
]
dependencies = []
requires-python = ">=3.7"
readme = "README.md"
license = {text = "MIT"}
 
[tool.pdm.scripts]
start = "src/example_package/main.py"

Nový příkaz start nyní můžeme zavolat takto:

$ pdm run start

18. Spouštění jednotkových testů přes pdm run

Do projektového souboru pyproject.toml samozřejmě můžeme přidat i další vlastní příkazy, například příkaz test určený pro spuštění jednotkových testů. Úprava bude triviální:

bitcoin školení listopad 24

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "Pavel", email = "pavel@nowhere.com"},
]
[tool.pdm.dev-dependencies]
dev = [
    "pytest>=7.4.2",
]
requires-python = ">=3.7"
readme = "README.md"
license = {text = "MIT"}
 
[tool.pdm.scripts]
start = "src/example_package/main.py"
test = "pytest"

A takto bude vypadat spuštění testů:

============================= test session starts ==============================
platform linux -- Python 3.11.5, pytest-7.4.2, pluggy-1.2.0
rootdir: /home/ptisnovs/x/project7
collected 0 items
 
============================ no tests ran in 0.00s =============================
Poznámka: žádné testy sice nemáme v projektu definovány, ovšem už jen fakt, že se pytest spustil na počítači, na němž není tento nástroj nainstalován (existuje jen ve virtuálním prostředí projektu) ukazuje, že vše funguje.

19. Repositář s demonstračními příklady

Všechny Pythonovské projekty, které jsme si vytvořili, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít nainstalovánu některou z podporovaných verzí Pythonu 3 a pochopitelně taktéž PDM:

# Projekt Stručný popis Adresa
1 project1/ nově vytvořený projekt bez závislostí https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project1/
2 project2/ projekt s jednou explicitně zapsanou závislostí https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project2/
3 project3/ projekt s dvojicí explicitně zapsaných závislostí https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project3/
4 project4/ projekt se závislostí na knihovně requests ve verzi 2.0.0 https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project4/
5 project5/ projekt se závislostí na knihovně requests v nejnovější dostupné verzi https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project5/
6 project6/ kostra Pythonovské knihovny (balíčku) https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project6/
7 project7/ specifikace příkazu spuštěného po zadání pdm start https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project7/
8 project8/ projekt se závislostmi pro vývojáře (nikoli pro výslednou aplikaci či knihovnu) https://github.com/tisnik/most-popular-python-libs/blob/master/pdm/project8/

20. Odkazy na Internetu

  1. Stránka projektu PDM
    https://pdm.fming.dev/latest/
  2. PDF na GitHubu
    https://github.com/pdm-project/pdm
  3. PEP 582 – Python local packages directory
    https://peps.python.org/pep-0582/
  4. PDM na PyPi
    https://pypi.org/project/pdm/
  5. Which Python package manager should you use?
    https://towardsdatascience.com/which-python-package-manager-should-you-use-d0fd0789a250
  6. How to Use PDM to Manage Python Dependencies without a Virtual Environment
    https://www.youtube.com/wat­ch?v=qOIWNSTYfcc
  7. What are the best Python package managers?
    https://www.slant.co/topics/2666/~best-python-package-managers
  8. PEP 621 – Storing project metadata in pyproject.toml
    https://peps.python.org/pep-0621/
  9. Pick a Python Lockfile and Improve Security
    https://blog.phylum.io/pick-a-python-lockfile-and-improve-security/
  10. PyPA specifications
    https://packaging.python.or­g/en/latest/specification­s/
  11. Creation of virtual environments
    https://docs.python.org/3/li­brary/venv.html
  12. How to Use virtualenv in Python
    https://learnpython.com/blog/how-to-use-virtualenv-python/
  13. Python Virtual Environments: A Primer
    https://realpython.com/python-virtual-environments-a-primer/
  14. virtualenv Cheatsheet
    https://aaronlelevier.git­hub.io/virtualenv-cheatsheet/
  15. Installing Python Modules
    https://docs.python.org/3/in­stalling/index.html

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.