PEP 702 – Markierung von Deprekationen mittels Typsystem
- Autor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 30-Dez-2022
- Python-Version:
- 3.13
- Post-History:
- 01-Jan-2023, 22-Jan-2023
- Resolution:
- 07-Nov-2023
Zusammenfassung
Diese PEP fügt einen Dekorator @warnings.deprecated() hinzu, der eine Klasse oder Funktion als veraltet markiert und statische Prüfer in die Lage versetzt, bei ihrer Verwendung Warnungen auszugeben. Standardmäßig löst dieser Dekorator auch eine Laufzeit-DeprecationWarning aus.
Motivation
Während sich Software weiterentwickelt, werden neue Funktionalitäten hinzugefügt und alte werden obsolet. Bibliotheksentwickler möchten darauf hinarbeiten, veralteten Code zu entfernen, während sie ihren Benutzern Zeit geben, auf neue APIs umzusteigen. Python bietet einen Mechanismus, um diese Ziele zu erreichen: die Warnungsklasse DeprecationWarning, die verwendet wird, um Warnungen auszugeben, wenn veraltete Funktionalität verwendet wird. Dieser Mechanismus wird weit verbreitet genutzt: zum Zeitpunkt der Erstellung dieser PEP enthält der Hauptzweig von CPython etwa 150 verschiedene Code-Pfade, die DeprecationWarning auslösen. Viele Drittanbieter-Bibliotheken verwenden ebenfalls DeprecationWarning, um Deprekationen zu markieren. In den Top 5000 PyPI-Paketen gibt es
- 1911 Treffer für den regulären Ausdruck
warnings\.warn.*\bDeprecationWarning\b, was auf die Verwendung vonDeprecationWarninghinweist (nicht eingeschlossen sind Fälle, in denen die Warnung über mehrere Zeilen aufgeteilt ist). - 1661 Treffer für den regulären Ausdruck
^\s*@deprecated, was auf die Verwendung einer Art von Deprekations-Dekorator hinweist.
Der aktuelle Mechanismus ist jedoch oft nicht ausreichend, um sicherzustellen, dass Benutzer veralteter Funktionalität ihren Code rechtzeitig aktualisieren. Beispielsweise musste die Entfernung verschiedener lange veralteter unittest-Funktionen aus Python 3.11 zurückgenommen werden, um den Benutzern mehr Zeit für die Aktualisierung ihres Codes zu geben. Benutzer können ihre Testsuite aus praktischen Gründen mit deaktivierten Warnungen ausführen, oder Deprekationen können in Code-Pfaden ausgelöst werden, die nicht von Tests abgedeckt sind.
Die Bereitstellung weiterer Möglichkeiten für Benutzer, veraltete Funktionalitäten zu finden, kann den Migrationsprozess beschleunigen. Diese PEP schlägt vor, statische Typ-Prüfer zu nutzen, um Deprekationen an Benutzer zu kommunizieren. Solche Prüfer verfügen über ein gründliches semantisches Verständnis des Benutzercodes, wodurch sie Deprekationen erkennen und melden können, die eine einzelne grep-Abfrage nicht finden könnte. Darüber hinaus integrieren sich viele Typ-Prüfer in IDEs, wodurch Benutzer Deprekationswarnungen direkt in ihren Editoren sehen können.
Begründung
Auf den ersten Blick scheinen Deprekationen kein Thema zu sein, das Typ-Prüfer berühren sollten. Schließlich geht es bei Typ-Prüfern darum zu prüfen, ob Code so funktioniert, wie er ist, und nicht um potenzielle zukünftige Änderungen. Die Analyse, die Typ-Prüfer auf Code durchführen, um Typfehler zu finden, ist jedoch der Analyse sehr ähnlich, die erforderlich wäre, um die Verwendung vieler Deprekationen zu erkennen. Daher sind Typ-Prüfer gut positioniert, um Deprekationen zu finden und zu melden.
Andere Sprachen verfügen bereits über ähnliche Funktionalitäten
- GCC unterstützt ein
deprecatedAttribut für Funktionsdeklarationen. Dies treibt CPythonsPy_DEPRECATEDMakro an. - GraphQL unterstützt die Markierung von Feldern als
@deprecated. - Kotlin unterstützt eine
DeprecatedAnnotation. - Scala unterstützt eine
@deprecatedAnnotation. - Swift unterstützt die Verwendung des
@availableAttributs, um APIs als veraltet zu markieren. - TypeScript verwendet das JSDoc-Tag
@deprecated, um einen Hinweis zur Markierung der Verwendung veralteter Funktionalität auszugeben.
Mehrere Benutzer haben die Unterstützung für eine solche Funktion gefordert
Es gibt ähnliche bestehende Tools von Drittanbietern
- Deprecated bietet einen Dekorator zur Markierung von Klassen, Funktionen oder Methoden als veraltet. Der Zugriff auf dekorierte Objekte löst eine Laufzeitwarnung aus, wird aber von Typ-Prüfern nicht erkannt.
- flake8-deprecated ist ein Linter-Plugin, das vor der Verwendung veralteter Funktionen warnt. Es ist jedoch auf eine kurze, fest codierte Liste von Deprekationen beschränkt.
Spezifikation
Ein neuer Dekorator @deprecated() wird dem Modul warnings hinzugefügt. Dieser Dekorator kann auf eine Klasse, Funktion oder Methode angewendet werden, um sie als veraltet zu markieren. Dies schließt typing.TypedDict und typing.NamedTuple Definitionen ein. Bei überladenen Funktionen kann der Dekorator auf einzelne Überladungen angewendet werden, was darauf hinweist, dass die jeweilige Überladung veraltet ist. Der Dekorator kann auch auf die Implementierungsfunktion der Überladung angewendet werden, was darauf hinweist, dass die gesamte Funktion veraltet ist.
Der Dekorator nimmt die folgenden Argumente entgegen:
- Ein erforderliches positionsgebundenes Argument, das die Deprekationsnachricht darstellt.
- Zwei schlüsselwortgebundene Argumente,
categoryundstacklevel, die das Laufzeitverhalten steuern (siehe unten unter "Laufzeitverhalten").
Das positionsgebundene Argument ist vom Typ str und enthält eine Nachricht, die vom Typ-Prüfer angezeigt werden sollte, wenn er auf eine Verwendung des dekorierten Objekts stößt. Tools können die Deprekationsnachricht zur Anzeige bereinigen, z. B. durch Verwendung von inspect.cleandoc() oder äquivalenter Logik. Die Nachricht muss ein Zeichenkettenliteral sein. Der Inhalt von Deprekationsnachrichten liegt im Ermessen des Benutzers, kann aber die Version, in der das veraltete Objekt entfernt werden soll, und Informationen über vorgeschlagene Ersatz-APIs enthalten.
Typ-Prüfer sollten eine Diagnose erstellen, wann immer sie auf eine Verwendung eines als veraltet markierten Objekts stoßen. Bei veralteten Überladungen umfasst dies alle Aufrufe, die auf die veraltete Überladung aufgelöst werden. Für veraltete Klassen und Funktionen umfasst dies:
- Referenzen über Modul-, Klassen- oder Instanzattribute (
module.deprecated_object,module.SomeClass.deprecated_method,module.SomeClass().deprecated_method) - Jede Verwendung von veralteten Objekten in ihrem definierenden Modul (
x = deprecated_object()inmodule.py) - Wenn
import *verwendet wird, die Verwendung von veralteten Objekten aus dem Modul (from module import *; x = deprecated_object()) - Importe mit
from(from module import deprecated_object) - Jede Syntax, die indirekt einen Aufruf der Funktion auslöst. Wenn beispielsweise die Methode
__add__einer KlasseCveraltet ist, sollte der CodeC() + C()eine Diagnose auslösen. Ebenso sollte, wenn der Setter einer Eigenschaft als veraltet markiert ist, der Versuch, die Eigenschaft zu setzen, eine Diagnose auslösen.
Wenn eine Methode mit dem Dekorator typing.override() aus PEP 698 markiert ist und die Basisklassenmethode, die sie überschreibt, veraltet ist, sollte der Typ-Prüfer eine Diagnose ausgeben.
Es gibt zusätzliche Szenarien, in denen Deprekationen eine Rolle spielen könnten. Zum Beispiel kann ein Objekt ein typing.Protocol implementieren, aber eine der für die Protokollerfüllung erforderlichen Methoden ist veraltet. Da Szenarien wie dieses komplex und in der Praxis relativ unwahrscheinlich erscheinen, schreibt diese PEP nicht vor, dass Typ-Prüfer sie erkennen.
Beispiel
Als Beispiel betrachten wir diese Bibliotheksstub-Datei namens library.pyi
from warnings import deprecated
@deprecated("Use Spam instead")
class Ham: ...
@deprecated("It is pining for the fiords")
def norwegian_blue(x: int) -> int: ...
@overload
@deprecated("Only str will be allowed")
def foo(x: int) -> str: ...
@overload
def foo(x: str) -> str: ...
class Spam:
@deprecated("There is enough spam in the world")
def __add__(self, other: object) -> object: ...
@property
@deprecated("All spam will be equally greasy")
def greasy(self) -> float: ...
@property
def shape(self) -> str: ...
@shape.setter
@deprecated("Shapes are becoming immutable")
def shape(self, value: str) -> None: ...
So sollten Typ-Prüfer die Verwendung dieser Bibliothek behandeln
from library import Ham # error: Use of deprecated class Ham. Use Spam instead.
import library
library.norwegian_blue(1) # error: Use of deprecated function norwegian_blue. It is pining for the fiords.
map(library.norwegian_blue, [1, 2, 3]) # error: Use of deprecated function norwegian_blue. It is pining for the fiords.
library.foo(1) # error: Use of deprecated overload for foo. Only str will be allowed.
library.foo("x") # no error
ham = Ham() # no error (already reported above)
spam = library.Spam()
spam + 1 # error: Use of deprecated method Spam.__add__. There is enough spam in the world.
spam.greasy # error: Use of deprecated property Spam.greasy. All spam will be equally greasy.
spam.shape # no error
spam.shape = "cube" # error: Use of deprecated property setter Spam.shape. Shapes are becoming immutable.
Die genaue Formulierung der Diagnosen liegt beim Typ-Prüfer und ist nicht Teil der Spezifikation.
Laufzeitverhalten
Zusätzlich zum positionsgebundenen Argument message nimmt der Dekorator @deprecated zwei schlüsselwortgebundene Argumente entgegen:
category: Eine Warnungsklasse. StandardmäßigDeprecationWarning. Wenn dies aufNonegesetzt wird, wird zur Laufzeit keine Warnung ausgegeben und der Dekorator gibt das ursprüngliche Objekt zurück, mit Ausnahme der Einstellung des Attributs__deprecated__(siehe unten).stacklevel: Die Anzahl der Stack-Frames, die beim Ausgeben der Warnung übersprungen werden sollen. Standardmäßig 1, was bedeutet, dass die Warnung an der Stelle ausgegeben werden soll, an der das veraltete Objekt aufgerufen wird. Intern fügt die Implementierung die Anzahl der Stack-Frames hinzu, die sie im Wrapper-Code verwendet.
Wenn das dekorierte Objekt eine Klasse ist, wickelt der Dekorator die Methode __new__ so ab, dass die Instanziierung der Klasse eine Warnung auslöst. Wenn das dekorierte Objekt aufrufbar ist, gibt der Dekorator ein neues aufrufbares Objekt zurück, das das ursprüngliche aufrufbare Objekt umschließt, aber eine Warnung auslöst, wenn es aufgerufen wird. Andernfalls löst der Dekorator einen TypeError aus (es sei denn, category=None wird übergeben).
Es gibt mehrere Szenarien, in denen die Verwendung des dekorierten Objekts keine Warnung ausgeben kann, darunter Überladungen, Protocol-Klassen und abstrakte Methoden. Typ-Prüfer können eine Warnung ausgeben, wenn @deprecated ohne category=None verwendet wird.
Zur Unterstützung der Laufzeit-Introspektion setzt der Dekorator ein Attribut __deprecated__ auf das übergebene Objekt sowie auf die von ihm generierten Wrapper-Funktionen für veraltete Klassen und Funktionen. Der Wert des Attributs ist die an den Dekorator übergebene Nachricht. Das Dekorieren von Objekten, bei denen dieses Attribut nicht gesetzt werden kann, wird nicht unterstützt.
Wenn ein Protocol mit dem Dekorator @runtime_checkable als veraltet markiert ist, sollte das Attribut __deprecated__ nicht als Mitglied des Protokolls betrachtet werden, sodass seine Anwesenheit isinstance-Prüfungen nicht beeinträchtigt.
Zur Kompatibilität mit typing.get_overloads() sollte der Dekorator @deprecated nach dem Dekorator @overload platziert werden.
Verhalten von Typ-Prüfern
Diese PEP spezifiziert nicht genau, wie Typ-Prüfer Deprekationsdiagnosen ihren Benutzern präsentieren sollen. Einige Benutzer (z. B. Anwendungsentwickler, die nur auf eine bestimmte Python-Version abzielen) interessieren sich möglicherweise nicht für Deprekationen, während andere (z. B. Bibliotheksentwickler, die möchten, dass ihre Bibliothek mit zukünftigen Python-Versionen kompatibel bleibt) jede Verwendung veralteter Funktionalität in ihrer CI-Pipeline erfassen möchten. Daher wird empfohlen, dass Typ-Prüfer Konfigurationsoptionen anbieten, die beide Anwendungsfälle abdecken. Wie bei jeder anderen Typ-Prüfer-Fehlermeldung ist es auch möglich, Deprekationen mithilfe von # type: ignore Kommentaren zu ignorieren.
Deprekationsrichtlinie
Wir schlagen vor, dass die Deprekationsrichtlinie von CPython (PEP 387) aktualisiert wird, um zu verlangen, dass neue Deprekationen die Funktionalität dieser PEP verwenden, um Benutzer auf die Deprekation aufmerksam zu machen, wenn möglich. Konkret bedeutet dies, dass neue Deprekationen von einer Änderung im typeshed-Repository begleitet werden sollten, um den Dekorator @deprecated an der entsprechenden Stelle hinzuzufügen. Diese Anforderung gilt nicht für Deprekationen, die sich nicht mit der Funktionalität dieser PEP ausdrücken lassen.
Abwärtskompatibilität
Das Erstellen eines neuen Dekorators birgt keine Probleme mit der Abwärtskompatibilität. Wie bei allen neuen Typ-Funktionen wird der Dekorator @deprecated dem Modul typing_extensions hinzugefügt, wodurch er in älteren Python-Versionen verwendet werden kann.
Wie man dies lehrt
Für Benutzer, die Deprekationswarnungen in ihrer IDE oder in der Ausgabe ihres Typ-Prüfers erhalten, sollten die erhaltenen Meldungen klar und selbsterklärend sein. Die Verwendung des Dekorators @deprecated wird eine fortgeschrittene Funktion sein, die hauptsächlich für Bibliotheksautoren relevant ist. Der Dekorator sollte in der einschlägigen Dokumentation (z. B. PEP 387 und die Dokumentation zu DeprecationWarning) als zusätzliche Möglichkeit zur Kennzeichnung veralteter Funktionalität erwähnt werden.
Referenzimplementierung
Eine Laufzeitimplementierung des Dekorators @deprecated ist in der Bibliothek typing-extensions ab Version 4.5.0 verfügbar. Der Typ-Prüfer pyanalyze hat prototypische Unterstützung für die Ausgabe von Deprekationsfehlern, ebenso wie Pyright.
Abgelehnte Ideen
Deprekation von Modulen und Attributen
Diese PEP behandelt Deprekationen von Klassen, Funktionen und Überladungen. Dies ermöglicht es Typ-Prüfern, viele, aber nicht alle möglichen Deprekationen zu erkennen. Um zu bewerten, ob zusätzliche Funktionalität sinnvoll wäre, habe ich alle aktuellen Deprekationen in der CPython-Standardbibliothek untersucht.
Ich habe festgestellt:
- 74 Deprekationen von Funktionen, Methoden und Klassen (von dieser PEP unterstützt)
- 28 Deprekationen ganzer Module (hauptsächlich aufgrund von PEP 594)
- 9 Deprekationen von Funktionsparametern (von dieser PEP durch Dekorieren von Überladungen unterstützt)
- 1 Deprekation einer Konstanten
- 38 Deprekationen, die im Typsystem nicht leicht erkennbar sind (z. B. das Aufrufen von
asyncio.get_event_loop()ohne eine aktive Ereignisschleife)
Module könnten durch Hinzufügen einer Modul-weiten Konstanten __deprecated__ als veraltet markiert werden. Die Notwendigkeit dafür ist jedoch begrenzt und es ist relativ einfach, die Verwendung von veralteten Modulen durch Grepping zu erkennen. Daher schließt diese PEP die Unterstützung für ganze Modul-Deprekationen aus. Als Workaround könnten Benutzer alle Modul-weiten Klassen und Funktionen mit @deprecated markieren.
Zum Deprekations-Markieren von Modul-weiten Konstanten, Objektattributen und Funktionsparametern könnte ein Deprecated[type, message] Typ-Modifikator, ähnlich wie Annotated, hinzugefügt werden. Dies würde jedoch einen neuen Ort im Typsystem schaffen, an dem Zeichenketten einfach Zeichenketten und keine Forward-Referenzen sind, was die Implementierung von Typ-Prüfern verkompliziert. Darüber hinaus zeigen meine Daten, dass diese Funktion nicht häufig benötigt wird.
Funktionen zur Deprekation weiterer Objektarten könnten in einer zukünftigen PEP hinzugefügt werden.
Platzierung des Dekorators im Modul typing
Eine frühere Version dieser PEP schlug vor, den Dekorator @deprecated im Modul typing zu platzieren. Es gab jedoch Feedback, dass es unerwartet wäre, wenn ein Dekorator im Modul typing Laufzeitverhalten hätte. Daher schlägt die PEP nun vor, den Dekorator stattdessen dem Modul warnings hinzuzufügen.
Danksagungen
Ein Treffen mit der typing-sig Meetup-Gruppe führte zu hilfreichem Feedback zu diesem Vorschlag.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0702.rst
Zuletzt geändert: 2024-10-16 16:05:18 GMT