Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 722 – Abhängigkeitsspezifikation für einzelne Skripte

Autor:
Paul Moore <p.f.moore at gmail.com>
PEP-Delegate:
Brett Cannon <brett at python.org>
Discussions-To:
Discourse thread
Status:
Abgelehnt
Typ:
Standards Track
Thema:
Packaging
Erstellt:
19. Juli 2023
Post-History:
19. Juli 2023
Ersetzt-Durch:
723
Resolution:
21. Okt. 2023

Inhaltsverzeichnis

Warnung

Diese PEP wurde zugunsten von PEP 723 abgelehnt.

×

Zusammenfassung

Diese PEP spezifiziert ein Format zum Einbetten von 3rd-Party-Abhängigkeiten in ein einzelnes Python-Skript.

Motivation

Nicht jeder Python-Code ist als „Projekt“ strukturiert, im Sinne eines eigenen Verzeichnisses mit einer pyproject.toml-Datei, das in ein installierbares Distributionspaket kompiliert wird. Python wird auch routinemäßig als Skriptsprache verwendet, wobei Python-Skripte eine (bessere) Alternative zu Shell-Skripten, Batch-Dateien usw. darstellen. Wenn Python zur Erstellung von Skripten verwendet wird, wird der Python-Code typischerweise als einzelne Datei gespeichert, oft in einem Verzeichnis, das solchen „Utility-Skripten“ gewidmet ist, das eine Mischung aus Sprachen sein kann, wobei Python nur eine unter vielen Möglichkeiten ist. Solche Skripte können geteilt werden, oft durch etwas so Einfaches wie E-Mail oder einen Link zu einer URL wie einem Github-Gist. Aber sie werden typischerweise nicht als Teil eines normalen Arbeitsablaufs „verteilt“ oder „installiert“.

Ein Problem bei der Verwendung von Python als Skriptsprache auf diese Weise ist, wie das Skript in einer Umgebung ausgeführt werden kann, die alle vom Skript benötigten Drittanbieter-Abhängigkeiten enthält. Derzeit gibt es kein Standardwerkzeug, das dieses Problem angeht, und diese PEP versucht nicht, eines zu definieren. Jedes Werkzeug, das dieses Problem angeht, muss jedoch wissen, welche 3rd-Party-Abhängigkeiten ein Skript benötigt. Durch die Definition eines Standardformats für die Speicherung solcher Daten können bestehende Werkzeuge sowie zukünftige Werkzeuge diese Informationen erhalten, ohne dass Benutzer tool-spezifische Metadaten in ihre Skripte aufnehmen müssen.

Begründung

Da eine Schlüsselanforderung darin besteht, einzelne Skripte zu schreiben und sie einfach zu teilen, indem man jemandem eine Kopie des Skripts gibt, definiert die PEP einen Mechanismus zum Einbetten von Abhängigkeitsdaten innerhalb des Skripts selbst und nicht in einer externen Datei.

Wir definieren das Konzept eines Abhängigkeitsblocks, der Informationen darüber enthält, welche 3rd-Party-Pakete ein Skript benötigt.

Um Abhängigkeitsblöcke zu identifizieren, kann das Skript einfach als Textdatei gelesen werden. Dies ist beabsichtigt, da sich die Python-Syntax im Laufe der Zeit ändert, sodass der Versuch, das Skript als Python-Code zu parsen, die Auswahl einer bestimmten Version der Python-Syntax erfordern würde. Außerdem werden wahrscheinlich zumindest einige Tools nicht in Python geschrieben sein, und von ihnen die Implementierung eines Python-Parsers zu verlangen, ist zu viel Aufwand.

Um jedoch Änderungen am Kern-Python zu vermeiden, ist das Format so konzipiert, dass es für den Python-Parser als Kommentare erscheint. Es ist möglich, Code zu schreiben, bei dem ein Abhängigkeitsblock nicht als Kommentar interpretiert wird (zum Beispiel, indem er in einen Python-Mehrzeilen-String eingebettet wird), aber solche Verwendungen werden nicht empfohlen und können leicht vermieden werden, vorausgesetzt, Sie versuchen nicht absichtlich, ein pathologisches Beispiel zu erstellen.

Eine Überprüfung, wie andere Sprachen Skripten die Angabe ihrer Abhängigkeiten ermöglichen, zeigt, dass ein „strukturierter Kommentar“ wie dieser ein gängiger Ansatz ist.

Spezifikation

Der Inhalt dieses Abschnitts wird im Python Packaging User Guide, Abschnitt PyPA Specifications, als Dokument mit dem Titel „Embedding Metadata in Script Files“ veröffentlicht.

Jedes Python-Skript kann einen Abhängigkeitsblock enthalten. Der Abhängigkeitsblock wird identifiziert, indem das Skript als Textdatei gelesen wird (d. h. die Datei wird nicht als Python-Quellcode geparst) und nach der ersten Zeile der Form gesucht wird

# Script Dependencies:

Das Hash-Zeichen muss am Anfang der Zeile ohne vorangestellten Leerraum stehen. Der Text „Script Dependencies“ wird unabhängig von der Groß-/Kleinschreibung erkannt, und die Leerzeichen stellen beliebige Leerzeichen dar (obwohl mindestens ein Leerzeichen vorhanden sein muss). Der folgende reguläre Ausdruck erkennt die Kopfzeile des Abhängigkeitsblocks

(?i)^#\s+script\s+dependencies:\s*$

Tools, die den Abhängigkeitsblock lesen, DÜRFEN die Standard-Python-Codierungsdeklaration berücksichtigen. Wenn sie sich dagegen entscheiden, MÜSSEN sie die Datei als UTF-8 verarbeiten.

Nach der Kopfzeile werden alle Zeilen in der Datei bis zur ersten Zeile, die nicht mit einem #-Zeichen beginnt, als Abhängigkeitszeilen betrachtet und wie folgt behandelt:

  1. Das anfängliche #-Zeichen wird entfernt.
  2. Wenn die Zeile die Zeichenfolge „ # “ (LEERZEICHEN HASH LEERZEICHEN) enthält, werden diese Zeichen und alle nachfolgenden Zeichen verworfen. Dies ermöglicht Inline-Kommentare in Abhängigkeitsblöcken.
  3. Leerzeichen am Anfang und Ende des verbleibenden Textes werden verworfen.
  4. Wenn die Zeile jetzt leer ist, wird sie ignoriert.
  5. Der Inhalt der Zeile MUSS jetzt ein gültiger PEP 508 Abhängigkeitsspezifizierer sein.

Die Anforderung von Leerzeichen vor und nach dem # in einem Inline-Kommentar ist notwendig, um sie von einem Teil eines PEP 508 URL-Spezifizierers zu unterscheiden (der einen Hash enthalten kann, aber ohne umgebende Leerzeichen).

Verbraucher MÜSSEN mindestens validieren, dass alle Abhängigkeiten mit einem name gemäß PEP 508 beginnen, und sie KÖNNEN validieren, dass alle Abhängigkeiten vollständig mit PEP 508 konform sind. Sie MÜSSEN mit einem Fehler fehlschlagen, wenn sie einen ungültigen Spezifizierer finden.

Beispiel

Das Folgende ist ein Beispiel für ein Skript mit einem eingebetteten Abhängigkeitsblock

# In order to run, this script needs the following 3rd party libraries
#
# Script Dependencies:
#    requests
#    rich     # Needed for the output
#
#    # Not needed - just to show that fragments in URLs do not
#    # get treated as comments
#    pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686

import requests
from rich.pretty import pprint

resp = requests.get("https://peps.pythonlang.de/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])

Abwärtskompatibilität

Da Abhängigkeitsblöcke die Form eines strukturierten Kommentars haben, können sie hinzugefügt werden, ohne die Bedeutung bestehenden Codes zu verändern.

Es ist möglich, dass bereits ein Kommentar existiert, der der Form eines Abhängigkeitsblocks entspricht. Während der identifizierende Header-Text „Script Dependencies“ gewählt wird, um dieses Risiko zu minimieren, ist es dennoch möglich.

In dem seltenen Fall, dass ein bestehender Kommentar fälschlicherweise als Abhängigkeitsblock interpretiert würde, kann dies behoben werden, indem ein tatsächlicher Abhängigkeitsblock (der leer sein kann, wenn das Skript keine Abhängigkeiten hat) früher im Code hinzugefügt wird.

Sicherheitsimplikationen

Wenn ein Skript, das einen Abhängigkeitsblock enthält, mit einem Tool ausgeführt wird, das Abhängigkeiten automatisch installiert, könnte dies dazu führen, dass beliebiger Code in der Umgebung des Benutzers heruntergeladen und installiert wird.

Das Risiko hier ist Teil der Funktionalität des Tools, das zum Ausführen des Skripts verwendet wird, und sollte daher bereits vom Tool selbst behandelt werden. Das einzige zusätzliche Risiko, das durch diese PEP entsteht, ist, wenn ein nicht vertrauenswürdiges Skript mit einem Abhängigkeitsblock ausgeführt wird, wobei eine potenziell bösartige Abhängigkeit installiert werden könnte. Dieses Risiko wird durch die normale bewährte Methode des Überprüfens des Codes vor der Ausführung abgedeckt.

Wie man das lehrt

Das Format ist so konzipiert, dass es nahe an der Art und Weise liegt, wie ein Entwickler Skriptabhängigkeiten bereits in einem erklärenden Kommentar angeben würde. Die erforderliche Struktur ist bewusst minimal, damit die Formatierungsregeln leicht zu erlernen sind.

Benutzer müssen wissen, wie sie Python-Abhängigkeitsspezifizierer schreiben. Dies wird von PEP 508 abgedeckt, aber für einfache Beispiele (was für unerfahrene Benutzer die Norm sein dürfte) ist die Syntax entweder nur ein Paketname oder ein Name und eine Versionsbeschränkung, was eine ziemlich gut verstandene Syntax ist.

Benutzer werden auch wissen, wie man ein Skript mit einem Tool ausführt, das Abhängigkeitsdaten interpretiert. Dies wird von dieser PEP nicht abgedeckt, da es die Verantwortung eines solchen Tools ist, zu dokumentieren, wie es verwendet werden soll.

Beachten Sie, dass der Kern-Python-Interpreter keine Abhängigkeitsblöcke interpretiert. Dies kann für Anfänger zu Verwirrung führen, die versuchen, python some_script.py auszuführen und nicht verstehen, warum es fehlschlägt. Dies ist jedoch nicht anders als der aktuelle Status quo, bei dem die Ausführung eines Skripts ohne die vorhandenen Abhängigkeiten zu einem Fehler führt.

Im Allgemeinen wird davon ausgegangen, dass, wenn einem Anfänger ein Skript mit Abhängigkeiten gegeben wird (unabhängig davon, ob sie in einem Abhängigkeitsblock angegeben sind), die Person, die das Skript bereitstellt, erklären sollte, wie dieses Skript ausgeführt wird, und wenn dies die Verwendung eines Skriptausführungs-Tools beinhaltet, sollte dies vermerkt werden.

Empfehlungen

Dieser Abschnitt ist nicht-normativ und beschreibt lediglich „bewährte Praktiken“ bei der Verwendung von Abhängigkeitsblöcken.

Während es für Tools zulässig ist, minimale Validierungen von Anforderungen durchzuführen, sollten sie in der Praxis so viele „Plausibilitätsprüfungen“ wie möglich durchführen, auch wenn sie keine vollständige Überprüfung der PEP 508-Syntax durchführen können. Dies hilft sicherzustellen, dass nicht korrekt beendete Abhängigkeitsblöcke frühzeitig gemeldet werden. Ein guter Kompromiss zwischen dem minimalen Ansatz, nur zu prüfen, ob die Anforderung mit einem Namen beginnt, und der vollständigen PEP 508-Validierung besteht darin, nach einem reinen Namen oder einem Namen gefolgt von optionalen Leerzeichen und dann einem der Zeichen [ (extra), @ (urlspec), ; (marker) oder einem der Zeichen (<!=>~ (Version) zu suchen.

Skripte sollten im Allgemeinen den Abhängigkeitsblock am Anfang der Datei platzieren, entweder unmittelbar nach einer Shebang-Zeile oder direkt nach dem Skript-Docstring. Insbesondere sollte der Abhängigkeitsblock immer vor jeglichem ausführbaren Code in der Datei platziert werden. Dies erleichtert dem menschlichen Leser das Auffinden.

Referenzimplementierung

Die Implementierung des Codes für diesen Vorschlag in Python ist ziemlich unkompliziert, daher kann die Referenzimplementierung hier aufgenommen werden.

import re
import tokenize
from packaging.requirements import Requirement

DEPENDENCY_BLOCK_MARKER = r"(?i)^#\s+script\s+dependencies:\s*$"

def read_dependency_block(filename):
    # Use the tokenize module to handle any encoding declaration.
    with tokenize.open(filename) as f:
        # Skip lines until we reach a dependency block (OR EOF).
        for line in f:
            if re.match(DEPENDENCY_BLOCK_MARKER, line):
                break
        # Read dependency lines until we hit a line that doesn't
        # start with #, or we are at EOF.
        for line in f:
            if not line.startswith("#"):
                break
            # Remove comments. An inline comment is introduced by
            # a hash, which must be preceded and followed by a
            # space.
            line = line[1:].split(" # ", maxsplit=1)[0]
            line = line.strip()
            # Ignore empty lines
            if not line:
                continue
            # Try to convert to a requirement. This will raise
            # an error if the line is not a PEP 508 requirement
            yield Requirement(line)

Ein Format, das dem hier vorgeschlagenen ähnelt, wird bereits in pipx und pip-run unterstützt.

Abgelehnte Ideen

Warum keine anderen Metadaten einschließen?

Der Kernanwendungsfall, der von diesem Vorschlag abgedeckt wird, ist die Identifizierung der Abhängigkeiten, die ein eigenständiges Skript benötigt, um erfolgreich ausgeführt zu werden. Dies ist ein häufiges reales Problem, das derzeit von Skript-Runner-Tools mit implementierungsspezifischen Möglichkeiten zur Speicherung der Daten gelöst wird. Die Standardisierung des Speicherformats verbessert die Interoperabilität, indem das Skript nicht an einen bestimmten Runner gebunden wird.

Während argumentiert werden kann, dass andere Formen von Metadaten in einem eigenständigen Skript nützlich sein könnten, ist die Notwendigkeit weitgehend theoretisch. Praktisch gesehen verwenden Skripte entweder keine anderen Metadaten, oder sie speichern sie in bestehenden, weit verbreiteten (und daher de facto Standard-) Formaten. Zum Beispiel benötigen Skripte README-ähnlichen Text, typischerweise wird die Standard-Python-Modul-Docstring verwendet, und Skripte, die eine Version deklarieren wollen, verwenden die gängige Konvention, eine Variable __version__ zu haben.

Ein während der Diskussion über diese PEP aufgetretener Fall war die Möglichkeit, eine minimale Python-Version zu deklarieren, die ein Skript zur Ausführung benötigt, analog zum Kernmetadaten-Element Requires-Python für Pakete. Im Gegensatz zu Paketen werden Skripte normalerweise nur von einem Benutzer oder in einer Umgebung ausgeführt, in der mehrere Python-Versionen ungewöhnlich sind. Die Notwendigkeit dieser Metadaten ist daher bei Skripten weitaus weniger kritisch. Als weiterer Beweis dafür bieten die beiden derzeit verfügbaren wichtigsten Skript-Runner, pipx und pip-run keine Möglichkeit, diese Daten in einem Skript aufzunehmen.

Die Erstellung eines standardmäßigen „Metadaten-Container“-Formats würde die verschiedenen Ansätze vereinheitlichen, aber praktisch gesehen gibt es keinen wirklichen Bedarf an einer Vereinheitlichung, und die Störung würde entweder die Einführung verzögern oder höchstwahrscheinlich einfach bedeuten, dass Skriptautoren den Standard ignorieren.

Dieser Vorschlag konzentriert sich daher nur auf den einen Anwendungsfall, bei dem ein klarer Bedarf an etwas besteht und keine bestehende Standard- oder gängige Praxis.

Warum keine Markierung pro Zeile verwenden?

Anstatt eines Kommentarblocks mit einer Kopfzeile wäre eine andere Möglichkeit, eine Markierung auf jeder Zeile zu verwenden, etwa so:

# Script-Dependency: requests
# Script-Dependency: click

Während dies das Parsen von Zeilen einzeln erleichtert, hat es eine Reihe von Problemen. Das erste ist einfach, dass es ziemlich wortreich und weniger lesbar ist. Dies wird eindeutig durch das gewählte Schlüsselwort beeinflusst, aber alle vorgeschlagenen Optionen waren (nach Meinung des Autors) weniger lesbar als die Blockkommentarform.

Wichtiger ist, dass diese Form per Design es unmöglich macht, zu verlangen, dass die Abhängigkeitsspezifizierer alle in einem einzigen Block zusammengefasst sind. Infolgedessen ist es für einen menschlichen Leser nicht möglich, ohne sorgfältige Überprüfung der gesamten Datei sicherzustellen, dass er alle Abhängigkeiten identifiziert hat. Siehe die folgende Frage: „Warum keine mehreren Abhängigkeitsblöcke zulassen und diese zusammenführen?“ für weitere Diskussionen dieses Problems.

Schließlich ist das Parsen der „Kommentarblock“-Form, wie die Referenzimplementierung zeigt, praktisch nicht wesentlich schwieriger als das Parsen dieser Form.

Warum keine eigene Kommentarform für den Abhängigkeitsblock verwenden?

Eine frühere Version dieses Vorschlags verwendete ## zur Identifizierung von Abhängigkeitsblöcken. Leider implementiert der flake8-Linter eine Regel, die besagt, dass Kommentare nach dem anfänglichen #-Zeichen ein Leerzeichen haben müssen. Während der PEP-Autor diese Regel für falsch hält, ist sie standardmäßig aktiviert und führt daher zu fehlgeschlagenen Checks, wenn sie auf einen Abhängigkeitsblock trifft.

Darüber hinaus fügt der black-Formatter, obwohl er die ##-Form zulässt, für die meisten anderen Kommentarformen ein Leerzeichen nach dem # ein. Das bedeutet, dass bei einer alternativen Form wie #% die automatische Neuformatierung den Abhängigkeitsblock beschädigen würde. Formen, die ein Leerzeichen enthalten, wie # #, sind möglich, aber für den Durchschnittsnutzer weniger natürlich (das Weglassen des Leerzeichens ist ein offensichtlicher Fehler).

Obwohl es möglich ist, dass Linter und Formatter geändert werden könnten, um den neuen Standard zu erkennen, schien der Vorteil eines dedizierten Präfixes nicht ausreichend, um die Übergangskosten oder das Risiko zu rechtfertigen, dass Benutzer ältere Tools verwenden.

Warum keine mehreren Abhängigkeitsblöcke zulassen und diese zusammenführen?

Weil es für den menschlichen Leser zu einfach ist, die Tatsache zu übersehen, dass es einen zweiten Abhängigkeitsblock gibt. Dies könnte einfach dazu führen, dass der Skript-Runner unerwartet zusätzliche Pakete herunterlädt, oder es könnte sogar ein Weg sein, bösartige Pakete auf den Computer eines Benutzers zu schmuggeln (indem ein zweiter Abhängigkeitsblock im Körper des Skripts „versteckt“ wird).

Während hier das Prinzip „Keinen unvertrauenswürdigen Code ausführen“ gilt, sind die Vorteile nicht ausreichend, um das Risiko wert zu sein.

Warum kein standardmäßigeres Datenformat (z. B. TOML) verwenden?

Erstens ist die einzige praktische Wahl für ein alternatives Format TOML. Python-Packaging hat sich für TOML als strukturiertes Datenformat standardisiert, und die Verwendung eines anderen Formats wie YAML oder JSON würde die Komplexität und Verwirrung ohne echten Nutzen erhöhen.

Die Frage ist also im Wesentlichen: „Warum nicht TOML verwenden?“

Die Kernidee hinter dem „Abhängigkeitsblock“-Format ist die Definition von etwas, das natürlich als Kommentar im Skript lesbar ist. Abhängigkeitsdaten sind sowohl für Tools als auch für den menschlichen Leser nützlich, daher ist ein lesbares Format von Vorteil. TOML hingegen hat notwendigerweise seine eigene Syntax, die von den zugrunde liegenden Daten ablenkt.

Es ist wichtig zu bedenken, dass Entwickler, die Skripte in Python schreiben, oft keine erfahrenen Python- oder Python-Packaging-Nutzer sind. Sie sind oft Systemadministratoren oder Datenanalysten, die Python einfach als „bessere Batch-Datei“ verwenden. Für solche Benutzer ist das TOML-Format höchstwahrscheinlich unbekannt, und die Syntax wird ihnen obskur und nicht besonders intuitiv sein. Solche Entwickler kopieren möglicherweise Abhängigkeitsspezifizierer aus Quellen wie Stack Overflow, ohne sie wirklich zu verstehen. Die Einbettung einer solchen Anforderung in eine TOML-Struktur ist eine zusätzliche Komplexität – und es ist wichtig zu bedenken, dass das Ziel hier darin besteht, die Verwendung von 3rd-Party-Bibliotheken für solche Benutzer einfach zu machen.

Darüber hinaus ist TOML von Natur aus ein flexibles Format, das sehr allgemeine Datenstrukturen unterstützt. Es gibt viele Möglichkeiten, eine einfache Liste von Zeichenketten darin zu schreiben, und unerfahrene Benutzer werden nicht klar erkennen, welche Form sie verwenden sollen.

Ein weiteres potenzielles Problem ist, dass die Verwendung eines allgemeinen TOML-Parsers in einigen Fällen zu einem messbaren Performance-Overhead führen kann. Die Startzeit wird oft als Problem bei der Ausführung kleiner Skripte genannt, daher könnte dies ein Problem für Skript-Runner sein, die auf hohe Leistung abzielen.

Und schließlich wird es Tools geben, die Abhängigkeitsdaten in Skripte schreiben sollen – zum Beispiel eine IDE mit einer Funktion, die automatisch einen Import und einen Abhängigkeitsspezifizierer hinzufügt, wenn Sie auf eine Bibliotheksfunktion verweisen. Obwohl es Bibliotheken gibt, die das Bearbeiten von TOML-Daten ermöglichen, sind sie nicht immer gut darin, das Layout des Benutzers beizubehalten. Selbst wenn es Bibliotheken gibt, die dies effektiv tun, ist die Erwartung, dass alle Tools eine solche Bibliothek verwenden, eine erhebliche Belastung für die Unterstützung dieses PEP.

Durch die Wahl eines einfachen, zeilenbasierten Formats ohne Anführungsregeln sind Abhängigkeitsdaten leicht zu lesen (für Menschen und Tools) und zu schreiben. Das Format hat nicht die Flexibilität von etwas wie TOML, aber der Anwendungsfall verlangt diese Art von Flexibilität einfach nicht.

Warum keine (möglicherweise eingeschränkte) Python-Syntax verwenden?

Dies würde typischerweise das Speichern der Abhängigkeiten als (Laufzeit-)Listenvariable mit einem konventionellen Namen beinhalten, wie z. B.:

__requires__ = [
    "requests",
    "click",
]

Andere Vorschläge umfassen einen statischen Mehrzeilen-String oder die Einbeziehung der Abhängigkeiten in die Docstring des Skripts.

Das bedeutendste Problem mit diesem Vorschlag ist, dass er erfordert, dass alle Verbraucher der Abhängigkeitsdaten einen Python-Parser implementieren. Selbst wenn die Syntax eingeschränkt ist, wird der Rest des Skripts die volle Python-Syntax verwenden, und der Versuch, eine Syntax zu definieren, die isoliert vom umgebenden Code erfolgreich geparst werden kann, ist wahrscheinlich extrem schwierig und fehleranfällig.

Außerdem ändert sich die Python-Syntax in jeder Version. Wenn das Extrahieren von Abhängigkeitsdaten einen Python-Parser benötigt, muss der Parser wissen, für welche Python-Version das Skript geschrieben ist, und der Overhead für ein generisches Tool, einen Parser zu haben, der mehrere Python-Versionen verarbeiten kann, ist unhaltbar.

Selbst wenn die oben genannten Probleme behoben werden könnten, würde das Format den Eindruck erwecken, dass die Daten zur Laufzeit geändert werden könnten. Dies ist jedoch im Allgemeinen nicht der Fall, und Code, der dies versucht, wird unerwartetes und verwirrendes Verhalten hervorrufen.

Und schließlich gibt es keine Beweise dafür, dass die Verfügbarkeit von Abhängigkeitsdaten zur Laufzeit von praktischem Nutzen ist. Sollte sich ein solcher Nutzen ergeben, ist es einfach genug, die Daten durch Parsen des Quellcodes zu erhalten - read_dependency_block(__file__).

Es ist jedoch erwähnenswert, dass das Dienstprogramm pip-run diesen Ansatz (in einer erweiterten Form) implementiert. Weitere Diskussionen über das pip-run-Design sind auf dem Issue-Tracker des Projekts verfügbar.

Warum keine pyproject.toml-Datei in das Skript einbetten?

Erstens basiert pyproject.toml auf TOML, daher gelten alle vorherigen Bedenken bezüglich TOML als Format. pyproject.toml ist jedoch ein Standard, der von Python-Packaging verwendet wird, und die Wiederverwendung eines bestehenden Standards ist ein vernünftiger Vorschlag, der auf seine eigenen Vorzüge hin gewürdigt werden muss.

Das erste Problem ist, dass der Vorschlag selten impliziert, dass alle pyproject.toml für Skripte unterstützt werden sollen. Ein Skript ist nicht dazu bestimmt, in ein verteilbares Artefakt wie ein Wheel kompiliert zu werden (siehe unten mehr dazu), daher ist der Abschnitt [build-system] von pyproject.toml wenig sinnvoll. Und während die tool-spezifischen Abschnitte von pyproject.toml für Skripte nützlich sein könnten, ist es nicht klar, ob ein Tool wie ruff die Konfiguration pro Datei auf diese Weise unterstützen würde, was zu Verwirrung führt, wenn Benutzer erwarten, dass es funktioniert, aber es tut es nicht. Darüber hinaus ist eine solche tool-spezifische Konfiguration für einzelne Dateien in einem größeren Projekt ebenso nützlich, sodass wir überlegen müssen, was es bedeuten würde, eine pyproject.toml in eine einzelne Datei eines größeren Projekts einzubetten, das seine eigene pyproject.toml hat.

Darüber hinaus konzentriert sich pyproject.toml derzeit auf Projekte, die in Wheels kompiliert werden sollen. Es gibt eine laufende Diskussion darüber, wie pyproject.toml für Projekte verwendet werden kann, die nicht als Wheels kompiliert werden sollen, und bis diese Frage geklärt ist (was wahrscheinlich eigene PEPs erfordert), scheint es verfrüht, die Einbettung von pyproject.toml in Skripte zu diskutieren, die definitiv nicht auf diese Weise kompiliert und verteilt werden sollen.

Die Schlussfolgerung ist daher (die in einigen, aber nicht allen Fällen explizit angegeben wurde), dass dieser Vorschlag bedeutet, dass wir einen Teil von pyproject.toml einbetten würden. Typischerweise ist dies der Abschnitt [project] aus PEP 621 oder sogar nur das Element dependencies aus diesem Abschnitt.

An diesem Punkt ist das erste Problem, dass wir durch die Formulierung des Vorschlags als „Einbetten von pyproject.toml“ die in den vorherigen Absätzen diskutierte Verwirrung fördern würden – Entwickler erwarten die vollen Fähigkeiten von pyproject.toml und sind verwirrt, wenn es Unterschiede und Einschränkungen gibt. Es wäre daher besser, diesen Vorschlag einfach als Vorschlag zur Verwendung eines eingebetteten TOML-Formats zu betrachten, aber speziell die Struktur eines bestimmten Teils von pyproject.toml wiederzuverwenden. Das Problem besteht dann darin, wie wir diese Struktur beschreiben, ohne Verwirrung bei Personen zu verursachen, die mit pyproject.toml vertraut sind. Wenn wir sie mit Bezug auf pyproject.toml beschreiben, ist die Verknüpfung immer noch da. Aber wenn wir sie isoliert beschreiben, werden die Leute durch die „ähnliche, aber andere“ Natur der Struktur verwirrt sein.

Es ist auch wichtig zu bedenken, dass ein wichtiger Teil der Zielgruppe dieses Vorschlags Entwickler sind, die Python einfach als „bessere Batch-Datei“-Lösung verwenden. Diese Entwickler sind im Allgemeinen nicht mit Python-Packaging und seinen Konventionen vertraut und sind oft die Personen, die die „Komplexität“ und „Schwierigkeit“ von Packaging-Lösungen am schärfsten kritisieren. Infolgedessen sind Vorschläge, die auf diesen bestehenden Lösungen basieren, für diese Zielgruppe wahrscheinlich unerwünscht und könnten leicht dazu führen, dass Leute einfach bestehende Ad-hoc-Lösungen weiter verwenden und den Standard ignorieren, der ihr Leben erleichtern sollte.

Warum keine Anforderungen aus Importanweisungen ableiten?

Die Idee wäre, import-Anweisungen im Quellcode automatisch zu erkennen und in eine Liste von Anforderungen umzuwandeln.

Dies ist jedoch aus mehreren Gründen nicht praktikabel. Erstens gelten die oben genannten Punkte zur Notwendigkeit, die Syntax für alle Python-Versionen, auch für Tools in anderen Sprachen, leicht parsen zu können, hier ebenso.

Zweitens bieten PyPI und andere Paket-Repositories, die der Simple Repository API entsprechen, keinen Mechanismus zur Auflösung von Paketnamen aus den importierten Modulnamen (siehe auch diese verwandte Diskussion).

Drittens, selbst wenn Repositories diese Informationen anbieten würden, kann derselbe Importname mehreren Paketen auf PyPI entsprechen. Man könnte einwenden, dass die Auflösung, welches Paket gewünscht wird, nur dann erforderlich ist, wenn mehrere Projekte denselben Importnamen bereitstellen. Dies würde es jedoch jedem ermöglichen, funktionierende Skripte versehentlich oder böswillig zu brechen, indem ein Paket auf PyPI hochgeladen wird, das einen Importnamen bereitstellt, der mit einem bestehenden Projekt identisch ist. Die Alternative, dass unter den Kandidaten das zuerst im Index registrierte Paket gewählt wird, wäre verwirrend, wenn ein beliebtes Paket mit demselben Importnamen wie ein bestehendes obskures Paket entwickelt wird, und sogar schädlich, wenn das bestehende Paket Malware ist, die absichtlich mit einem ausreichend generischen Importnamen hochgeladen wurde, der eine hohe Wahrscheinlichkeit hat, wiederverwendet zu werden.

Eine verwandte Idee wäre, die Anforderungen als Kommentare an die Importanweisungen anzuhängen, anstatt sie in einem Block zu sammeln, mit einer Syntax wie:

import numpy as np # requires: numpy
import rich # requires: rich

Dies leidet immer noch unter Parsing-Schwierigkeiten. Außerdem ist die Platzierung des Kommentars bei Mehrzeilen-Imports mehrdeutig und kann hässlich aussehen.

from PyQt5.QtWidgets import (
    QCheckBox, QComboBox, QDialog, QDialogButtonBox,
    QGridLayout, QLabel, QSpinBox, QTextEdit
) # requires: PyQt5

Darüber hinaus kann diese Syntax nicht in allen Situationen intuitiv erwartet werden. Betrachten Sie:

import platform
if platform.system() == "Windows":
    import pywin32 # requires: pywin32

Hier ist die Absicht des Benutzers, dass das Paket nur unter Windows benötigt wird, aber dies kann vom Skript-Runner nicht verstanden werden (der richtige Weg, dies zu schreiben, wäre requires: pywin32 ; sys_platform == 'win32').

(Danke an Jean Abou-Samra für die klare Erörterung dieses Punktes)

Warum die Umgebung nicht zur Laufzeit verwalten?

Ein weiterer Ansatz zum Ausführen von Skripten mit Abhängigkeiten ist einfach die Verwaltung dieser Abhängigkeiten zur Laufzeit. Dies kann durch die Verwendung einer Bibliothek erfolgen, die Pakete verfügbar macht. Es gibt viele Optionen zur Implementierung einer solchen Bibliothek, z. B. durch direkte Installation in die Umgebung des Benutzers oder durch Manipulation von sys.path, um sie aus einem lokalen Cache verfügbar zu machen.

Diese Ansätze sind nicht unvereinbar mit dieser PEP. Eine API wie

env_mgr.install("rich")
env_mgr.install("click")

import rich
import click

...

ist sicherlich machbar. Eine solche Bibliothek könnte jedoch ohne neue Standards geschrieben werden, und soweit dem PEP-Autor bekannt ist, ist dies noch nicht geschehen. Dies deutet darauf hin, dass ein solcher Ansatz nicht so attraktiv ist, wie er zunächst erscheint. Es gibt auch das Bootstrapping-Problem, die env_mgr-Bibliothek überhaupt verfügbar zu machen. Und schließlich bietet dieser Ansatz keine Interoperabilitätsvorteile, da er keine Standardform für die Abhängigkeitsliste verwendet und so andere Tools nicht auf die Daten zugreifen können.

In jedem Fall könnte eine solche Bibliothek von diesem Vorschlag immer noch profitieren, da sie eine API zum Lesen der zu installierenden Pakete aus dem Skript-Abhängigkeitsblock enthalten könnte. Dies würde die gleiche Funktionalität bieten und gleichzeitig die Interoperabilität mit anderen Tools ermöglichen, die diese Spezifikation unterstützen.

# Script Dependencies:
#     rich
#     click
env_mgr.install_dependencies(__file__)

import rich
import click

...

Warum nicht einfach ein Python-Projekt mit einer pyproject.toml einrichten?

Auch hier ist ein Schlüsselproblem, dass die Zielgruppe dieses Vorschlags Personen sind, die Skripte schreiben, die nicht für die Verteilung bestimmt sind. Manchmal werden Skripte „geteilt“, aber dies ist weit informeller als „Verteilung“ – es beinhaltet typischerweise das Senden eines Skripts per E-Mail mit einigen schriftlichen Anweisungen zur Ausführung oder das Weitergeben eines Links zu einem Gist.

Von solchen Benutzern zu erwarten, dass sie die Komplexität von Python-Packaging erlernen, ist ein erheblicher Schritt nach oben in der Komplexität und würde fast sicher den Eindruck erwecken, dass „Python für Skripte zu schwierig ist“.

Darüber hinaus, wenn die Erwartung hier ist, dass die pyproject.toml irgendwie für die Ausführung von Skripten an Ort und Stelle ausgelegt sein wird, ist das eine neue Funktion des Standards, die derzeit nicht existiert. Zumindest ist dies kein vernünftiger Vorschlag, bis die aktuelle Diskussion auf Discourse über die Verwendung von pyproject.toml für Projekte, die nicht als Wheels kompiliert werden, abgeschlossen ist. Und selbst dann adressiert es nicht den Anwendungsfall „jemandem ein Skript in einem Gist oder per E-Mail senden“.

Warum keine Anforderungen-Datei für Abhängigkeiten verwenden?

Die Anforderungen in eine Anforderungen-Datei zu legen, erfordert keine PEP. Sie können das jetzt tun, und tatsächlich ist es sehr wahrscheinlich, dass viele Ad-hoc-Lösungen dies tun. Ohne einen Standard gibt es jedoch keine Möglichkeit zu wissen, wie die Abhängigkeitsdaten eines Skripts gefunden werden sollen. Und darüber hinaus ist das Format der Anforderungen-Datei pip-spezifisch, sodass Tools, die sich darauf verlassen, von einer Pip-Implementierungsdetails abhängig sind.

Um also einen Standard zu schaffen, wären zwei Dinge erforderlich:

  1. Eine standardisierte Ersetzung für das Format der Anforderungen-Datei.
  2. Ein Standard dafür, wie die Anforderungen-Datei für ein gegebenes Skript gefunden werden soll.

Der erste Punkt ist ein bedeutendes Unterfangen. Er wurde mehrfach diskutiert, aber bisher hat niemand versucht, ihn tatsächlich umzusetzen. Der wahrscheinlichste Ansatz wäre die Entwicklung von Standards für einzelne Anwendungsfälle, die derzeit mit Anforderungen-Dateien abgedeckt werden. Eine Option hier wäre, dass diese PEP einfach ein neues Dateiformat definiert, das nur eine Textdatei ist, die PEP 508-Anforderungen, eine pro Zeile, enthält. Das würde nur die Frage hinterlassen, wie diese Datei gefunden werden soll.

Die „offensichtliche“ Lösung wäre, etwas wie die Benennung der Datei mit demselben Namen wie das Skript, aber mit der Erweiterung .reqs (oder etwas Ähnlichem) zu tun. Dies erfordert jedoch immer noch zwei Dateien, wo derzeit nur eine einzige Datei benötigt wird, und entspricht somit nicht dem Modell der „besseren Batch-Datei“ (Shell-Skripte und Batch-Dateien sind typischerweise in sich abgeschlossen). Es erfordert, dass der Entwickler daran denkt, die beiden Dateien zusammenzuhalten, und das ist nicht immer möglich. Zum Beispiel können Systemadministrationsrichtlinien erfordern, dass alle Dateien in einem bestimmten Verzeichnis ausführbar sind (die Linux-Dateisystemstandards schreiben dies für /usr/bin vor). Und einige Methoden zum Teilen eines Skripts (z. B. das Veröffentlichen auf einer Textdatei-Sharing-Dienst wie Githubs Gist oder einem Firmenintranet) erlauben möglicherweise nicht, den Speicherort einer zugehörigen Anforderungen-Datei aus dem Speicherort des Skripts abzuleiten (Tools wie pipx unterstützen die Ausführung eines Skripts direkt von einer URL, sodass „Zip-Datei des Skripts und seiner Abhängigkeiten herunterladen und entpacken“ möglicherweise keine geeignete Anforderung ist).

Im Wesentlichen ist hier jedoch das Problem, dass die explizit formulierte Anforderung besteht, dass das Format das Speichern von Abhängigkeitsdaten in der Skriptdatei selbst unterstützt. Lösungen, die dies nicht tun, ignorieren diese Anforderung einfach.

Sollten Skripte einen Paketindex angeben können?

Abhängigkeitsmetadaten beziehen sich darauf, wonach das Paket abhängig ist, und nicht woher dieses Paket stammt. Hier gibt es keinen Unterschied zwischen Metadaten für Skripte und Metadaten für Verteilungspakete (wie in pyproject.toml definiert). In beiden Fällen werden Abhängigkeiten in "abstrakter" Form angegeben, ohne zu spezifizieren, wie sie bezogen werden.

Einige Tools, die die Abhängigkeitsinformationen verwenden, müssen natürlich konkrete Abhängigkeitsartefakte lokalisieren – zum Beispiel, wenn sie eine Umgebung erstellen möchten, die diese Abhängigkeiten enthält. Die Art und Weise, wie sie dies tun, wird jedoch eng mit der allgemeinen Benutzeroberfläche des Tools verknüpft sein, und diese PEP versucht nicht, die Benutzeroberfläche von Tools vorzuschreiben.

Weitere Diskussionen zu diesem Punkt, insbesondere zu den von dem Tool pip-run getroffenen UI-Entscheidungen, finden Sie in dem bereits erwähnten pip-run-Issue.

Was ist mit lokalen Abhängigkeiten?

Diese können ohne spezielle Metadaten und Tooling gehandhabt werden, indem einfach der Speicherort der Abhängigkeiten zu sys.path hinzugefügt wird. Diese PEP ist für diesen Fall einfach nicht erforderlich. Wenn andererseits die "lokalen Abhängigkeiten" tatsächliche, lokal veröffentlichte Distributionen sind, können sie wie üblich mit einer PEP 508-Anforderung angegeben werden, und der lokale Paketindex wird beim Ausführen eines Tools über die Benutzeroberfläche des Tools dafür spezifiziert.

Offene Fragen

Zu diesem Zeitpunkt keine.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0722.rst

Zuletzt geändert: 2025-07-08 12:01:15 GMT