Kapitel 4. Abhängigkeitsmanagement

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Python-Programmiererinnen und -Programmierer profitieren von einem reichhaltigen Ökosystem an Bibliotheken und Tools von Drittanbietern. Die Tatsache, dass du auf den Schultern von Giganten stehst, hat ihren Preis: Die Pakete, auf die du für deine Projekte angewiesen bist, hängen in der Regel selbst von einer Reihe von Paketen ab. Alle diese Pakete sind in Bewegung - solange ein Projekt existiert, werden seine Betreuer immer wieder neue Versionen veröffentlichen, um Fehler zu beheben, Funktionen hinzuzufügen und sich an das sich entwickelnde Ökosystem anzupassen.

Die Verwaltung von Abhängigkeiten ist eine große Herausforderung, wenn du Software über einen längeren Zeitraum pflegst. Du musst dein Projekt auf dem neuesten Stand halten, schon allein um Sicherheitslücken rechtzeitig zu schließen. Das erfordert oft, dass du deine Abhängigkeiten auf die neueste Version aktualisierst - nur wenige Open-Source-Projekte haben die Ressourcen, um Sicherheitsupdates für ältere Versionen zu verteilen. Du wirst deine Abhängigkeiten ständig aktualisieren müssen! Wenn du diesen Prozess so reibungslos, automatisch und zuverlässig wie möglich gestaltest, zahlt sich das sehr aus.

Abhängigkeiten eines Python-Projekts sind die Pakete von Drittanbietern, die in seiner Umgebung installiert werden müssen.1 Meistens entsteht eine Abhängigkeit von einem Paket, weil es ein Modul verteilt, das du importierst. Wir sagen auch, dass das Projekt ein Paket benötigt.

Viele Projekte verwenden auch Tools von Drittanbietern für Entwickleraufgaben, wie z.B. die Ausführung der Testsuite oder die Erstellung der Dokumentation. Diese Pakete werden als Entwicklungsabhängigkeiten bezeichnet: Endnutzer brauchen sie nicht, um deinen Code auszuführen. Ein ähnlicher Fall sind die Build-Abhängigkeiten aus Kapitel 3, mit denen du Pakete für dein Projekt erstellen kannst.

Abhängigkeiten sind wie Verwandte. Wenn du von einem Paket abhängig bist, sind seine Abhängigkeiten auch deine Abhängigkeiten - egal, wie sehr du sie magst. Diese Pakete werden als indirekte Abhängigkeiten bezeichnet; du kannst sie dir unter wie einen Baum vorstellen, an dessen Wurzel dein Projekt steht.

In diesem Kapitel wird erklärt, wie du Abhängigkeiten effektiv verwalten kannst. Im nächsten Abschnitt erfährst du, wie du Abhängigkeiten in pyproject.toml als Teil der Projektmetadaten festlegst. Danach spreche ich über Entwicklungsabhängigkeiten und Anforderungsdateien. Zum Schluss erkläre ich dir, wie du Abhängigkeiten an bestimmte Versionen binden kannst, um zuverlässige Einsätze und wiederholbare Prüfungen zu ermöglichen.

Hinzufügen von Abhängigkeiten zur Beispielanwendung

Als Arbeitsbeispiel wollen wir random-wikipedia-article ausBeispiel 3-1 mit der HTTPX-Bibliothek erweitern, einem vollwertigen HTTP-Client, der sowohl synchrone als auch asynchrone Anfragen sowie die neuere (und viel effizientere) Protokollversion HTTP/2 unterstützt. Außerdem verbesserst du die Ausgabe des Programms mitRich, einer Bibliothek für Rich Text und schöne Formatierungen im Terminal.

Eine API mit HTTPX konsumieren

Wikipedia bittet Entwickler/innen, einen User-Agent Header mit Kontaktdaten zu setzen. Das dient nicht dazu, Postkarten zu verschicken, um den Leuten zu gratulieren, dass sie die Wikipedia-API gekonnt nutzen. Sie haben so die Möglichkeit, sich zu melden, wenn ein Kunde versehentlich ihre Server beschädigt.

Beispiel 4-1 zeigt, wie du verwenden kannst, um mit dem Header httpx eine Anfrage an die Wikipedia-API zu senden. Du könntest auch die Standardbibliothek verwenden, um einen User-Agent Header mit deinen Anfragen zu senden. Aber httpx bietet eine intuitivere, eindeutigere und flexiblere Schnittstelle, auch wenn du keine der erweiterten Funktionen verwendest.

Beispiel 4-1. Verwendung von httpx für die Nutzung der Wikipedia-API
import textwrap
import httpx

API_URL = "https://en.wikipedia.org/api/rest_v1/page/random/summary"
USER_AGENT = "random-wikipedia-article/0.1 (Contact: you@example.com)"

def main():
    headers = {"User-Agent": USER_AGENT}

    with httpx.Client(headers=headers) as client: 1
        response = client.get(API_URL, follow_redirects=True) 2
        response.raise_for_status() 3
        data = response.json() 4

    print(data["title"], end="\n\n")
    print(textwrap.fill(data["extract"]))
1

Wenn du eine Client-Instanz erstellst, kannst du Kopfzeilen angeben, die mit jeder Anfrage gesendet werden sollen, z. B. die User-Agent Kopfzeile. Die Verwendung des Clients als Kontextmanager stellt sicher, dass die Netzwerkverbindung am Ende des with Blocks geschlossen wird.

2

Diese Zeile führt zwei HTTP GET Anfragen an die API aus. Die erste geht an den zufälligen Endpunkt, der mit einem Redirect zum eigentlichen Artikel antwortet. Die zweite Anfrage folgt der Weiterleitung.

3

Die Methode raise_for_status löst eine Ausnahme aus, wenn die Serverantwort über ihren Statuscode einen Fehler anzeigt.

4

Die Methode json abstrahiert die Details des Parsens des Antwortkörpers als JSON.

Konsolenausgabe mit Rich

Wenn du schon dabei bist, lass uns auch das Aussehen des Programms verbessern.Beispiel 4-2 verwendet Rich, eine Bibliothek für Konsolenausgaben, um den Titel des Artikels fett darzustellen. Das kratzt kaum an der Oberfläche der Formatierungsmöglichkeiten von Rich. Moderne Terminals sind erstaunlich leistungsfähig, und mit Rich kannst du ihr Potenzial mühelos ausschöpfen. In deroffiziellen Dokumentation findest du weitere Informationen.

Beispiel 4-2. Rich zur Verbesserung der Konsolenausgabe verwenden
import httpx
from rich.console import Console

def main():
    ...
    console = Console(width=72, highlight=False) 1
    console.print(data["title"], style="bold", end="\n\n") 2
    console.print(data["extract"])
1

Konsolenobjekte bieten eine funktionsreiche print Methode für die Konsolenausgabe. Die Einstellung der Konsolenbreite auf 72 Zeichen ersetzt unseren früheren Aufruf vontextwrap.fill. Du solltest auch die automatische Syntaxhervorhebung deaktivieren, da du eher Prosa als Daten oder Code formatierst.

2

Mit dem Schlüsselwort style kannst du den Titel durch eine fette Schrift hervorheben.

Festlegen von Abhängigkeiten für ein Projekt

Wenn du das noch nicht getan hast, erstelle und aktiviere eine virtuelle Umgebung für das Projekt und führe eine editierbare Installation aus dem aktuellen Verzeichnis durch:

$ uv venv
$ uv pip install --editable .

Du könntest versucht sein, httpx und rich manuell in die Umgebung zu installieren. Stattdessen fügst du sie zu den Projektabhängigkeiten in pyproject.toml hinzu. Dadurch wird sichergestellt, dass die beiden Pakete bei jeder Installation deines Projekts mit installiert werden:

[project]
name = "random-wikipedia-article"
version = "0.1"
dependencies = ["httpx", "rich"]
...

Wenn du das Projekt neu installierst, wirst du sehen, dass uv seine Abhängigkeiten ebenfalls installiert:

$ uv pip install --editable .

Jeder Eintrag im Feld dependencies ist eine Abhängigkeitsangabe. Neben dem Paketnamen kannst du hier zusätzliche Informationen angeben: Versionsangaben, Extras und Umgebungsmarkierungen. Die folgenden Abschnitte erklären, was das ist.

Version Spezifizierer

Versionsangaben definieren den Bereich der akzeptablen Versionen für ein Paket. Wenn du eine neue Abhängigkeit hinzufügst, ist es eine gute Idee, die aktuelle Version als untere Grenze anzugeben - es sei denn, dein Projekt muss mit älteren Versionen kompatibel sein. Aktualisiere die untere Grenze, sobald du dich auf neuere Funktionen des Pakets verlässt:

[project]
dependencies = ["httpx>=0.27.0", "rich>=13.7.1"]

Warum sollten Sie Untergrenzen für Ihre Abhängigkeiten angeben? Installationsprogramme wählen standardmäßig die neueste Version für eine Abhängigkeit. Es gibt drei Gründe, warum dir das wichtig sein sollte. Erstens werden Bibliotheken normalerweise zusammen mit anderen Paketen installiert, die zusätzliche Versionsbeschränkungen haben können. Zweitens werden auch Anwendungen nicht immer isoliert installiert - zum Beispiel können Linux-Distributionen deine Anwendung für die systemweite Umgebung paketieren. Drittens helfen dir Untergrenzen dabei, Versionskonflikte in deinem eigenen Abhängigkeitsbaum zu erkennen - zum Beispiel, wenn du eine aktuelle Version eines Pakets benötigst, eine andere Abhängigkeit aber nur mit älteren Versionen funktioniert.

Vermeide spekulative Versionsobergrenzen - du solltest dich nicht vor neueren Versionen schützen, es sei denn, du weißt, dass sie nicht mit deinem Projekt kompatibel sind. Siehe"Obere Versionsgrenzen in Python" über Probleme mit Versionskappungen.

Sperrdateien sind eine viel bessere Lösung für durch Abhängigkeiten verursachte Fehler als Obergrenzen - sie fordern "bekannt gute" Versionen deiner Abhängigkeiten an, wenn du einen Dienst bereitstellst oder automatische Prüfungen durchführst (siehe"Sperren von Abhängigkeiten").

Wenn eine verpfuschte Version dein Projekt kaputt macht, veröffentliche ein Bugfix-Release , um diese spezielle kaputte Version auszuschließen:

[project]
dependencies = ["awesome>=1.2,!=1.3.1"]

Verwende eine Obergrenze als letzten Ausweg, wenn eine Abhängigkeit die Kompatibilität dauerhaft unterbricht. Hebe die Versionsobergrenze auf, sobald du deinen Code anpassen kannst:

[project]
dependencies = ["awesome>=1.2,<2"]
Warnung

Der nachträgliche Ausschluss von Versionen birgt einen Fallstrick, dessen du dir bewusst sein musst: Die Auflöser von Abhängigkeiten können beschließen, dein Projekt auf eine Version ohne den Ausschluss herabzustufen und die Abhängigkeit trotzdem aktualisieren. Lock-Dateien können dabei helfen.

Versionsspezifizierer unterstützen mehrere Operatoren, wie inTabelle 4-1 gezeigt. Kurz gesagt: Verwende die Gleichheits- und Vergleichsoperatoren, die du aus Python kennst: ==, !=, <=, >=, <, und >.

Tabelle 4-1. Versionsbezeichner
Betreiber Name Beschreibung

==

Versionsabgleich

Die Versionen müssen nach der Normalisierung gleich sein. Nachstehende Nullen werden abgezogen.

!=

Version Ausschluss

Die Umkehrung des == Operators

<=, >=

Inklusive geordneter Vergleich

Führt einen lexikografischen Vergleich durch. Vorabversionen gehen den endgültigen Versionen voraus.

<, >

Exklusiv bestellter Vergleich

Ähnlich wie bei Inclusive, aber die Versionen müssen nicht gleich sein

~=

Kompatible Veröffentlichung

Äquivalent zu >=x.y,==x.* mit der angegebenen Genauigkeit

===

Willkürliche Gleichheit

Einfacher String-Vergleich für nicht standardisierte Versionen

Drei Betreiber verdienen eine zusätzliche Diskussion:

  • Der == Operator unterstützt Wildcards (*), allerdings nur am Ende der Versionszeichenfolge. Mit anderen Worten: Du kannst verlangen, dass die Version einem bestimmten Präfix entspricht, z. B. 1.2.*.

  • Mit dem Operator === kannst du einen einfachen Zeichen-für-Zeichen-Vergleich durchführen. Er wird am besten als letzter Ausweg für nicht standardisierte Versionen verwendet.

  • Der ~= Operator für kompatible Versionen gibt an, dass die Version größer oder gleich dem angegebenen Wert sein soll, aber immer noch mit demselben Präfix beginnt. Zum Beispiel ist ~=1.2.3 gleichbedeutend mit >=1.2.3,==1.2.* und ~=1.2ist gleichbedeutend mit >=1.2,==1.*.

Du brauchst dich nicht vor Vorabversionen zu schützen - Versionsspezifikationen schließen sie standardmäßig aus. Vorabversionen sind nur in drei Situationen gültige Kandidaten: wenn sie bereits installiert sind, wenn keine anderen Versionen die Abhängigkeitsspezifikation erfüllen, und wenn du sie explizit mit einer Klausel wie>=1.0.0rc1 anforderst.

Extras

Angenommen, du möchtest das neuere HTTP/2-Protokoll mit httpx verwenden. Dies erfordert nur eine kleine Änderung des Codes, der den HTTP-Client erstellt:

def main():
    headers = {"User-Agent": USER_AGENT}
    with httpx.Client(headers=headers, http2=True) as client:
        ...

Unter der Haube delegiert httpx die Feinheiten des HTTP/2-Protokolls an ein anderes Paket, h2. Diese Abhängigkeit wird jedoch nicht standardmäßig aktiviert. Auf diese Weise kommen Nutzer, die das neuere Protokoll nicht brauchen, mit einem kleineren Abhängigkeitsbaum aus. Du brauchst es hier, also aktiviere die optionale Funktion mit der Syntaxhttpx[http2]:

[project]
dependencies = ["httpx[http2]>=0.27.0", "rich>=13.7.1"]

Optionale Funktionen, die zusätzliche Abhängigkeiten erfordern, werden als Extras bezeichnet, und du kannst mehr als eine haben. Du könntest zum Beispielhttpx[http2,brotli] angeben, um die Dekodierung von Antworten mit Brotli-Kompression zu ermöglichen. Das ist ein Kompressionsalgorithmus, der bei Google entwickelt wurde und in Webservern und Content Delivery Networks weit verbreitet ist.

Optionale Abhängigkeiten

Betrachten wir diese Situation aus der Sicht von httpx. Die Abhängigkeiten h2und brotli sind optional, deshalb deklariert httpx sie unteroptional-dependencies statt dependencies(Beispiel 4-3).

Beispiel 4-3. Optionale Abhängigkeiten von httpx (vereinfacht)
[project]
name = "httpx"

[project.optional-dependencies]
http2 = ["h2>=3,<5"]
brotli = ["brotli"]

Das Feld optional-dependencies ist eine TOML-Tabelle. Sie kann mehrere Listen von Abhängigkeiten enthalten, eine für jedes Extra, das das Paket bereitstellt. Jeder Eintrag ist eine Abhängigkeitsspezifikation und verwendet die gleichen Regeln wie das Feld dependencies.

Wenn du eine optionale Abhängigkeit zu deinem Projekt hinzufügst, wie verwendest du sie in deinem Code? Überprüfe nicht, ob dein Paket mit dem Extra installiert wurde - importiere einfach das optionale Paket. Du kannst die ImportError Ausnahme abfangen, wenn der Benutzer das Extra nicht angefordert hat:

try:
    import h2
except ImportError:
    h2 = None

# Check h2 before use.
if h2 is not None:
    ...

Dieses Muster ist in Python so weit verbreitet, dass es einen Namen und ein Akronym hat: "Easier to Ask Forgiveness than Permission" (EAFP). Sein weniger pythonisches Gegenstück heißt "Look Before You Leap" (LBYL).

Umweltmarkierungen

Die dritte Metadatengruppe, die du für eine Abhängigkeit angeben kannst, sind die Umgebungsmarkierungen. Bevor ich erkläre, was diese Marker sind, möchte ich dir ein Beispiel zeigen, wo sie nützlich sind.

Wenn du dir den User-Agent Header inBeispiel 4-1 ansiehst und denkst: "Ich sollte die Versionsnummer nicht im Code wiederholen müssen", hast du völlig recht. Wie du in"Single-Sourcing der Projektversion" gesehen hast, kannst du die Version deines Pakets aus den Metadaten in der Umgebung lesen.

Beispiel 4-4 zeigt , wie du die Funktion importlib.metadata.metadata verwenden kannst, um die Kopfzeile User-Agent aus den Kernmetadatenfeldern Name, Version und Author-email zu konstruieren. Diese Felder entsprechen den Feldern name, version und authors in den Projekt-Metadaten.3

Beispiel 4-4. importlib.metadata verwenden, um eine User-Agent Kopfzeile zu erstellen
from importlib.metadata import metadata

USER_AGENT = "{Name}/{Version} (Contact: {Author-email})"

def build_user_agent():
    fields = metadata("random-wikipedia-article") 1
    return USER_AGENT.format_map(fields) 2

def main():
    headers = {"User-Agent": build_user_agent()}
    ...
1

Die Funktion metadata ruft die wichtigsten Metadatenfelder für das Paket ab.

2

Die Funktion str.format_map sucht nach jedem Platzhalter in der Zuordnung.

Die Bibliothek importlib.metadata wurde in Python 3.8 eingeführt. Jetzt ist sie zwar in allen unterstützten Versionen verfügbar, aber das war nicht immer so. Hattest du Pech, wenn du eine ältere Python-Version unterstützen musstest?

Nicht ganz. Zum Glück gibt es für viele Ergänzungen der StandardbibliothekBackports -Paketevon Drittanbietern, die die Funktionalität für ältere Interpreter bereitstellen. Für importlib.metadata kannst du auf denimportlib-metadata Backport von PyPI zurückgreifen. Der Backport ist immer noch nützlich, weil die Bibliothek nach ihrer Einführung mehrmals geändert wurde.

Du brauchst Backports nur in Umgebungen, die bestimmte Python-Versionen verwenden. Mit einer Umgebungsmarkierung kannst du dies als bedingte Abhängigkeit ausdrücken:

importlib-metadata; python_version < '3.8'

Installateure installieren das Paket nur auf Interpretern, die älter als Python 3.8 sind.

Allgemeiner ausgedrückt, drückt ein Umgebungsmarker eine Bedingung aus, die eine Umgebung erfüllen muss, damit die Abhängigkeit gilt. Installateure werten die Bedingung auf dem Interpreter der Zielumgebung aus.

Mit Umgebungsmarkierungen kannst du Abhängigkeiten für bestimmte Betriebssysteme, Prozessorarchitekturen, Python-Implementierungen oder Python-Versionen anfordern.Tabelle 4-2 listet alle Umgebungsmarkierungen auf, die dir gemäß PEP 508 zur Verfügung stehen.4

Tabelle 4-2. Umweltmarkierungena
Umweltmarker Standardbibliothek Beschreibung Beispiele

os_name

os.name()

Die Betriebssystemfamilie

posix, nt

sys_platform

sys.platform()

Die Kennung der Plattform

linux, darwin, win32

platform_system

platform.system()

Der Systemname

Linux, Darwin, Windows

platform_release

platform.release()

Die Version des Betriebssystems

23.2.0

platform_version

platform.version()

Die Systemfreigabe

Darwin Kernel Version 23.2.0: ...

platform_machine

platform.machine()

Die Prozessorarchitektur

x86_64, arm64

python_version

platform.python​_ver⁠sion_tuple()

Die Python-Feature-Version im Format x.y

3.12

python_full_version

platform.python​_ver⁠sion()

Die vollständige Python-Version

3.12.0, 3.13.0a4

platform_python​_imple⁠mentation

platform.python​_imple⁠mentation()

Die Python-Implementierung

CPython, PyPy

implementation_name

sys.implementa⁠tion​.name

Die Python-Implementierung

cpython, pypy

implementation​_ver⁠sion

sys.implementation​.ver⁠sion

Die Version der Python-Implementierung

3.12.0, 3.13.0a4

a Die Markierungen python_version und implementation_version wenden Transformationen an. Siehe PEP 508 für Details.

Zurück zu Beispiel 4-4, hier sind die Felder requires-python und dependencies, um das Paket mit Python 3.7 kompatibel zu machen:

[project]
requires-python = ">=3.7"
dependencies = [
    "httpx[http2]>=0.24.1",
    "rich>=13.7.1",
    "importlib-metadata>=6.7.0; python_version < '3.8'",
]

Der Importname für den Backport ist importlib_metadata, während das Modul der Standardbibliothek importlib.metadata heißt. Du kannst das entsprechende Modul in deinem Code importieren, indem du die Python-Version in sys.version_info überprüfst:

if sys.version_info >= (3, 8):
    from importlib.metadata import metadata
else:
    from importlib_metadata import metadata

Habe ich gerade jemanden "EAFP" schreien hören? Wenn deine Importe von der Python-Version abhängen, ist es besser, die Technik von"Optional dependencies" und "look before you leap" zu vermeiden. Eine explizite Versionsprüfung teilt statischen Analysatoren wie oder dem mypy type checker (siehe Kapitel 10) deine Absicht mit. EAFP kann bei diesen Tools zu Fehlern führen, weil sie nicht erkennen können, wann welches Modul verfügbar ist.

Markierungen unterstützen dieselben Gleichheits- und Vergleichsoperatoren wie Versionsspezifikationen(Tabelle 4-1). Zusätzlich kannst du in undnot in verwenden, um eine Teilzeichenkette mit der Markierung abzugleichen. Der Ausdruck'arm' in platform_version prüft zum Beispiel, ob platform.version() die Zeichenfolge'arm' enthält.

Du kannst auch mehrere Markierungen mit den booleschen Operatoren and undor kombinieren. Hier ist ein ziemlich ausgeklügeltes Beispiel, das all diese Funktionen kombiniert:

[project]
dependencies = ["""                                                         \
  awesome-package; python_full_version <= '3.8.1'                           \
    and (implementation_name == 'cpython' or implementation_name == 'pypy') \
    and sys_platform == 'darwin'                                            \
    and 'arm' in platform_version                                           \
"""]

Das Beispiel basiert auch auf TOMLs Unterstützung für mehrzeilige Strings, die genau wie Python dreifache Anführungszeichen verwendet. Abhängigkeitsangaben können sich nicht über mehrere Zeilen erstrecken, daher musst du die Zeilenumbrüche mit einem Backslash ausblenden.

Entwicklung Abhängigkeiten

Entwicklungsabhängigkeiten sind Pakete von Drittanbietern, die du während der Entwicklung benötigst. Als Entwickler verwendest du vielleicht das Testframework pytest, um die Testsuite für dein Projekt auszuführen, das Dokumentationssystem Sphinx , um seine Dokumente zu erstellen, oder eine Reihe anderer Tools, die dir bei der Projektpflege helfen. Deine Nutzer/innen hingegen müssen keines dieser Pakete installieren, um deinen Code auszuführen.

Ein Beispiel: Testen mit pytest

Als konkretes Beispiel wollen wir einen kleinen Test für die Funktion build_user_agentaus Beispiel 4-4 hinzufügen. Erstelle ein Verzeichnis tests mit zwei Dateien: eine leere __init__.py und ein Modultest_random_wikipedia_article.py mit dem Code ausBeispiel 4-5.

Beispiel 4-5. Testen des erzeugten User-Agent Headers
from random_wikipedia_article import build_user_agent

def test_build_user_agent():
    assert "random-wikipedia-article" in build_user_agent()

Beispiel 4-5 verwendet nur eingebaute Python-Funktionen, sodass du den Test einfach importieren und manuell ausführen könntest. Aber selbst für diesen kleinen Test fügt pytest drei nützliche Funktionen hinzu. Erstens findet es Module und Funktionen, deren Namen mit test beginnen. So kannst du deine Tests ausführen, indem dupytest ohne Argumente aufrufst. Zweitens zeigt pytest die Tests an, während sie ausgeführt werden, und gibt am Ende eine Zusammenfassung mit den Testergebnissen aus. Drittens schreibt pytest die Assertions in deinen Tests so um, dass du freundliche und informative Meldungen erhältst, wenn sie fehlschlagen.

Lass uns den Test mit pytest durchführen. Ich gehe davon aus, dass du bereits eine aktive virtuelle Umgebung mit einer bearbeitbaren Installation deines Projekts hast. Gib die folgenden Befehle ein, um pytest in dieser Umgebung zu installieren und auszuführen:

$ uv pip install pytest
$ py -m pytest
========================= test session starts ==========================
platform darwin -- Python 3.12.2, pytest-8.1.1, pluggy-1.4.0
rootdir: ...
plugins: anyio-4.3.0
collected 1 item

tests/test_random_wikipedia_article.py .                          [100%]

========================== 1 passed in 0.22s ===========================

Im Moment sieht es gut aus. Tests helfen deinem Projekt, sich weiterzuentwickeln, ohne etwas kaputt zu machen. Der Test für build_user_agent ist ein erster Schritt in diese Richtung. Die Installation und der Betrieb von pytest sind im Vergleich zu diesen langfristigen Vorteilen nur geringe Infrastrukturkosten.

Das Einrichten einer Projektumgebung wird schwieriger, wenn du mehr Entwicklungsabhängigkeiten erwirbst - Dokumentationsgeneratoren, Linters, Code-Formatierer, Typprüfungen oder andere Werkzeuge. Auch für deine Testsuite brauchst du vielleicht mehr als pytest: Plugins für pytest, Tools zur Messung der Codeabdeckung oder einfach Pakete, die dir helfen, deinen Code zu üben.

Außerdem brauchst du kompatible Versionen dieser Pakete - deine Testsuite benötigt vielleicht die neueste Version von pytest, während deine Dokumentation nicht auf der neuen Sphinx-Version aufbauen kann. Jedes deiner Projekte kann leicht unterschiedliche Anforderungen haben. Multipliziere dies mit der Anzahl der Entwickler, die an jedem Projekt arbeiten, und es wird klar, dass du eine Möglichkeit brauchst, deine Entwicklungsabhängigkeiten zu verfolgen.

Zum jetzigen Zeitpunkt gibt es in Python keine Standardmethode, um die Entwicklungsabhängigkeiten eines Projekts zu deklarieren - obwohl viele Python-Projektmanager sie in ihrer [tool] Tabelle unterstützen und ein PEP-Entwurf existiert.5 Neben den Projektmanagern gibt es zwei weitere Ansätze, um die Lücke zu schließen: optionale Abhängigkeiten und Anforderungsdateien.

Optionale Abhängigkeiten

Wie du in "Extras" gesehen hast , enthält die Tabelle optional-dependenciesGruppen von optionalen Abhängigkeiten, die Extras genannt werden. Sie hat drei Eigenschaften, die sie für das Aufspüren von Entwicklungsabhängigkeiten geeignet machen. Erstens werden die Pakete nicht standardmäßig installiert, so dass die Endnutzer ihre Python-Umgebung nicht mit ihnen belasten müssen. Zweitens kannst du die Pakete unter aussagekräftigen Namen wie tests oder docs gruppieren. Und drittens verfügt das Feld über die volle Ausdruckskraft von Abhängigkeitsspezifikationen, einschließlich Versionsbeschränkungen und Umgebungsmarkierungen.

Auf der anderen Seite gibt es ein Impedanzungleichgewicht zwischen Entwicklungsabhängigkeiten und optionalen Abhängigkeiten. Optionale Abhängigkeiten werden den Nutzern über die Paket-Metadaten angezeigt - sie ermöglichen es den Nutzern, sich für Funktionen zu entscheiden, die zusätzliche Pakete erfordern. Im Gegensatz dazu sollen die Nutzer keine Entwicklungsabhängigkeiten installieren - diese Pakete werden für keine der Funktionen benötigt, die den Nutzern zur Verfügung stehen.

Außerdem kannst du ohne das Projekt selbst keine Extras installieren. Im Gegensatz dazu müssen nicht alle Entwicklerwerkzeuge dein Projekt installieren. Linters zum Beispiel analysieren deinen Quellcode auf Fehler und mögliche Verbesserungen. Du kannst sie auf einem Projekt ausführen, ohne sie in der Umgebung zu installieren. "Fette" Umgebungen verschwenden nicht nur Zeit und Platz, sondern schränken auch die Auflösung von Abhängigkeiten unnötig ein. Zum Beispiel konnten viele Python-Projekte wichtige Abhängigkeiten nicht mehr aktualisieren, als der Linter Flake8 eine Versionsobergrenze auf importlib-metadata setzte.

Vor diesem Hintergrund werden Extras häufig für Entwicklungsabhängigkeiten verwendet und sind die einzige Methode, die von einem Paketierungsstandard abgedeckt wird. Sie sind eine pragmatische Wahl, vor allem, wenn du Linters mit pre-commit verwaltest (siehe Kapitel 9).Beispiel 4-6 zeigt, wie du Extras verwendest, um Pakete zu verfolgen, die für Tests und Dokumentation benötigt werden.

Beispiel 4-6. Extras verwenden, um Entwicklungsabhängigkeiten darzustellen
[project.optional-dependencies]
tests = ["pytest>=7.4.4", "pytest-sugar>=1.0.0"] 1
docs = ["sphinx>=5.3.0"] 2
1

Das pytest-sugar Plugin erweitert die Ausgabe von pytest um einen Fortschrittsbalken und zeigt Fehler sofort an.

2

Sphinx ist ein Dokumentationsgenerator , der von der offiziellen Python-Dokumentation und vielen Open-Source-Projekten verwendet wird.

Du kannst jetzt die Testabhängigkeiten mit dem tests extra installieren:

$ uv pip install -e ".[tests]"
$ py -m pytest

Du kannst auch ein dev extra mit allen Entwicklungsabhängigkeiten definieren. So kannst du in einem Rutsch eine Entwicklungsumgebung mit deinem Projekt und allen Tools, die es verwendet, einrichten:

$ uv pip install -e ".[dev]"

Es ist nicht nötig, alle Pakete zu wiederholen, wenn du dev definierst. Stattdessen kannst du einfach auf die anderen Extras verweisen, wie inBeispiel 4-7 gezeigt.

Beispiel 4-7. Ein dev extra mit allen Entwicklungsabhängigkeiten bereitstellen
[project.optional-dependencies]
tests = ["pytest>=7.4.4", "pytest-sugar>=1.0.0"]
docs = ["sphinx>=5.3.0"]
dev = ["random-wikipedia-article[tests,docs]"]

Diese Art, ein Extra zu deklarieren, wird auch als rekursive optionale Abhängigkeit bezeichnet, da das Paket mit dem Extra dev von sich selbst abhängt (mit den Extrastests und docs ).

Anforderungen Dateien

Anforderungsdateien sind reine Textdateien mit Abhängigkeitsangaben in jeder Zeile(Beispiel 4-8). Zusätzlich können sie URLs und Pfade enthalten, optional mit dem Präfix -e für eine editierbare Installation, sowie globale Optionen wie -r, um eine andere Anforderungsdatei einzubinden oder--index-url, um einen anderen Paketindex als PyPI zu verwenden. Das Dateiformat unterstützt auch Kommentare im Python-Stil (mit einem führenden # Zeichen) und Zeilenfortsetzungen (mit einem abschließenden \ Zeichen).

Beispiel 4-8. Eine einfache requirements.txt-Datei
pytest>=7.4.4
pytest-sugar>=1.0.0
sphinx>=5.3.0

Du kannst die Abhängigkeiten, die unter in einer Anforderungsdatei aufgeführt sind, mit pip oder uv installieren:

$ uv pip install -r requirements.txt

Normalerweise heißt eine Anforderungsdatei requirements.txt. Es sind jedoch auch andere Namen üblich. Du könntest eine dev-requirements.txt für Entwicklungsabhängigkeiten oder ein Anforderungsverzeichnis mit einer Datei pro Abhängigkeitsgruppe haben(Beispiel 4-9).

Beispiel 4-9. Verwendung von Anforderungsdateien zur Festlegung von Entwicklungsabhängigkeiten
# requirements/tests.txt
-e . 1
pytest>=7.4.4
pytest-sugar>=1.0.0

# requirements/docs.txt
sphinx>=5.3.0 2

# requirements/dev.txt
-r tests.txt 3
-r docs.txt
1

Die Datei tests.txt erfordert eine bearbeitbare Installation des Projekts, da die Testsuite die Anwendungsmodule importieren muss.

2

Für die Datei docs.txt ist das Projekt nicht erforderlich. (Das setzt voraus, dass du die Dokumentation nur aus statischen Dateien erstellst. Wenn du die autodoc Sphinx-Erweiterung verwendest, um die API-Dokumentation aus den Docstrings in deinem Code zu erstellen, brauchst du auch hier das Projekt).

3

Die Datei dev.txt enthält die anderen Anforderungsdateien.

Hinweis

Wenn du andere Anforderungsdateien mit -r einfügst, werden ihre Pfade relativ zu der einschließenden Datei ausgewertet. Im Gegensatz dazu werden Pfade zu Abhängigkeiten relativ zu deinem aktuellen Verzeichnis ausgewertet, das normalerweise das Projektverzeichnis ist.

Erstelle und aktiviere eine virtuelle Umgebung und führe dann die folgenden Befehle aus, um die Entwicklungsabhängigkeiten zu installieren und die Testsuite auszuführen:

$ uv pip install -r requirements/dev.txt
$ py -m pytest

Anforderungsdateien sind nicht Teil der Projektmetadaten. Du teilst sie mit anderen Entwicklern über die Versionskontrolle, aber sie sind für deine Nutzer unsichtbar. Für Entwicklungsabhängigkeiten ist das genau das, was du willst. Außerdem schließen Anforderungsdateien dein Projekt nicht implizit in die Abhängigkeiten ein. Das spart Zeit bei allen Aufgaben, für die das Projekt nicht installiert werden muss.

Anforderungsdateien haben auch ihre Schattenseiten. Sie sind kein Paketierungsstandard und werden es wahrscheinlich auch nicht werden - jede Zeile einer Anforderungsdatei ist im Wesentlichen ein Argument für pip install. "Was immer pip tut" mag für viele Kanten in der Python-Paketierung das ungeschriebene Gesetz bleiben, aber die Standards der Community ersetzen es mehr und mehr. Ein weiterer Nachteil ist die Unordnung, die diese Dateien in deinem Projekt verursachen, verglichen mit einer Tabelle in pyproject.toml.

Wie bereits erwähnt, kannst du bei Python-Projektmanagern Entwicklungsabhängigkeiten inpyproject.toml außerhalb der Projektmetadaten deklarieren - Rye, Hatch, PDM und Poetry bieten alle diese Funktion. In Kapitel 5 findest du eine Beschreibung der Abhängigkeitsgruppen von Poetry.

Sperren von Abhängigkeiten

Du hast deine Abhängigkeiten in einer lokalen Umgebung oder in der kontinuierlichen Integration (CI) installiert und deine Testsuite und alle anderen Prüfungen, die du eingerichtet hast, ausgeführt. Alles sieht gut aus, und du bist bereit, deinen Code zu verteilen. Aber wie installierst du dieselben Pakete in der Produktionsumgebung, die du bei deinen Tests verwendet hast?

Die Verwendung unterschiedlicher Pakete in der Entwicklung und in der Produktion hat Konsequenzen: In der Produktion kann ein Paket landen, das mit deinem Code nicht kompatibel ist, einen Fehler oder eine Sicherheitslücke aufweist oder - im schlimmsten Fall - von einem Angreifer gekapert wurde. Wenn dein Dienst häufig genutzt wird, ist dieses Szenario besorgniserregend - und es kann jedes Paket in deinem Abhängigkeitsbaum betreffen, nicht nur die Pakete, die du direkt importierst.

Warnung

Angriffe auf die Lieferkette infiltrieren ein System, indem sie auf die Abhängigkeiten von Dritten abzielen. Im Jahr 2022 lud beispielsweise ein Bedrohungsakteur mit dem Namen "JuiceLedger" bösartige Pakete in legitime PyPI-Projekte hoch, nachdem er sie mit einer Phishing-Kampagne kompromittiert hatte.6

Es gibt viele Gründe, warum Umgebungen bei gleichen Abhängigkeitsspezifikationen mit unterschiedlichen Paketen enden. Die meisten davon lassen sich in zwei Kategorien einteilen: Änderungen im vorgelagerten Bereich und Unstimmigkeiten in der Umgebung. Erstens kannst du unterschiedliche Pakete erhalten, wenn sich die Menge der verfügbaren Pakete im Vorfeld ändert:

  • Eine neue Version kommt herein, bevor du sie einsetzt.

  • Ein neues Artefakt wird für eine bestehende Version hochgeladen. Zum Beispiel laden Maintainer manchmal zusätzliche Wheels hoch, wenn eine neue Python-Version herauskommt.

  • Ein Maintainer löscht ein Release oder Artefakt. Yanking ist ein sanftes Löschen, das die Datei vor der Auflösung von Abhängigkeiten verbirgt, es sei denn, du forderst es ausdrücklich an.

Zweitens kannst du verschiedene Pakete beziehen, wenn deine Entwicklungsumgebung nicht mit der Produktionsumgebung übereinstimmt:

  • Umgebungsmarkierungen werden je nach Zielinterpreter unterschiedlich ausgewertet (siehe"Umgebungsmarkierungen"). Die Produktionsumgebung könnte zum Beispiel eine alte Python-Version verwenden, die einen Backport wieimportlib-metadata erfordert.

  • Wheel-Kompatibilitäts-Tags können dazu führen, dass der Installer ein anderes Wheel für dasselbe Paket auswählt (siehe "Wheel-Kompatibilitäts-Tags"). Das kann zum Beispiel passieren, wenn du auf einem Mac mit Apple-Silizium entwickelst, während die Produktion Linux auf einer x86-64-Architektur verwendet.

  • Wenn das Release kein Wheel für die Zielumgebung enthält, baut der Installer es spontan aus der sdist. Wheels für Erweiterungsmodule kommen oft zu spät, wenn eine neue Python-Version das Licht der Welt erblickt.

  • Wenn die Umgebungen nicht denselben Installer (oder unterschiedliche Versionen desselben Installers) verwenden, kann jeder Installer die Abhängigkeiten anders auflösen. Zum Beispiel verwendet uv den PubGrub-Algorithmus zur Auflösung von Abhängigkeiten,7 während pip einen Backtracking-Resolver für Python-Pakete verwendet, resolvelib.

  • Auch die Konfiguration oder der Zustand der Werkzeuge kann zu unterschiedlichen Ergebnissen führen - zum Beispiel, wenn du von einem anderen Paketindex oder aus einem lokalen Cache installierst.

Du brauchst eine Möglichkeit, um genau die Pakete zu definieren, die deine Anwendung benötigt, und du möchtest, dass ihre Umgebung ein genaues Abbild dieses Paketinventars ist. Dieser Prozess wird als Locking oder Pinning der Projektabhängigkeiten bezeichnet, die in einer Lock-Datei aufgelistet sind.

Bisher habe ich über das Sperren von Abhängigkeiten für zuverlässige und reproduzierbare Einsätze gesprochen. Das Sperren ist auch während der Entwicklung von Vorteil, sowohl für Anwendungen als auch für Bibliotheken. Indem du eine Lock-Datei mit deinem Team und den Mitwirkenden teilst, sind alle auf derselben Seite: Jeder Entwickler verwendet dieselben Abhängigkeiten, wenn er die Testsuite ausführt, die Dokumentation erstellt oder andere Aufgaben erledigt. Durch die Verwendung der Lock-Datei für obligatorische Prüfungen werden Überraschungen vermieden, wenn Prüfungen in der CI fehlschlagen, obwohl sie lokal bestanden wurden. Um diese Vorteile zu nutzen, müssen die Lock-Dateien auch die Entwicklungsabhängigkeiten enthalten.

Zum jetzigen Zeitpunkt gibt es in Python noch keinen Paketierungsstandard für Lock-Dateien - auch wenn das Thema aktiv diskutiert wird.8 In der Zwischenzeit haben viele Python-Projektmanager wie Poetry, PDM und pipenv ihre eigenen Sperrdateiformate implementiert; andere, wie Rye, verwenden Anforderungsdateien zum Sperren von Abhängigkeiten.

In diesem Abschnitt stelle ich zwei Methoden vor, um Abhängigkeiten mit Hilfe von Anforderungsdateien zu sperren: Einfrieren und Kompilieren von Anforderungen. InKapitel 5 beschreibe ich die Lock-Dateien von Poetry.

Einfrieren Anforderungen mit pip und uv

Anforderungsdateien sind ein beliebtes Format für sperrende Abhängigkeiten. Mit ihnen kannst du die Abhängigkeitsinformationen getrennt von den Projektmetadaten halten. Pip und uv können diese Dateien aus einer bestehenden Umgebung erzeugen:

$ uv pip install .
$ uv pip freeze
anyio==4.3.0
certifi==2024.2.2
h11==0.14.0
h2==4.1.0
hpack==4.0.0
httpcore==1.0.4
httpx==0.27.0
hyperframe==6.0.1
idna==3.6
markdown-it-py==3.0.0
mdurl==0.1.2
pygments==2.17.2
random-wikipedia-article @ file:///Users/user/random-wikipedia-article
rich==13.7.1
sniffio==1.3.1

Eine Bestandsaufnahme der in einer Umgebung installierten Pakete wird alsFreezing bezeichnet. Speichere die Liste in der Datei requirements.txt und übertrage die Datei in die Versionsverwaltung - mit einer Änderung: Ersetze die URL der Datei durch einen Punkt für das aktuelle Verzeichnis. So kannst du die Anforderungsdatei überall verwenden, solange du dich im Projektverzeichnis befindest.

Wenn du dein Projekt in die Produktion überführst, kannst du das Projekt und seine Abhängigkeiten wie folgt installieren:

$ uv pip install -r requirements.txt

Wenn deine Entwicklungsumgebung einen aktuellen Interpreter verwendet, wird in der Anforderungsdatei importlib-metadatanicht aufgeführt - diese Bibliothek ist nur vor Python 3.8 erforderlich. Wenn in deiner Produktionsumgebung eine ältere Python-Version läuft, wird dein Deployment nicht funktionieren. Daraus lässt sich eine wichtige Lehre ziehen: Sperre deine Abhängigkeiten in einer Umgebung, die mit der Produktionsumgebung übereinstimmt.

Tipp

Binde deine Abhängigkeiten an dieselbe Python-Version, Python-Implementierung, dasselbe Betriebssystem und dieselbe Prozessorarchitektur wie in der Produktionsumgebung. Wenn du in mehreren Umgebungen arbeitest, erstelle eine Anforderungsdatei für jede Umgebung.

Das Einfrieren von Anforderungen ist mit einigen Einschränkungen verbunden. Erstens musst du deine Abhängigkeiten jedes Mal installieren, wenn du die Anforderungsdatei aktualisierst. Zweitens kann es leicht passieren, dass du die Anforderungsdatei versehentlich verschmutzt, wenn du ein Paket vorübergehend installierst und danach vergisst, die Umgebung neu zu erstellen.9 Drittens kannst du mit Freezing keine Paket-Hashes aufzeichnen - es wird lediglich ein Inventar der Umgebung erstellt, und Umgebungen zeichnen keine Hashes für die Pakete auf, die du in sie installierst. (Auf Paket-Hashes gehe ich im nächsten Abschnitt ein.)

Anforderungen mit pip-tools und uv kompilieren

Mit dem pip-tools Projekt kannst du Abhängigkeiten ohne diese Einschränkungen sperren. Du kannst die Anforderungen direkt aus pyproject.toml kompilieren, ohne die Pakete zu installieren. Unter der Haube nutzt pip-tools pip und seinen Dependency Resolver.

Pip-tools verfügt über zwei Befehle: pip-compile Mit dem Befehl kannst du eine Anforderungsdatei aus Abhängigkeitsangaben erstellen und mit dem Befehl pip-sync kannst du die Anforderungsdatei auf eine bestehende Umgebung anwenden. Das uv-Tool bietet Drop-in-Ersetzungen für beide Befehle: uv pip compile und uv pip sync.

Führe pip-compile in einer Umgebung aus, die der Zielumgebung deines Projekts entspricht. Wenn du pipx verwendest, gib die Ziel-Python-Version an:

$ pipx run --python=3.12 --spec=pip-tools pip-compile

Standardmäßig liest pip-compile aus pyproject.toml und schreibt inrequirements.txt. Du kannst die Option --output-file verwenden, um ein anderes Ziel anzugeben. Das Tool gibt die Anforderungen auch in die Standardfehlerausgabe aus, es sei denn, du gibst --quiet an, um die Terminalausgabe auszuschalten.

Uv verlangt, dass du die Ein- und Ausgabedateien genau kennzeichnest:

$ uv pip compile --python-version=3.12 pyproject.toml -o requirements.txt

Pip-tools und uv kommentieren die Datei, um das abhängige Paket für jede Abhängigkeit anzugeben, sowie den Befehl, mit dem die Datei erzeugt wurde. Es gibt noch einen weiteren Unterschied zur Ausgabe von pip freeze: Die kompilierten Anforderungen enthalten nicht dein eigenes Projekt. Du musst es separat installieren, nachdem du die Anforderungsdatei angewendet hast.

In den Anforderungsdateien kannst du für jede Abhängigkeit Hashes für die Pakete angeben. Diese Hashes bieten eine weitere Sicherheitsebene für deine Verteilungen: Sie ermöglichen es dir, nur geprüfte Paketartefakte in der Produktion zu installieren. Die Option--generate-hashes enthält SHA-256-Hashes für jedes in der Anforderungsdatei aufgeführte Paket. Hier sind zum Beispiel die Hashes über die sdist- und wheel-Dateien für eine httpx Version:

httpx==0.27.0 \
--hash=sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5 \
--hash=sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5

Paket-Hashes machen Installationen deterministischer und reproduzierbarer. Sie sind auch ein wichtiges Werkzeug für Unternehmen, die jedes Artefakt, das in die Produktion geht, überprüfen müssen. Die Überprüfung der Integrität von Paketen verhindertOn-Path-Angriffe, bei denen ein Bedrohungsakteur ("man in the middle") einen Paketdownload abfängt, um ein kompromittiertes Artefakt zu liefern.

Hashes haben außerdem den Nebeneffekt, dass pip sich weigert, Pakete ohne sie zu installieren: Entweder haben alle Pakete Hashes oder keines. Hashes schützen dich also davor, Dateien zu installieren, die nicht in der Anforderungsdatei aufgeführt sind.

Installiere die Anforderungsdatei in der Zielumgebung mit pip oder uv, gefolgt von dem Projekt selbst. Du kannst die Installation mit ein paar Optionen abhärten: Die Option --no-deps stellt sicher, dass du nur Pakete installierst, die in der Anforderungsdatei aufgeführt sind, und die Option --no-cache verhindert, dass der Installer heruntergeladene oder lokal erstellte Artefakte wiederverwendet:

$ uv pip install -r requirements.txt
$ uv pip install --no-deps --no-cache .

Aktualisiere deine Abhängigkeiten in regelmäßigen Abständen . Einmal pro Woche kann für eine ausgereifte Anwendung, die in der Produktion läuft, akzeptabel sein. Bei einem Projekt, das sich in der Entwicklung befindet, ist ein tägliches Update angemessener - oder sogar, sobald die neuen Versionen verfügbar sind. Tools wie Dependabot und Renovate helfen bei dieser Aufgabe: Sie öffnen Pull Requests in deinen Repositories und aktualisieren die Abhängigkeiten automatisch.

Wenn du deine Abhängigkeiten nicht regelmäßig aktualisierst, kann es sein, dass du unter Zeitdruck zu einem "Big Bang"-Upgrade gezwungen wirst. Eine einzige Sicherheitslücke kann dich dazu zwingen, dein Projekt auf die Hauptversionen mehrerer Pakete zu portieren, ebenso wie Python selbst.

Du kannst deine Abhängigkeiten alle auf einmal oder eine Abhängigkeit nach der anderen aktualisieren. Verwende die Option --upgrade, um alle Abhängigkeiten auf ihre neueste Version zu aktualisieren, oder übergebe ein bestimmtes Paket mit der Option --upgrade-package (-P).

So kannst du zum Beispiel Rich auf die neueste Version aktualisieren:

$ uv pip compile -p 3.12 pyproject.toml -o requirements.txt -P rich

Bis jetzt hast du die Zielumgebung von Grund auf neu erstellt. Du kannst auchpip-sync verwenden, um die Zielumgebung mit der aktualisierten Anforderungsdatei zu synchronisieren. Installiere dazu nicht pip-tools in der Zielumgebung: Seine Abhängigkeiten könnten mit denen deines Projekts in Konflikt geraten. Verwende stattdessen pipx, wie du es mit pip-compile getan hast. Verweise pip-sync mit der Option--python-executable auf den Zielinterpreter:

$ py -m venv .venv
$ pipx run --spec=pip-tools pip-sync --python-executable=.venv/bin/python

Der Befehl entfernt das Projekt selbst, da es nicht in der Anforderungsdatei aufgeführt ist. Installiere es nach der Synchronisierung erneut:

$ .venv/bin/python -m pip install --no-deps --no-cache .

Uv verwendet standardmäßig die Umgebung in .venv, sodass du diese Befehle vereinfachen kannst:

$ uv pip sync requirements.txt
$ uv pip install --no-deps --no-cache .

In "Entwicklungsabhängigkeiten" hast du zwei Möglichkeiten gesehen, Entwicklungsabhängigkeiten zu deklarieren: Extras und Anforderungsdateien. Pip-tools und uv unterstützen beide als Eingabe. Wenn du Entwicklungsabhängigkeiten in einem dev extra verfolgst, erstelle die Dateidev-requirements.txt wie folgt:

$ uv pip compile --extra=dev pyproject.toml -o dev-requirements.txt

Wenn du feinkörnigere Extras hast, ist der Prozess derselbe. Vielleicht möchtest du die Anforderungsdateien in einem Anforderungsverzeichnis speichern, um Unordnung zu vermeiden.

Wenn du deine Entwicklungsabhängigkeiten in Anforderungsdateien statt in Extras angibst, kompiliere jede dieser Dateien nacheinander. Eingabedateien haben die Endung .in, während Ausgabedateien die Endung .txt haben(Beispiel 4-10).

Beispiel 4-10. Eingabeanforderungen für Entwicklungsabhängigkeiten
# requirements/tests.in
pytest>=7.4.4
pytest-sugar>=1.0.0

# requirements/docs.in
sphinx>=5.3.0

# requirements/dev.in
-r tests.in
-r docs.in

Anders als in Beispiel 4-9 wird in den Eingabeanforderungen nicht das Projekt selbst aufgeführt. Wenn sie das täten, würden die Ausgabeanforderungen den Pfad zum Projekt enthalten - und jeder Entwickler hätte am Ende einen anderen Pfad. Stattdessen übergibst du pyproject.toml zusammen mit den Eingabeanforderungen, um den gesamten Satz an Abhängigkeiten zusammenzufassen:

$ uv pip compile requirements/tests.in pyproject.toml -o requirements/tests.txt
$ uv pip compile requirements/docs.in -o requirements/docs.txt
$ uv pip compile requirements/dev.in pyproject.toml -o requirements/dev.txt

Erinnere dich daran, das Projekt zu installieren, nachdem du die Ausgabeanforderungen installiert hast.

Warum sollte man die dev.txt überhaupt kompilieren? Kann sie nicht einfach docs.txt undtests.txt enthalten? Wenn du separat gesperrte Anforderungen übereinander installierst, kann es passieren, dass sie miteinander in Konflikt geraten. Lass den Dependency Resolver das gesamte Bild sehen. Wenn du alle Eingangsanforderungen übergibst, kann er dir im Gegenzug einen konsistenten Abhängigkeitsbaum liefern.

Tabelle 4-3 fasst die Befehlszeilenoptionen fürpip-compile (und uv pip compile) zusammen, die du in diesem Kapitel gesehen hast:

Tabelle 4-3. Ausgewählte Befehlszeilenoptionen für pip-compile
Option Beschreibung

--generate-hashes

SHA-256-Hashes für alle Verpackungsartefakte einschließen

--output-file

Bestimme die Zieldatei

--quiet

Die Anforderungen nicht als Standardfehler ausgeben

--upgrade

Aktualisiere alle Abhängigkeiten auf ihre neueste Version

--upgrade-package=<package>

Upgrade eines bestimmten Pakets auf die neueste Version

--extra=<extra>

Abhängigkeiten aus dem angegebenen Extra in pyproject.toml einbinden

Zusammenfassung

In diesem Kapitel hast du gelernt, wie man Projektabhängigkeiten mitpyproject.toml deklariert und wie man Entwicklungsabhängigkeiten entweder mit extras oder mit Anforderungsdateien deklariert. Außerdem hast du gelernt, wie du Abhängigkeiten für zuverlässige Einsätze und reproduzierbare Prüfungen mit pip-tools und uv sperren kannst. Im nächsten Kapitel erfährst du, wie der Projektmanager Poetry bei der Verwaltung von Abhängigkeiten mit Hilfe von Abhängigkeitsgruppen und Lock-Dateien hilft.

1 Im weiteren Sinne bestehen die Abhängigkeiten eines Projekts aus allen Softwarepaketen, die die Benutzer benötigen, um den Code auszuführen - einschließlich des Interpreters, der Standardbibliothek, Pakete von Drittanbietern und Systembibliotheken. Conda und Distro-Pakete Manager wie APT, DNF und Homebrew unterstützen diesen allgemeinen Begriff der Abhängigkeiten.

2 Henry Schreiner, "Should You Use Upper Bound Version Constraints?", 9. Dezember 2021.

3 Der Einfachheit halber behandelt der Code nicht mehrere Autoren - welcher von ihnen in der Kopfzeile landet, ist undefiniert.

4 Robert Collins, "PEP 508 - Dependency specification for Python Software Packages", November 11, 2015.

5 Stephen Rosen, "PEP 735 - Dependency Groups in pyproject.toml", November 20, 2023.

6 Dan Goodin, "Actors Behind PyPI Supply Chain Attack Have Been Active Since Late 2021", September 2, 2022.

7 Natalie Weizenbaum, "PubGrub: Next-Generation Version Solving", April 2, 2018.

8 Brett Cannon, "Lock Files, Again (But This Time w/ Sdists!)", 22. Februar 2024.

9 Mit der Deinstallation des Pakets ist es nicht getan: Die Installation kann Nebeneffekte auf deinen Abhängigkeitsbaum haben. Sie kann zum Beispiel andere Pakete upgraden oder downgraden oder zusätzliche Abhängigkeiten mit sich bringen.

Get Hypermodern Python Tooling now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.