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

Python Enhancement Proposals

PEP 386 – Ändern des Versionsvergleichsmoduls in Distutils

Autor:
Tarek Ziadé <tarek at ziade.org>
Status:
Abgelöst
Typ:
Standards Track
Thema:
Packaging
Erstellt:
04-Jun-2009
Ersetzt-Durch:
440

Inhaltsverzeichnis

Zusammenfassung

Hinweis: Dieses PEP wurde durch das in PEP 440 definierte Schema zur Versionsidentifikation und Abhängigkeitsspezifikation abgelöst.

Dieses PEP schlug ein neues System für den Versionsvergleich in Distutils vor.

Motivation

In Python gibt es noch keine wirklichen Einschränkungen, wie ein Projekt seine Versionen verwalten und inkrementieren soll.

Distutils stellt ein Metadatenfeld für die Distribution namens version bereit, das jedoch freiformatig ist. Aktuelle Benutzer, wie z.B. PyPI, betrachten in der Regel die zuletzt hochgeladene Version als die latest, unabhängig von der erwarteten Semantik.

Distutils wird seine Fähigkeiten bald erweitern, um Distributionen die Möglichkeit zu geben, Abhängigkeiten von anderen Distributionen über das Metadatenfeld Requires-Dist auszudrücken (siehe PEP 345). Optional wird die Verwendung dieses Feldes gestattet, um die Abhängigkeit auf einen Satz kompatibler Versionen zu beschränken. Beachten Sie, dass dieses Feld Requires ersetzt, das Abhängigkeiten von Modulen und Paketen ausdrückte.

Das Feld Requires-Dist ermöglicht es einer Distribution, eine Abhängigkeit von einem anderen Paket zu definieren und diese Abhängigkeit optional auf einen Satz kompatibler Versionen zu beschränken. Man kann also schreiben:

Requires-Dist: zope.interface (>3.5.0)

Dies bedeutet, dass die Distribution zope.interface mit einer Version größer als 3.5.0 benötigt.

Dies bedeutet auch, dass Python-Projekte dieselbe Konvention wie das Tool, mit dem sie installiert werden, befolgen müssen, damit sie Versionen vergleichen können.

Aus diesem Grund schlägt dieses PEP zur Verbesserung der Interoperabilität ein Standardschema zur Darstellung von Versionsinformationen und deren Vergleichssemantik vor.

Darüber hinaus wird dies die Arbeit von OS-Paketierern bei der Neuerstellung von standardkonformen Distributionen erleichtern, da es derzeit schwierig sein kann zu entscheiden, wie zwei Distributionsversionen verglichen werden.

Voraussetzungen und aktueller Status

Es liegt nicht im Geltungsbereich dieses PEPs, ein universelles Versionierungsschema bereitzustellen, das alle oder auch nur die meisten bestehenden Versionierungsschemata unterstützt. Es wird immer konkurrierende Grammatiken geben, entweder durch Distro- oder Projektrichtlinien oder aus historischen Gründen, von denen wir nicht erwarten können, dass sie sich ändern.

Das vorgeschlagene Schema sollte in der Lage sein, die übliche Versionierungssemantik auszudrücken, sodass es möglich ist, jedes alternative Versionierungsschema zu parsen und in ein konformes zu transformieren. So gehen OS-Paketierer normalerweise mit den bestehenden Versionsschemata um, und dies ist eine bevorzugte Alternative gegenüber der Unterstützung einer beliebigen Anzahl von Versionierungsschemata.

Konformität mit üblicher Praxis und Konventionen sowie Einfachheit sind ein Pluspunkt, um eine reibungslose Einführung und einen schmerzfreien Übergang zu ermöglichen. Praktikabilität schlägt manchmal Reinheit.

Projekte haben sehr unterschiedliche Versionierungsbedürfnisse, aber die folgenden sind weitgehend als wichtige Semantik anerkannt:

  1. Es sollte möglich sein, mehr als eine Versionsebene auszudrücken (üblicherweise wird dies als Haupt- und Nebenrevision und manchmal auch als Mikrorevision ausgedrückt).
  2. Eine beträchtliche Anzahl von Projekten benötigt spezielle Bedeutungen für Versionen von „Vorabversionen“ (wie „alpha“, „beta“, „rc“) und diese haben weit verbreitete Aliase („a“ steht für „alpha“, „b“ für „beta“ und „c“ für „rc“). Und diese Vorabversionen machen es unmöglich, eine einfache alphanumerische Sortierung der Versionszeichenfolgenkomponenten zu verwenden. (Beispiel: 3.1a1 < 3.1)
  3. Einige Projekte benötigen auch „Nachveröffentlichungen“ von regulären Versionen, hauptsächlich für Installationsarbeiten, die anderweitig nicht klar ausgedrückt werden können.
  4. Entwicklungsversionen ermöglichen es Paketierern von unveröffentlichten Arbeiten, Versionskonflikte mit späteren regulären Veröffentlichungen zu vermeiden.

Für Leute, die weiter gehen und ein Tool zur Verwaltung ihrer Versionsnummern verwenden möchten, sind die beiden wichtigsten:

  • Das aktuelle Distutils-System [1]
  • Setuptools [2]

Distutils

Distutils bietet derzeit die Klassen StrictVersion und LooseVersion, die zur Verwaltung von Versionen verwendet werden können.

Die Klasse LooseVersion ist ziemlich lax. Laut Distutils-Dokumentation:

Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above.  A version number consists of a series of numbers,
separated by either periods or strings of letters.  When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically.  The following
are all valid version numbers, in no particular order:

    1.5.1
    1.5.2b2
    161
    3.10a
    8.02
    3.4j
    1996.07.12
    3.2.pl0
    3.1.1.6
    2g6
    11g
    0.960923
    2.2beta29
    1.13++
    5.5.kw
    2.0b1pl0

In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").

Diese Klasse macht jede Versionszeichenfolge gültig und bietet einen Algorithmus, um sie numerisch und dann lexikalisch zu sortieren. Das bedeutet, dass alles zur Versionierung Ihres Projekts verwendet werden kann.

>>> from distutils.version import LooseVersion as V
>>> v1 = V('FunkyVersion')
>>> v2 = V('GroovieVersion')
>>> v1 > v2
False

Das Problem dabei ist, dass sie zwar jede Verschachtelungsebene ausdrücken kann, aber keine spezielle Bedeutung für Versionen (Vorab- und Nachveröffentlichungen sowie Entwicklungsversionen) zulässt, wie in den Anforderungen 2, 3 und 4 dargelegt.

Die Klasse StrictVersion ist strenger. Laut Dokumentation:

Version numbering for meticulous retentive and software idealists.
Implements the standard interface for version number classes as
described above.  A version number consists of two or three
dot-separated numeric components, with an optional "pre-release" tag
on the end.  The pre-release tag consists of the letter 'a' or 'b'
followed by a number.  If the numeric components of two version
numbers are equal, then one with a pre-release tag will always
be deemed earlier (lesser) than one without.

The following are valid version numbers (shown in the order that
would be obtained by sorting according to the supplied cmp function):

    0.4       0.4.0  (these two are equivalent)
    0.4.1
    0.5a1
    0.5b3
    0.5
    0.9.6
    1.0
    1.0.4a3
    1.0.4b1
    1.0.4

The following are examples of invalid version numbers:

    1
    2.7.2.2
    1.3.a4
    1.3pl1
    1.3c4

Diese Klasse erzwingt einige Regeln und ist ein brauchbares Werkzeug für die Arbeit mit Versionsnummern.

>>> from distutils.version import StrictVersion as V
>>> v2 = V('GroovieVersion')
Traceback (most recent call last):
...
ValueError: invalid version number 'GroovieVersion'
>>> v2 = V('1.1')
>>> v3 = V('1.3')
>>> v2 < v3
True

Sie fügt Vorabversionen und einige Struktur hinzu, aber es fehlen einige semantische Elemente, um sie nutzbar zu machen, wie z.B. Entwicklungsveröffentlichungen oder Nachveröffentlichung-Tags, wie in den Anforderungen 3 und 4 dargelegt.

Beachten Sie auch, dass die Distutils-Version-Klassen seit Jahren existieren, aber in der Community nicht wirklich verwendet werden.

Setuptools

Setuptools bietet ein weiteres Versionsvergleichstool [3], das keine Regeln für die Version erzwingt, aber versucht, einen besseren Algorithmus zum Konvertieren von Zeichenfolgen in sortierbare Schlüssel bereitzustellen, mit einer Funktion namens parse_version.

Laut Dokumentation:

Convert a version string to a chronologically-sortable key

This is a rough cross between Distutils' StrictVersion and LooseVersion;
if you give it versions that would work with StrictVersion, then it behaves
the same; otherwise it acts like a slightly-smarter LooseVersion. It is
*possible* to create pathological version coding schemes that will fool
this parser, but they should be very rare in practice.

The returned value will be a tuple of strings.  Numeric portions of the
version are padded to 8 digits so they will compare numerically, but
without relying on how numbers compare relative to strings.  Dots are
dropped, but dashes are retained.  Trailing zeros between alpha segments
or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
"2.4". Alphanumeric parts are lower-cased.

The algorithm assumes that strings like "-" and any alpha string that
alphabetically follows "final"  represents a "patch level".  So, "2.4-1"
is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
considered newer than "2.4-1", which in turn is newer than "2.4".

Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
come before "final" alphabetically) are assumed to be pre-release versions,
so that the version "2.4" is considered newer than "2.4a1".

Finally, to handle miscellaneous cases, the strings "pre", "preview", and
"rc" are treated as if they were "c", i.e. as though they were release
candidates, and therefore are not as new as a version string that does not
contain them, and "dev" is replaced with an '@' so that it sorts lower
than any other pre-release tag.

Mit anderen Worten, parse_version gibt ein Tupel für jede Versionszeichenfolge zurück, das mit StrictVersion kompatibel ist, aber auch beliebige Versionen akzeptiert und mit ihnen umgeht, sodass sie verglichen werden können.

>>> from pkg_resources import parse_version as V
>>> V('1.2')
('00000001', '00000002', '*final')
>>> V('1.2b2')
('00000001', '00000002', '*b', '00000002', '*final')
>>> V('FunkyVersion')
('*funkyversion', '*final')

In diesem Schema hat Praktikabilität Vorrang vor Reinheit, führt aber als Ergebnis zu keiner Politik durchsetzung und zu sehr komplexen Semantiken aufgrund des Fehlens eines klaren Standards. Es versucht lediglich, sich an weit verbreitete Konventionen anzupassen.

Vorbehalte bestehender Systeme

Das Hauptproblem bei den beschriebenen Versionsvergleichswerkzeugen ist, dass sie zu permissiv sind und gleichzeitig einige der erforderlichen Semantiken nicht ausdrücken können. Viele der Versionen auf PyPI [4] sind offensichtlich keine nützlichen Versionen, was es für Benutzer schwierig macht, die Versionierung eines bestimmten Pakets zu verstehen und Tools auf PyPI bereitzustellen.

Distutils-Klassen werden nicht wirklich in Python-Projekten verwendet, aber die Setuptools-Funktion ist ziemlich verbreitet, da sie von Tools wie easy_install [6], pip [5] oder zc.buildout [7] zur Installation von Abhängigkeiten eines gegebenen Projekts verwendet wird.

Obwohl Setuptools *einen* Mechanismus zum Vergleichen/Sortieren von Versionen bietet, ist es viel besser, wenn die Versionierungsangabe so ist, dass ein Mensch einen vernünftigen Versuch unternehmen kann, diese zu sortieren, ohne sie gegen Code ausführen zu müssen.

Außerdem gibt es ein Problem mit der Verwendung von Daten als „Hauptversionsnummer“ (z.B. eine Versionszeichenfolge „20090421“) mit RPMs: Dies bedeutet, dass jeder Versuch, zu einem typischeren „Haupt.Neben…“-Versionsschema zu wechseln, problematisch ist, da es immer kleiner als „20090421“ sortiert wird.

Schließlich ist die Bedeutung von - spezifisch für Setuptools, während sie in einigen Paketiersystemen wie denen von Debian oder Ubuntu vermieden wird.

Der neue Versionierungsalgorithmus

Auf der Pycon arbeiteten Mitglieder der Python-, Ubuntu- und Fedora-Community an einem Versionsstandard, der für alle akzeptabel wäre.

Er heißt derzeit verlib und ein Prototyp befindet sich unter [9].

Das unterstützte Pseudo-Format ist:

N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]

Der eigentliche reguläre Ausdruck lautet:

expr = r"""^
(?P<version>\d+\.\d+)         # minimum 'N.N'
(?P<extraversion>(?:\.\d+)*)  # any number of extra '.N' segments
(?:
    (?P<prerel>[abc]|rc)         # 'a' = alpha, 'b' = beta
                                 # 'c' or 'rc' = release candidate
    (?P<prerelversion>\d+(?:\.\d+)*)
)?
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
$"""

Einige Beispiele machen es wahrscheinlich klarer:

>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
...  < V('1.0a2.dev456')
...  < V('1.0a2')
...  < V('1.0a2.1.dev456')
...  < V('1.0a2.1')
...  < V('1.0b1.dev456')
...  < V('1.0b2')
...  < V('1.0b2.post345')
...  < V('1.0c1.dev456')
...  < V('1.0c1')
...  < V('1.0.dev456')
...  < V('1.0')
...  < V('1.0.post456.dev34')
...  < V('1.0.post456'))
True

Das nachgestellte .dev123 ist für Vorabversionen. Das .post123 ist für Nachveröffentlichungen – die offenbar von einer Reihe von Projekten verwendet werden (z.B. Twisted [8]). Zum Beispiel könnte *nach* einer Veröffentlichung 1.2.0 eine Veröffentlichung 1.2.0-r678 folgen. Wir haben post anstelle von r verwendet, da das r mehrdeutig ist, ob es eine Vor- oder Nachveröffentlichung angibt.

.post456.dev34 gibt einen Entwicklungsmarker für eine Nachveröffentlichung an, der kleiner als ein .post456 Marker ist. Dies kann verwendet werden, um Entwicklungsversionen von Nachveröffentlichungen zu erstellen.

Vorabversionen können a für „alpha“, b für „beta“ und c für „release candidate“ verwenden. rc ist eine alternative Notation für „release candidate“, die hinzugefügt wird, um das Versionierungsschema mit Pythons eigenem Versionsschema kompatibel zu machen. rc wird nach c sortiert.

>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
...  < V('1.0a2')
...  < V('1.0b3')
...  < V('1.0c1')
...  < V('1.0rc2')
...  < V('1.0'))
True

Beachten Sie, dass c der bevorzugte Marker für Drittanbieterprojekte ist.

verlib bietet eine Klasse NormalizedVersion und eine Funktion suggest_normalized_version.

NormalizedVersion

Die Klasse NormalizedVersion wird verwendet, um eine Version zu speichern und sie mit anderen zu vergleichen. Sie nimmt einen String als Argument, der die Darstellung der Version enthält.

>>> from verlib import NormalizedVersion
>>> version = NormalizedVersion('1.0')

Die Version kann als String dargestellt werden:

>>> str(version)
'1.0'

Oder verglichen mit anderen:

>>> NormalizedVersion('1.0') > NormalizedVersion('0.9')
True
>>> NormalizedVersion('1.0') < NormalizedVersion('1.1')
True

Eine Klassenmethode namens from_parts ist verfügbar, wenn Sie eine Instanz erstellen möchten, indem Sie die Teile angeben, aus denen sich die Version zusammensetzt.

Beispiele

>>> version = NormalizedVersion.from_parts((1, 0))
>>> str(version)
'1.0'

>>> version = NormalizedVersion.from_parts((1, 0), ('c', 4))
>>> str(version)
'1.0c4'

>>> version = NormalizedVersion.from_parts((1, 0), ('c', 4), ('dev', 34))
>>> str(version)
'1.0c4.dev34'

suggest_normalized_version

suggest_normalized_version ist eine Funktion, die eine normalisierte Version vorschlägt, die der gegebenen Versionszeichenfolge nahe kommt. Wenn Sie eine Versionszeichenfolge haben, die nicht normalisiert ist (d.h. NormalizedVersion mag sie nicht), dann können Sie möglicherweise eine äquivalente (oder nahegelegene) normalisierte Version von dieser Funktion erhalten.

Dies führt eine Reihe einfacher Normalisierungen der gegebenen Zeichenfolge durch, basierend auf einer Beobachtung von Versionen, die derzeit auf PyPI verwendet werden.

Bei einem Dump dieser Versionen vom 6. Januar 2010 hat die Funktion aus den 8821 Distributionen, die PyPI hatte, die folgenden Ergebnisse geliefert:

  • 7822 (88,67 %) stimmen bereits mit NormalizedVersion ohne Änderungen überein.
  • 717 (8,13 %) stimmen überein, wenn diese Vorschlagsmethode verwendet wird.
  • 282 (3,20 %) stimmen überhaupt nicht überein.

Die 3,20 % der Projekte, die mit NormalizedVersion inkompatibel sind und nicht in eine kompatible Form umgewandelt werden können, sind meistens datumsbasierte Versionsschemata, Versionen mit benutzerdefinierten Markern oder Dummy-Versionen. Beispiele:

  • funktionierender Proof of Concept
  • 1 (erste Fassung)
  • unreleased.unofficialdev
  • 0.1.alphadev
  • 2008-03-29_r219
  • usw.

Wenn ein Tool mit Versionen arbeiten muss, ist eine Strategie die Verwendung von suggest_normalized_version auf die Versionszeichenfolgen. Wenn diese Funktion None zurückgibt, bedeutet dies, dass die bereitgestellte Version nicht nah genug am Standardschema ist. Wenn sie eine Version zurückgibt, die sich geringfügig von der ursprünglichen Version unterscheidet, handelt es sich um eine vorgeschlagene normalisierte Version. Schließlich bedeutet, wenn sie dieselbe Zeichenfolge zurückgibt, dass die Version mit dem Schema übereinstimmt.

Hier ist ein Anwendungsbeispiel:

>>> from verlib import suggest_normalized_version, NormalizedVersion
>>> import warnings
>>> def validate_version(version):
...     rversion = suggest_normalized_version(version)
...     if rversion is None:
...         raise ValueError('Cannot work with "%s"' % version)
...     if rversion != version:
...         warnings.warn('"%s" is not a normalized version.\n'
...                       'It has been transformed into "%s" '
...                       'for interoperability.' % (version, rversion))
...     return NormalizedVersion(rversion)
...

>>> validate_version('2.4-rc1')
__main__:8: UserWarning: "2.4-rc1" is not a normalized version.
It has been transformed into "2.4c1" for interoperability.
NormalizedVersion('2.4c1')

>>> validate_version('2.4c1')
NormalizedVersion('2.4c1')

>>> validate_version('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in validate_version
ValueError: Cannot work with "foo"

Roadmap

Distutils wird seine bestehenden Versionen-Klassen zugunsten von NormalizedVersion als veraltet kennzeichnen. Das in diesem PEP vorgestellte Modul verlib wird in version umbenannt und im Paket distutils platziert.

Referenzen

Danksagungen

Trent Mick, Matthias Klose, Phillip Eby, David Lyon und viele Leute auf der Pycon und im Distutils-SIG.


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

Zuletzt geändert: 2024-12-15 20:57:19 GMT