392 lines
12 KiB
Plaintext
392 lines
12 KiB
Plaintext
|
PEP: 386
|
|||
|
Title: Changing the version comparison module in Distutils
|
|||
|
Version: $Revision$
|
|||
|
Last-Modified: $Date$
|
|||
|
Author: Tarek Ziadé <tarek@ziade.org>
|
|||
|
Status: Draft
|
|||
|
Type: Standards Track
|
|||
|
Content-Type: text/x-rst
|
|||
|
Created: 4-June-2009
|
|||
|
|
|||
|
|
|||
|
Abstract
|
|||
|
========
|
|||
|
|
|||
|
This PEP proposes the inclusion of a new version comparison system in
|
|||
|
Distutils.
|
|||
|
|
|||
|
|
|||
|
Motivation
|
|||
|
==========
|
|||
|
|
|||
|
Distutils will soon extend the metadata standard, by including the
|
|||
|
`install_requires` field from Setuptools [#requires]_ among other changes.
|
|||
|
|
|||
|
These changes are a work in progress in PEP 345 [#pep345]_, but validating
|
|||
|
the current PEP is mandatory to continue the work.
|
|||
|
|
|||
|
The `install_requires` field will allow a package to define a dependency on
|
|||
|
another package and optionally restrict this dependency to a set of
|
|||
|
compatible versions.
|
|||
|
|
|||
|
That's why Distutils needs to provide a robust standard and reference
|
|||
|
implementation to compare versions numbers.
|
|||
|
|
|||
|
|
|||
|
Proposal
|
|||
|
========
|
|||
|
|
|||
|
In Python there are no real restriction yet on how a project should manage
|
|||
|
its versions, and how they should be incremented. They are no standard
|
|||
|
either, even if they are a few conventions widely used, like having a major
|
|||
|
and a minor revision (1.1, 1.2, etc.).
|
|||
|
|
|||
|
Developers are free to put in the `version` meta-data of their package any
|
|||
|
string they want, and push a new release at PyPI. This version will appear
|
|||
|
as the `latest` for end users.
|
|||
|
|
|||
|
Some project are also using dates as their major version numbers, or a custom
|
|||
|
versioning standard that is sometimes quite exotic.
|
|||
|
|
|||
|
The problem with this freedom is that the package will be harder to re-package
|
|||
|
for OS packagers, that need to have stricter conventions. The worst case is
|
|||
|
when a packager is unable to easily compare the versions he needs to package.
|
|||
|
|
|||
|
This PEP proposes to change the `version` module in Distutils with a new one
|
|||
|
that complies with the needs.
|
|||
|
|
|||
|
|
|||
|
Existing version systems
|
|||
|
========================
|
|||
|
|
|||
|
There are two main systems in Python:
|
|||
|
|
|||
|
- The current Distutils system [#distutils]_
|
|||
|
- Setuptools [#setuptools]_
|
|||
|
|
|||
|
Distutils
|
|||
|
---------
|
|||
|
|
|||
|
Distutils currently provides a `StrictVersion` and a `LooseVersion` class
|
|||
|
that can be used to manage versions.
|
|||
|
|
|||
|
The `LooseVersion` class is quite laxest. From Distutils doc::
|
|||
|
|
|||
|
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").
|
|||
|
|
|||
|
This class makes any version string valid, and provides an algorithm to sort
|
|||
|
them numerically then lexically. It means that anything can be used to version
|
|||
|
your project::
|
|||
|
|
|||
|
>>> from distutils.version import LooseVersion as V
|
|||
|
>>> v1 = V('FunkyVersion')
|
|||
|
>>> v2 = V('GroovieVersion')
|
|||
|
>>> v1 > v2
|
|||
|
False
|
|||
|
|
|||
|
The `StrictVersion` class is more strict. From the doc::
|
|||
|
|
|||
|
Version numbering for anal 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
|
|||
|
|
|||
|
This class enforces a few rules, and makes a decent tool to work with version
|
|||
|
numbers::
|
|||
|
|
|||
|
>>> 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
|
|||
|
|
|||
|
Although, it lacks a few elements to make it usable:
|
|||
|
|
|||
|
- development releases
|
|||
|
- post-release tags
|
|||
|
- development releases of post-release tags.
|
|||
|
|
|||
|
Notice that Distutils version classes are not really used in the community.
|
|||
|
|
|||
|
Setuptools
|
|||
|
----------
|
|||
|
|
|||
|
Setuptools provides another version comparison tool [#setuptools-version]_
|
|||
|
which does not enforce any rule for the version, but try to provide a better
|
|||
|
algorithm to convert the strings to sortable keys, with a ``parse_version``
|
|||
|
function.
|
|||
|
|
|||
|
From the doc::
|
|||
|
|
|||
|
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
|
|||
|
than any other pre-release tag.
|
|||
|
|
|||
|
In other words, ``parse_version`` will return a tuple for each version string,
|
|||
|
that is compatible with ``StrictVersion`` but also accept arbitrary version and
|
|||
|
deal with them so they can be compared::
|
|||
|
|
|||
|
>>> 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')
|
|||
|
|
|||
|
Caveats of existing systems
|
|||
|
---------------------------
|
|||
|
|
|||
|
The major problem with the described version comparison tools is that they are
|
|||
|
too permissive. Many of the versions on PyPI [#pypi]_ are obviously not useful
|
|||
|
versions, which makes it difficult for users to grok the versioning that a
|
|||
|
particular package was using and to provide tools on top of PyPI.
|
|||
|
|
|||
|
Distutils classes are not really used in Python projects, but the
|
|||
|
Setuptools function is quite spread because it's used by tools like
|
|||
|
`easy_install` [#ezinstall]_, `pip` [#pip]_ or `zc.buildout` [#zc.buildout]_
|
|||
|
to install dependencies of a given project.
|
|||
|
|
|||
|
While Setuptools *does* provide a mechanism for comparing/sorting versions,
|
|||
|
it is much preferable if the versioning spec is such that a human can make a
|
|||
|
reasonable attempt at that sorting without having to run it against some code.
|
|||
|
|
|||
|
Also there's a problem with the use of dates at the "major" version number
|
|||
|
(e.g. a version string "20090421") with RPMs: it means that any attempt to
|
|||
|
switch to a more typical "major.minor..." version scheme is problematic because
|
|||
|
it will always sort less than "20090421".
|
|||
|
|
|||
|
Last, the meaning of `-` is specific to Setuptools, while it is avoided in
|
|||
|
some packaging systems like the one used by Debian or Ubuntu.
|
|||
|
|
|||
|
The new versioning algorithm
|
|||
|
============================
|
|||
|
|
|||
|
During Pycon, members of the Python, Ubuntu and Fedora community worked on
|
|||
|
a version standard that would be acceptable for everyone.
|
|||
|
|
|||
|
It's currently called `verlib` and a prototype lives here :
|
|||
|
http://bitbucket.org/tarek/distutilsversion/src/
|
|||
|
|
|||
|
The pseudo-format supported is::
|
|||
|
|
|||
|
N.N[.N]+[abc]N[.N]+[.(dev|post)N+|(devNpostN)]
|
|||
|
|
|||
|
Some examples probably make it clearer::
|
|||
|
|
|||
|
>>> from verlib import RationalVersion 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.0c1.dev456')
|
|||
|
... < V('1.0c1')
|
|||
|
... < V('1.0.dev456')
|
|||
|
... < V('1.0')
|
|||
|
... < V('1.0.dev456post623')
|
|||
|
... < V('1.0.post456'))
|
|||
|
True
|
|||
|
|
|||
|
The trailing ".dev123" is for pre-releases. The ".post123" is for
|
|||
|
post-releases -- which apparently is used by a number of projects out there
|
|||
|
(e.g. Twisted [#twisted]_). For example *after* a "1.2.0" release there might
|
|||
|
be a "1.2.0-r678" release. We used "post" instead of "r" because the "r" is
|
|||
|
ambiguous as to whether it indicates a pre- or post-release.
|
|||
|
Last ".dev456post623" is a development version of a post-release.
|
|||
|
|
|||
|
``verlib`` provides a ``RationalVersion`` class and a
|
|||
|
``suggest_rational_version`` function.
|
|||
|
|
|||
|
RationalVersion
|
|||
|
---------------
|
|||
|
|
|||
|
The `RationalVersion` class is used to hold a version and to compare it with
|
|||
|
others. It takes a string as an argument, that contains the representation of
|
|||
|
the version::
|
|||
|
|
|||
|
>>> from verlib import RationalVersion
|
|||
|
>>> version = RationalVersion('1.0')
|
|||
|
|
|||
|
The version can be represented as a string::
|
|||
|
|
|||
|
>>> str(version)
|
|||
|
'1.0'
|
|||
|
|
|||
|
Or compared with others::
|
|||
|
|
|||
|
>>> RationalVersion('1.0') > RationalVersion('0.9')
|
|||
|
True
|
|||
|
>>> RationalVersion('1.0') < RationalVersion('1.1')
|
|||
|
True
|
|||
|
|
|||
|
A class method called ``from_parts`` is available if you want to create an
|
|||
|
instance by providing the parts that composes the version.
|
|||
|
|
|||
|
Each part is a tuple and there are three parts:
|
|||
|
|
|||
|
- the main version part
|
|||
|
- the pre-release part
|
|||
|
- the `devpost` marker part
|
|||
|
|
|||
|
Examples ::
|
|||
|
|
|||
|
>>> version = RationalVersion.from_parts((1, 0))
|
|||
|
>>> str(version)
|
|||
|
'1.0'
|
|||
|
|
|||
|
>>> version = RationalVersion.from_parts((1, 0), ('c', 4))
|
|||
|
>>> str(version)
|
|||
|
'1.0c4'
|
|||
|
|
|||
|
>>> version = RationalVersion.from_parts((1, 0), ('c', 4), ('dev', 34))
|
|||
|
>>> str(version)
|
|||
|
'1.0c4.dev34'
|
|||
|
|
|||
|
suggest_rational_version
|
|||
|
------------------------
|
|||
|
|
|||
|
XXX explain here suggest_rational_version
|
|||
|
|
|||
|
|
|||
|
References
|
|||
|
==========
|
|||
|
|
|||
|
.. [#distutils]
|
|||
|
http://docs.python.org/distutils
|
|||
|
|
|||
|
.. [#setuptools]
|
|||
|
http://peak.telecommunity.com/DevCenter/setuptools
|
|||
|
|
|||
|
.. [#setuptools-version]
|
|||
|
http://peak.telecommunity.com/DevCenter/setuptools#specifying-your-project-s-version
|
|||
|
|
|||
|
.. [#pypi]
|
|||
|
http://pypi.python.org/pypi
|
|||
|
|
|||
|
.. [#pip]
|
|||
|
http://pypi.python.org/pypi/pip
|
|||
|
|
|||
|
.. [#ezinstall]
|
|||
|
http://peak.telecommunity.com/DevCenter/EasyInstall
|
|||
|
|
|||
|
.. [#zc.buildout]
|
|||
|
http://pypi.python.org/pypi/zc.buildout
|
|||
|
|
|||
|
.. [#twisted]
|
|||
|
http://twistedmatrix.com/trac/
|
|||
|
|
|||
|
.. [#requires]
|
|||
|
http://peak.telecommunity.com/DevCenter/setuptools
|
|||
|
|
|||
|
.. [#pep345]
|
|||
|
http://svn.python.org/projects/peps/branches/jim-update-345/pep-0345.txt
|
|||
|
|
|||
|
Aknowledgments
|
|||
|
==============
|
|||
|
|
|||
|
Trent Mick, Matthias Klose, Phillip Eby, and many people at Pycon and
|
|||
|
Distutils-SIG.
|
|||
|
|
|||
|
Copyright
|
|||
|
=========
|
|||
|
|
|||
|
This document has been placed in the public domain.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
..
|
|||
|
Local Variables:
|
|||
|
mode: indented-text
|
|||
|
indent-tabs-mode: nil
|
|||
|
sentence-end-double-space: t
|
|||
|
fill-column: 70
|
|||
|
coding: utf-8
|
|||
|
End:
|