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

Python Enhancement Proposals

PEP 723 – Inline-Skript-Metadaten

Autor:
Ofek Lev <ofekmeister at gmail.com>
Sponsor:
Adam Turner <adam at python.org>
PEP-Delegate:
Brett Cannon <brett at python.org>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Thema:
Packaging
Erstellt:
04. August 2023
Post-History:
04. August 2023, 06. August 2023, 23. August 2023, 06. Dezember 2023
Ersetzt:
722
Resolution:
08. Januar 2024

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuelle, kanonische Spezifikation, Inline-Skript-Metadaten, wird auf der PyPA-Specs-Seite gepflegt.

×

Siehe den PyPA-Spezifikations-Update-Prozess, um Änderungen vorzuschlagen.

Zusammenfassung

Dieses PEP spezifiziert ein Metadatenformat, das in einzelne Python-Skripte eingebettet werden kann, um Launcher, IDEs und andere externe Werkzeuge zu unterstützen, die möglicherweise mit solchen Skripten interagieren müssen.

Motivation

Python wird routinemäßig als Skriptsprache verwendet, wobei Python-Skripte eine (bessere) Alternative zu Shell-Skripten, Batch-Dateien usw. darstellen. Wenn Python-Code als Skript strukturiert ist, wird er normalerweise als einzelne Datei gespeichert und erwartet keine Verfügbarkeit anderer lokaler Codes, die für Importe verwendet werden könnten. Als solches ist es möglich, ihn über beliebige textbasierte Mittel wie E-Mail, eine URL zum Skript oder sogar ein Chatfenster mit anderen zu teilen. Code, der auf diese Weise strukturiert ist, kann für immer als einzelne Datei bestehen bleiben, ohne jemals zu einem vollwertigen Projekt mit eigenem Verzeichnis und einer pyproject.toml-Datei zu werden.

Ein Problem, auf das Benutzer bei diesem Ansatz stoßen, ist, dass es keinen Standardmechanismus gibt, um Metadaten für Tools zu definieren, deren Aufgabe es ist, solche Skripte auszuführen. Zum Beispiel muss ein Tool, das ein Skript ausführt, möglicherweise wissen, welche Abhängigkeiten erforderlich sind oder welche Python-Versionen unterstützt werden.

Derzeit gibt es kein Standardwerkzeug, das dieses Problem angeht, und dieses PEP versucht *nicht*, eines zu definieren. Jedes Werkzeug, das dieses Problem *angeht*, muss jedoch die Laufzeitanforderungen von Skripten kennen. Durch die Definition eines Standardformats für die Speicherung solcher Metadaten können bestehende Werkzeuge sowie zukünftige Werkzeuge diese Informationen erhalten, ohne dass Benutzer Werkzeug-spezifische Metadaten in ihre Skripte aufnehmen müssen.

Begründung

Dieses PEP definiert einen Mechanismus zum Einbetten von Metadaten *innerhalb des Skripts selbst* und nicht in einer externen Datei.

Das Metadatenformat ist so konzipiert, dass es dem Layout von Daten in der pyproject.toml-Datei eines Python-Projektverzeichnisses ähnelt, um Benutzern, die Erfahrung mit Python-Projekten haben, eine vertraute Erfahrung zu bieten. Durch die Verwendung eines ähnlichen Formats vermeiden wir unnötige Inkonsistenzen zwischen Paketwerkzeugen, eine häufige Frustration, die von Benutzern in der jüngsten Paketierungs-Umfrage geäußert wurde.

Die folgenden sind einige der Anwendungsfälle, die dieses PEP unterstützen möchte

  • Eine benutzerorientierte CLI, die Skripte ausführen kann. Wenn wir Hatch als Beispiel nehmen, wäre die Schnittstelle einfach hatch run /path/to/script.py [args] und Hatch verwaltet die Umgebung für dieses Skript. Solche Werkzeuge könnten als Shebang-Zeilen auf Nicht-Windows-Systemen verwendet werden, z.B. #!/usr/bin/env hatch run.
  • Ein Skript, das zu einem Verzeichnis-Typ-Projekt übergehen möchte. Ein Benutzer kann lokal oder in einer Remote-REPL-Umgebung schnell Prototypen erstellen und dann zu einem formelleren Projektlayout übergehen, wenn seine Idee funktioniert. Die Fähigkeit, Abhängigkeiten im Skript zu definieren, wäre sehr nützlich, um vollständig reproduzierbare Fehlerberichte zu erhalten.
  • Benutzer, die eine manuelle Abhängigkeitsverwaltung vermeiden möchten. Zum Beispiel Paketmanager mit Befehlen zum Hinzufügen/Entfernen von Abhängigkeiten oder Automatisierung von Abhängigkeitsaktualisierungen in CI, die auf neue Versionen reagieren oder als Reaktion auf CVEs ausgelöst werden [1].

Spezifikation

Dieses PEP definiert ein Metadaten-Kommentarblockformat, das lose [2] von reStructuredText-Direktiven inspiriert ist.

Jedes Python-Skript kann Top-Level-Kommentarblöcke haben, die MIT der Zeile # /// TYPE beginnen müssen, wobei TYPE bestimmt, wie der Inhalt verarbeitet wird. Das heißt: ein einzelnes #, gefolgt von einem einzelnen Leerzeichen, gefolgt von drei Schrägstrichen, gefolgt von einem einzelnen Leerzeichen, gefolgt vom Metadatentyp. Der Block muss MIT der Zeile # /// enden. Das heißt: ein einzelnes #, gefolgt von einem einzelnen Leerzeichen, gefolgt von drei Schrägstrichen. Der TYPE darf nur aus ASCII-Buchstaben, Zahlen und Bindestrichen bestehen.

Jede Zeile zwischen diesen beiden Zeilen (# /// TYPE und # ///) muss ein Kommentar sein, der mit # beginnt. Wenn nach dem # Zeichen vorhanden sind, muss das erste Zeichen ein Leerzeichen sein. Der eingebettete Inhalt wird gebildet, indem die ersten beiden Zeichen jeder Zeile entfernt werden, wenn das zweite Zeichen ein Leerzeichen ist, andernfalls nur das erste Zeichen (was bedeutet, dass die Zeile nur ein einzelnes # enthält).

Die Priorität für eine Endzeile # /// wird gegeben, wenn die nächste Zeile keine gültige eingebettete Inhaltszeile wie oben beschrieben ist. Zum Beispiel ist das Folgende ein einzelner vollständig gültiger Block

# /// some-toml
# embedded-csharp = """
# /// <summary>
# /// text
# ///
# /// </summary>
# public class MyClass { }
# """
# ///

Eine Startzeile darf NICHT zwischen einer anderen Startzeile und ihrer Endzeile platziert werden. In solchen Fällen können Tools einen Fehler ausgeben. Ungeschlossene Blöcke müssen ignoriert werden.

Wenn mehrere Kommentarblöcke desselben TYPE definiert sind, müssen Tools einen Fehler ausgeben.

Tools, die eingebettete Metadaten lesen, können die Standard-Python-Kodierungsdeklaration berücksichtigen. Wenn sie dies nicht tun, müssen sie die Datei als UTF-8 verarbeiten.

Dies ist der kanonische reguläre Ausdruck, der zum Parsen der Metadaten verwendet werden KANN

(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$

Bei Diskrepanzen zwischen der Textspezifikation und dem regulären Ausdruck hat die Textspezifikation Vorrang.

Tools dürfen NICHT aus Metadatenblöcken mit Typen lesen, die von diesem PEP oder zukünftigen nicht standardisiert wurden.

Skripttyp

Der erste Typ von Metadatenblöcken wird script genannt und enthält Skriptmetadaten (Abhängigkeitsdaten und Tool-Konfiguration).

Dieses Dokument kann Top-Level-Felder dependencies und requires-python enthalten und kann optional eine [tool]-Tabelle enthalten.

Die [tool]-Tabelle kann von jedem Tool, Skript-Runner oder anderweitig, zur Konfiguration des Verhaltens verwendet werden. Sie hat die gleichen Semantik wie die Tool-Tabelle in pyproject.toml.

Die Top-Level-Felder sind

  • dependencies: Eine Liste von Zeichenketten, die die Laufzeitabhängigkeiten des Skripts angibt. Jeder Eintrag muss eine gültige PEP 508-Abhängigkeit sein.
  • requires-python: Eine Zeichenkette, die die Python-Version(en) angibt, mit denen das Skript kompatibel ist. Der Wert dieses Feldes muss ein gültiger Versionsspezifizierer sein.

Skript-Runner müssen einen Fehler ausgeben, wenn die angegebenen dependencies nicht bereitgestellt werden können. Skript-Runner sollten einen Fehler ausgeben, wenn keine Python-Version bereitgestellt werden kann, die requires-python erfüllt.

Beispiel

Das Folgende ist ein Beispiel für ein Skript mit eingebetteten Metadaten

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

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])

Referenzimplementierung

Das Folgende ist ein Beispiel dafür, wie die Metadaten unter Python 3.11 oder höher gelesen werden.

import re
import tomllib

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def read(script: str) -> dict | None:
    name = 'script'
    matches = list(
        filter(lambda m: m.group('type') == name, re.finditer(REGEX, script))
    )
    if len(matches) > 1:
        raise ValueError(f'Multiple {name} blocks found')
    elif len(matches) == 1:
        content = ''.join(
            line[2:] if line.startswith('# ') else line[1:]
            for line in matches[0].group('content').splitlines(keepends=True)
        )
        return tomllib.loads(content)
    else:
        return None

Oft bearbeiten Tools Abhängigkeiten wie Paketmanager oder Automatisierung zur Abhängigkeitsaktualisierung in CI. Das Folgende ist ein grobes Beispiel für die Modifikation des Inhalts mit der tomlkit- Bibliothek.

import re

import tomlkit

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def add(script: str, dependency: str) -> str:
    match = re.search(REGEX, script)
    content = ''.join(
        line[2:] if line.startswith('# ') else line[1:]
        for line in match.group('content').splitlines(keepends=True)
    )

    config = tomlkit.parse(content)
    config['dependencies'].append(dependency)
    new_content = ''.join(
        f'# {line}' if line.strip() else f'#{line}'
        for line in tomlkit.dumps(config).splitlines(keepends=True)
    )

    start, end = match.span('content')
    return script[:start] + new_content + script[end:]

Beachten Sie, dass dieses Beispiel eine Bibliothek verwendet, die TOML-Formatierung beibehält. Dies ist keine Anforderung für die Bearbeitung, sondern eher ein „nice-to-have“-Feature.

Das Folgende ist ein Beispiel dafür, wie ein Stream von beliebigen Metadatenblöcken gelesen wird.

import re
from typing import Iterator

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def stream(script: str) -> Iterator[tuple[str, str]]:
    for match in re.finditer(REGEX, script):
        yield match.group('type'), ''.join(
            line[2:] if line.startswith('# ') else line[1:]
            for line in match.group('content').splitlines(keepends=True)
        )

Abwärtskompatibilität

Zum Zeitpunkt der Erstellung scheint der Starter für Kommentarblöcke # /// script in keinem Python-Dateien auf GitHub vorzukommen. Daher besteht ein geringes Risiko, dass bestehende Skripte durch dieses PEP beschädigt werden.

Sicherheitsimplikationen

Wenn ein Skript mit eingebetteten Metadaten über ein Tool ausgeführt wird, das Abhängigkeiten automatisch installiert, könnte dies dazu führen, dass beliebiger Code in die Umgebung des Benutzers heruntergeladen und installiert wird.

Das Risiko ist hier 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 dieses PEP eingeführt wird, besteht darin, wenn ein nicht vertrauenswürdiges Skript mit eingebetteten Metadaten ausgeführt wird, wobei eine potenziell bösartige Abhängigkeit oder transitive Abhängigkeit installiert werden könnte.

Dieses Risiko wird durch die übliche gute Praxis des Überprüfens von Code vor der Ausführung behandelt. Darüber hinaus können Tools möglicherweise eine Sperrfunktionalität anbieten, um dieses Risiko zu mindern.

Wie man das lehrt

Um Metadaten in ein Skript einzubetten, definieren Sie einen Kommentarblock, der mit der Zeile # /// script beginnt und mit der Zeile # /// endet. Jede Zeile zwischen diesen beiden Zeilen muss ein Kommentar sein, und der vollständige Inhalt ergibt sich durch Entfernen der ersten beiden Zeichen.

# /// script
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# requires-python = ">=3.11"
# ///

Die zulässigen Felder werden in der folgenden Tabelle beschrieben

Feld. Description Tool-Verhalten
dependencies Eine Liste von Zeichenketten, die die Laufzeitabhängigkeiten des Skripts angibt. Jeder Eintrag muss eine gültige PEP 508-Abhängigkeit sein. Tools geben einen Fehler aus, wenn die angegebenen Abhängigkeiten nicht bereitgestellt werden können.
requires-python Eine Zeichenkette, die die Python-Version(en) angibt, mit denen das Skript kompatibel ist. Der Wert dieses Feldes muss ein gültiger Versionsspezifizierer sein. Tools geben möglicherweise einen Fehler aus, wenn keine Python-Version, die die Einschränkung erfüllt, ausgeführt werden kann.

Darüber hinaus ist eine [tool]-Tabelle erlaubt. Details darüber, was erlaubt ist, ähneln dem, was in pyproject.toml erlaubt ist, aber präzise Informationen müssen in der Dokumentation des jeweiligen Tools enthalten sein.

Es liegt an einzelnen Tools, ob ihr Verhalten auf Basis der eingebetteten Metadaten geändert wird oder nicht. Beispielsweise kann nicht jeder Skript-Runner eine Umgebung für bestimmte Python-Versionen bereitstellen, wie im Feld requires-python definiert.

Die Tool-Tabelle kann von jedem Tool, Skript-Runner oder anderweitig, zur Konfiguration des Verhaltens verwendet werden.

Empfehlungen

Tools, die die Verwaltung verschiedener Python-Versionen unterstützen, sollten versuchen, die höchste verfügbare Python-Version zu verwenden, die mit den requires-python-Metadaten des Skripts kompatibel ist, falls definiert.

Tool-Akzeptanz

Das Folgende ist eine Liste von Tools, die Unterstützung für dieses PEP geäußert haben oder sich zur Implementierung von Unterstützung verpflichtet haben, falls es angenommen wird

  • Pantsbuild und Pex: Haben Unterstützung für jede Methode zur Definition von Abhängigkeiten und auch für Funktionen geäußert, die dieses PEP als gültige Anwendungsfälle betrachtet, wie z. B. das Erstellen von Paketen aus Skripten und das Einbetten von Tool-Konfigurationen.
  • Mypy und Ruff: Haben starke Unterstützung für das Einbetten von Tool-Konfigurationen geäußert, da dies bestehende Probleme für Benutzer lösen würde.
  • Hatch: (Autor dieses PEP) Hat Unterstützung für alle Aspekte dieses PEP geäußert und wird eines der ersten Tools sein, das das Ausführen von Skripten mit spezifisch konfigurierten Python-Versionen unterstützt.

Abgelehnte Ideen

Warum kein Kommentarblock ähnlich wie requirements.txt verwenden?

Dieses PEP betrachtet verschiedene Arten von Benutzern, für die Python-Code als Single-File-Skripte existieren würde

  • Nicht-Programmierer, die Python nur als Skriptsprache für eine bestimmte Aufgabe verwenden. Diese Benutzer sind unwahrscheinlich mit Betriebssystemkonzepten wie Shebang-Zeilen oder der PATH-Umgebungsvariablen vertraut. Einige Beispiele
    • Die durchschnittliche Person, vielleicht am Arbeitsplatz, die ein Skript schreiben möchte, um etwas zur Effizienz zu automatisieren oder mühsame Arbeit zu reduzieren
    • Jemand, der im Bereich Data Science oder Machine Learning in Industrie oder Wissenschaft tätig ist und ein Skript schreiben möchte, um einige Daten zu analysieren oder für Forschungszwecke. Diese Benutzer sind besonders, da sie trotz begrenzter Programmierkenntnisse aus Quellen wie StackOverflow und Blogs mit Programmierungsschwerpunkt lernen und zunehmend Teil von Gemeinschaften sind, die Wissen und Code teilen. Daher wird eine nicht unerhebliche Anzahl dieser Benutzer mit Dingen wie Git(Hub), Jupyter, HuggingFace usw. vertraut sein.
  • Nicht-Programmierer, die Betriebssysteme verwalten, z. B. ein Sysadmin. Diese Benutzer können beispielsweise PATH einrichten, sind aber wahrscheinlich nicht mit Python-Konzepten wie virtuellen Umgebungen vertraut. Diese Benutzer arbeiten oft isoliert und haben wenig Bedarf, sich mit Werkzeugen zu beschäftigen, die für den Austausch gedacht sind, wie Git.
  • Programmierer, die Betriebssysteme/Infrastruktur verwalten, z. B. SREs. Diese Benutzer sind wahrscheinlich nicht sehr vertraut mit Python-Konzepten wie virtuellen Umgebungen, aber wahrscheinlich vertraut mit Git und verwenden es meistens, um alles zu versionieren, was für die Verwaltung von Infrastruktur wie Python-Skripten und Kubernetes-Konfigurationen erforderlich ist.
  • Programmierer, die Skripte hauptsächlich für sich selbst schreiben. Diese Benutzer sammeln im Laufe der Zeit eine große Anzahl von Skripten in verschiedenen Sprachen, die sie zur Automatisierung ihres Workflows verwenden und oft in einem einzigen Verzeichnis speichern, das möglicherweise zur Persistenz versioniert wird. Nicht-Windows-Benutzer richten möglicherweise jedes Python-Skript mit einer Shebang-Zeile ein, die auf die gewünschte Python-Ausführungsdatei oder den Skript-Runner verweist.

Dieses PEP argumentiert, dass das vorgeschlagene TOML-basierte Metadatenformat für jede Benutzerkategorie das beste ist und dass der Anforderung-ähnliche Kommentarblock nur für diejenigen zugänglich ist, die mit requirements.txt vertraut sind, was einer kleinen Teilmenge von Benutzern entspricht.

  • Für die durchschnittliche Person, die eine Aufgabe automatisiert, oder für den Datenwissenschaftler, der mit Nullkontext beginnt und wahrscheinlich weder mit TOML noch mit requirements.txt vertraut ist. Diese Benutzer werden höchstwahrscheinlich auf Snippets zurückgreifen, die online über eine Suchmaschine gefunden wurden, oder KI in Form eines Chatbots oder einer direkten Code-Vervollständigungssoftware nutzen. Die Ähnlichkeit mit den in pyproject.toml gespeicherten Abhängigkeitsinformationen liefert relativ schnell nützliche Suchergebnisse, und obwohl das Format pyproject.toml und das Skriptmetadatenformat nicht identisch sind, werden etwaige daraus resultierende Diskrepanzen für die beabsichtigten Benutzer wahrscheinlich nicht schwer zu lösen sein.

    Zusätzlich sind diese Benutzer am anfälligsten für Formatierungsfehler und Syntaxfehler. TOML ist ein gut definiertes Format mit vorhandenen Online-Validatoren, das Zuweisungen bietet, die mit Python-Ausdrücken kompatibel sind, und keine strengen Einrückungsregeln aufweist. Der Kommentarblock hingegen könnte leicht falsch formatiert werden, z. B. durch Vergessen des Doppelpunkts, und die Fehlersuche, warum er nicht funktioniert, wäre für einen solchen Benutzer schwierig.

  • Für die Sysadmin-Typen ist es ebenso unwahrscheinlich wie für die zuvor beschriebenen Benutzer, mit TOML oder requirements.txt vertraut zu sein. Für jedes Format müssten sie Dokumentation lesen. Sie wären wahrscheinlich mit TOML vertrauter, da sie strukturierte Datenformate gewohnt sind und weniger wahrgenommene Magie in ihren Systemen hätten.

    Darüber hinaus wäre /// script für die Wartung ihrer Systeme viel einfacher aus einer Shell zu durchsuchen als ein Kommentarblock mit potenziell zahlreichen Erweiterungen im Laufe der Zeit.

  • Für die SRE-Typen sind sie wahrscheinlich bereits mit TOML aus anderen Projekten vertraut, mit denen sie möglicherweise arbeiten müssen, wie z. B. der Konfiguration des GitLab Runners oder Cloud Native Buildpacks.

    Diese Benutzer sind für die Sicherheit ihrer Systeme verantwortlich und haben höchstwahrscheinlich Sicherheitsscanner eingerichtet, um automatisch PRs zu öffnen, um Abhängigkeitsversionen zu aktualisieren. Solche automatisierten Tools wie Dependabot hätten es viel einfacher, bestehende TOML-Bibliotheken zu verwenden, als ihren eigenen benutzerdefinierten Parser für ein Kommentarblockformat zu schreiben.

  • Für die Programmierertypen ist es wahrscheinlicher, dass sie mit TOML vertraut sind, als dass sie jemals eine requirements.txt-Datei gesehen haben, es sei denn, sie sind Python-Programmierer mit vorheriger Erfahrung im Schreiben von Anwendungen. Im Falle von Erfahrung mit dem Requirements-Format bedeutet dies zwangsläufig, dass sie zumindest einigermaßen mit dem Ökosystem vertraut sind und es daher sicher ist anzunehmen, dass sie wissen, was TOML ist.

    Ein weiterer Vorteil dieses PEPs für diese Benutzer ist, dass ihre IDEs wie Visual Studio Code viel einfacher TOML-Syntaxhervorhebung bieten können, als jeder benutzerdefinierte Logik für diese Funktion schreiben müsste.

Zusätzlich, da das ursprüngliche alternative Kommentarblockformat (doppeltes #) gegen die Empfehlung von PEP 8 verstieß und infolgedessen Linter und IDE-Autoformatierer, die die Empfehlung berücksichtigten, standardmäßig fehlerhaft waren, verwendet der endgültige Vorschlag Standardkommentare, die mit einem einzelnen #-Zeichen beginnen, ohne offensichtliche Start- oder Endsequenz.

Das Konzept von regulären Kommentaren, die nicht für Maschinen bestimmt zu sein scheinen (z. B. Kodierungsdeklarationen), die das Verhalten beeinflussen, wäre für Python-Benutzer unüblich und widerspricht direkt dem grundlegenden Prinzip „explizit ist besser als implizit“.

Benutzer, die etwas eingeben, das für sie wie Prosa aussieht, könnten das Laufzeitverhalten ändern. Dieses PEP vertritt die Ansicht, dass die Möglichkeit, dass dies geschieht, auch wenn ein Tool entsprechend eingerichtet wurde (vielleicht von einem Sysadmin), für Benutzer unfreundlich ist.

Schließlich und entscheidend ist, dass die Alternativen zu diesem PEP wie PEP 722 die hier aufgeführten Anwendungsfälle nicht erfüllen, wie z. B. die Angabe der unterstützten Python-Versionen, das endgültige Erstellen von Skripten zu Paketen und die Fähigkeit, Metadaten im Auftrag von Benutzern von Maschinen bearbeiten zu lassen. Es ist sehr wahrscheinlich, dass die Anfragen nach solchen Funktionen bestehen bleiben und denkbar, dass ein zukünftiges PEP die Einbettung solcher Metadaten ermöglichen würde. Zu diesem Zeitpunkt gäbe es mehrere Möglichkeiten, dasselbe zu erreichen, was gegen unser Grundprinzip verstößt: „Es sollte einen – und vorzugsweise nur einen – offensichtlichen Weg dafür geben.“

Warum keine mehrzeilige Zeichenkette verwenden?

Eine frühere Version dieses PEP schlug vor, die Metadaten wie folgt zu speichern

__pyproject__ = """
...
"""

Das signifikanteste Problem mit diesem Vorschlag ist, dass die eingebetteten TOML auf folgende Weise eingeschränkt wären

  • Es wäre nicht möglich, mehrzeilige doppelt geführte Zeichenketten im TOML zu verwenden, da dies mit der Python-Zeichenkette, die das Dokument enthält, in Konflikt stünde. Viele TOML-Schreiber erhalten den Stil nicht und könnten potenziell eine Ausgabe erzeugen, die fehlerhaft ist.
  • Die Art und Weise, wie Zeichen-Escaping in Python-Zeichenketten funktioniert, ist nicht ganz dieselbe wie in TOML-Zeichenketten. Es wäre möglich, eine Eins-zu-eins-Zeichenabbildung beizubehalten, indem Rohzeichenketten erzwungen werden, aber dieses r-Präfix kann für Benutzer potenziell verwirrend sein.

Warum nicht Kernmetadatenfelder wiederverwenden?

Eine frühere Version dieses PEP schlug vor, den bestehenden Metadatenstandard wiederzuverwenden, der zur Beschreibung von Projekten verwendet wird.

Es gibt zwei signifikante Probleme mit diesem Vorschlag

Warum nicht auf bestimmte Metadatenfelder beschränken?

Durch die Beschränkung der Metadaten nur auf dependencies würden wir den bekannten Anwendungsfall von Tools verhindern, die die Verwaltung von Python-Installationen unterstützen, was Benutzern ermöglichen würde, spezifische Python-Versionen für neue Syntax oder Standardbibliotheksfunktionalität anzuzielen.

Warum die Tool-Konfiguration nicht einschränken?

Durch die Nichtzulassung der [tool]-Tabelle würden wir bekannte Funktionalitäten verhindern, die Benutzern zugutekommen würden. Zum Beispiel

  • Ein Skript-Runner kann die Auflösung von Abhängigkeitsdaten für eine eingebettete Sperrdatei unterstützen (dies kann Go’s gorun tun).
  • Ein Skript-Runner kann eine Konfiguration unterstützen, die anweist, Skripte in Containern auszuführen, wenn keine plattformübergreifende Unterstützung für eine Abhängigkeit vorhanden ist oder wenn die Einrichtung für den Durchschnittsnutzer zu komplex ist, z. B. bei Anforderungen an Nvidia-Treiber. Situationen wie diese würden es Benutzern ermöglichen, mit dem fortzufahren, was sie tun möchten, während sie ansonsten an diesem Punkt aufhören würden.
  • Tools möchten möglicherweise mit Funktionen experimentieren, um den Entwicklungsaufwand für Benutzer zu verringern, wie z. B. das Erstellen von Single-File-Skripten zu Paketen. Wir erhielten Feedback, dass es bereits existierende Tools gibt, die Wheels und Quellverteilungen aus einzelnen Dateien erstellen.

    Der Autor des Rust-RFC für das Einbetten von Metadaten erwähnte uns, dass sie dies ebenfalls aufgrund von Benutzerfeedback aktiv prüfen, das besagt, dass es unnötige Reibung bei der Verwaltung kleiner Projekte gibt.

    Es gab eine Zusage zur Unterstützung dieses PEPs durch mindestens ein großes Build-System.

Warum das Tool-Verhalten nicht einschränken?

Eine frühere Version dieses PEP schlug vor, dass Tools, die keine Skripte ausführen, ihr Verhalten NICHT ändern sollten, wenn das Skript nicht der einzige Input für das Tool ist. Wenn beispielsweise ein Linter mit dem Pfad zu einem Verzeichnis aufgerufen wird, sollte er sich genauso verhalten, als ob null Dateien eingebettete Metadaten hätten.

Dies geschah vorsorglich, um Verwirrung im Tool-Verhalten zu vermeiden und verschiedene Funktionsanfragen für Tools zur Unterstützung dieses PEPs zu generieren. Während der Diskussion erhielten wir jedoch Feedback von Tool-Maintainern, dass dies unerwünscht und potenziell verwirrend für Benutzer wäre. Darüber hinaus könnte dies eine universell einfachere Möglichkeit zur Konfiguration von Tools in bestimmten Situationen ermöglichen und bestehende Probleme lösen.

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

Auch hier ist ein zentrales Problem, dass die Zielgruppe dieses Vorschlags Leute sind, die Skripte schreiben, die nicht zur Verteilung gedacht sind. Manchmal werden Skripte „geteilt“, aber das ist viel informeller als „Verteilung“ - es beinhaltet normalerweise das Senden eines Skripts per E-Mail mit schriftlichen Anweisungen zur Ausführung oder das Weitergeben eines Links zu einem GitHub-Gist.

Von solchen Benutzern zu erwarten, dass sie die Komplexität des Python-Paketierens lernen, ist ein erheblicher Anstieg 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 für die Ausführung von Skripten vor Ort konzipiert wird, ist dies 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 verteilt werden, gelöst ist. Und selbst dann löst es nicht den Anwendungsfall des „Senden eines Skripts in einem Gist oder per E-Mail“.

Warum die Anforderungen nicht aus Importanweisungen ableiten?

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

Dies ist jedoch aus mehreren Gründen nicht durchführbar. Erstens gelten die oben genannten Punkte zur Notwendigkeit, die Syntax leicht parsierbar zu halten, auch für Tools, die in anderen Sprachen geschrieben sind, gleichermaßen hier.

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, könnte derselbe Importname mehreren Paketen auf PyPI entsprechen. Man könnte einwenden, dass die Auflösung, welches Paket gewünscht ist, nur dann erforderlich wäre, wenn mehrere Projekte denselben Importnamen bereitstellen. Dies würde es jedoch jedem leicht machen, funktionierende Skripte unbeabsichtigt oder böswillig zu brechen, indem ein Paket auf PyPI hochgeladen wird, das einen Importnamen hat, der demselben vorhandenen Projekt entspricht. Die Alternative, bei der unter den Kandidaten das zuerst im Index registrierte Paket ausgewä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 vorhandene Paket Malware ist, das absichtlich mit einem ausreichend generischen Importnamen hochgeladen wurde, der mit hoher Wahrscheinlichkeit wiederverwendet wird.

Eine verwandte Idee wäre, die Anforderungen als Kommentare zu den 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 Parsingschwierigkeiten. Außerdem ist die Platzierung des Kommentars bei mehrzeiligen Importen 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 in allen Situationen nicht wie intuitiv erwartet funktionieren. 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, es zu schreiben, wäre requires: pywin32 ; sys_platform == 'win32').

(Danke an Jean Abou-Samra für die klare Erläuterung dieses Punktes)

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

Die Anforderungen in eine Requirements-Datei zu schreiben, erfordert kein PEP. Das können Sie jetzt schon tun, und es ist sogar 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. Darüber hinaus ist das Requirements-Dateiformat pip-spezifisch, so dass Tools, die es verwenden, von einem pip-Implementierungsdetail abhängig sind.

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

  1. Ein standardisierter Ersatz für das Requirements-Dateiformat.
  2. Ein Standard dafür, wie die Requirements-Datei für ein gegebenes Skript zu lokalisieren ist.

Der erste Punkt ist ein bedeutendes Unterfangen. Er wurde mehrmals diskutiert, aber bisher hat sich niemand bemüht, ihn tatsächlich umzusetzen. Der wahrscheinlichste Ansatz wäre, dass Standards für einzelne Anwendungsfälle entwickelt werden, die derzeit mit Requirements-Dateien adressiert werden. Eine Option wäre hier, dass dieses PEP einfach ein neues Dateiformat definiert, das lediglich eine Textdatei ist und PEP 508-Anforderungen enthält, eine pro Zeile. Dies würde nur die Frage offen lassen, wie diese Datei zu finden ist.

Die „offensichtliche“ Lösung wäre, die Datei genauso zu benennen wie das Skript, aber mit einer .reqs-Erweiterung (oder etwas Ähnlichem). Dies erfordert jedoch immer noch *zwei* Dateien, während derzeit nur eine Datei benötigt wird, und passt daher nicht zum Modell „bessere Batch-Datei“ (Shell-Skripte und Batch-Dateien sind typischerweise in sich abgeschlossen). Es erfordert vom Entwickler, sich daran zu erinnern, die beiden Dateien zusammenzuhalten, und dies ist möglicherweise nicht immer möglich. Beispielsweise können Richtlinien für die Systemadministration vorschreiben, dass *alle* Dateien in einem bestimmten Verzeichnis ausführbar sein müssen (die Standards des Linux-Dateisystems schreiben dies für /usr/bin vor, zum Beispiel). Und einige Methoden zum Teilen eines Skripts (z. B. das Veröffentlichen auf einer Textdatei-Sharing-Dienst wie Githubs Gist oder ein Firmenintranet) erlauben möglicherweise nicht die Ableitung des Speicherorts einer zugehörigen Requirements-Datei vom Speicherort des Skripts (Tools wie pipx unterstützen das Ausführen eines Skripts direkt von einer URL, daher ist „Herunterladen und Entpacken eines Zip-Archivs des Skripts und seiner Abhängigkeiten“ möglicherweise keine geeignete Anforderung).

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

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

Dies würde typischerweise die Speicherung von Metadaten als mehrere spezielle Variablen beinhalten, wie die folgenden.

__requires_python__ = ">=3.11"
__dependencies__ = [
    "requests",
    "click",
]

Das signifikanteste Problem mit diesem Vorschlag ist, dass alle Konsumenten der Abhängigkeitsdaten einen Python-Parser implementieren müssen. Selbst wenn die Syntax eingeschränkt ist, wird der *Rest* des Skripts die vollständige 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.

Darüber hinaus ändert sich die Python-Syntax in jeder Version. Wenn die Extraktion 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 nicht tragbar.

Bei diesem Ansatz besteht die Gefahr, dass Skripte mit vielen Variablen überladen werden, wenn neue Erweiterungen hinzugefügt werden. Darüber hinaus würde die Intuitivität, welche Metadatenfelder welchen Variablennamen entsprechen, zu Verwirrung bei den Benutzern führen.

Es ist jedoch erwähnenswert, dass das Dienstprogramm pip-run (eine erweiterte Form) dieses Ansatzes implementiert. Weitere Diskussion über das Design von pip-run ist im Issue-Tracker des Projekts verfügbar.

Was ist mit lokalen Abhängigkeiten?

Diese können ohne speziellen Metadaten und Werkzeuge gehandhabt werden, indem einfach der Speicherort der Abhängigkeiten zu sys.path hinzugefügt wird. Dieses PEP ist für diesen Fall einfach nicht notwendig. Wenn andererseits die "lokalen Abhängigkeiten" tatsächliche Distributionen sind, die lokal veröffentlicht werden, können sie wie gewohnt mit einer PEP 508-Anforderung spezifiziert werden, und der lokale Paketindex wird bei der Ausführung eines Werkzeugs über die Benutzeroberfläche des Werkzeugs dafür angegeben.

Offene Fragen

Derzeit keine.

Fußnoten


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

Zuletzt geändert: 2025-08-08 15:00:59 GMT