July 12, 2008

Problems with Python __version__ parsing

As stated by Armin and commenters [*] the change from 0.9 to 0.10 is a convention in open source versioning, and the fault seems to lie more on the version-parsers than the version-suppliers. [†] Armin also notes that the appropriate solution is to use:

from pkg_resources import parse_version

Despite it not being the fault of the version supplier, we've recognized that this can be an issue and can certainly take precautions against letting client code interpret __version__ as a float. Right now there are two ways that I can think of doing this:

  1. Keep __version__ as a tuple. If you keep __version__ in tuple form you don't need to worry about client code forgetting to use the parse_version method.

  2. Use version numbers with more than one decimal. This prohibits the version from being parsed as a float because it's not the correct format — taking the current Linux kernel version as an example:

    >>> __version__ = '2.6.26'
    >>> float(__version__)
    Traceback (most recent call last):
    ...
    ValueError: invalid literal for float(): 2.6.26
    >>> tuple(int(i) for i in __version__.split('.'))
    (2, 6, 26)
    

    This ensures that the client code will think about a more appropriate way to parse the version number than using the float builtin; however, it doesn't prevent people from performing an inappropriate string comparison like the tuple does.

Footnotes

[*]

In … and 0.10 follows 0.9

[†]

Which seems to invalidate the implied conclusion of my title How not to do software version numbers, which I now realize was stupidly named.

How not to do software version numbers

There's a really nice, production-ready package called Pygments available from the Cheese Shop. [*] It's a brilliant package, but the developers made a silly mistake. [†]

The release version numbers progress from 0.9 to 0.10.

Sure, this looks reasonable; especially given that in Python developers typically write their version numbers like the following:

>>> import pygments
>>> if pygments.__version__ < (0, 5):
...     raise ImportError('Pygments too old.')
...
Traceback (most recent call last):
...
ImportError: Pygments too old.

This would be fine progressing from the (0, 9) tuple to the (0, 10) tuple. However, if your clients interpret your version number as a float (which isn't fully unreasonable, given that it really is represented as a float in writing) and you progress from version 0.9 to 0.10, your code base just fell back 8 minor releases, as in the following:

>>> import pygments
>>> print pygments.__version__
0.10

Additionally, you annoy people like me who get errors like this:

2008-06-17 20:43:58,653 Trac[loader] ERROR: Skipping "TracPygments 0.3dev" ("Pygments&gt;=0.5" not found)

When their setup says this:

cdleary$ pygmentize -V
Pygments version 0.10, (c) 2006-2007 by Georg Brandl .

Anybody who looks at numerical data in a sorted directory is familiar with the results of this phenomenon:

cdleary@gamma:~/example$ ls -l
total 0
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 10
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 11
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 7
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 8
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 9

Which is resolved with a simple prefixing method, as follows:

cdleary@gamma:~/example$ ls -l
total 0
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 07
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 08
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 09
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 10
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 11

Now I have the unfortunate choice of either hacking up my installation to allow the release number, or downgrading.

Footnotes

[*]

The Cheese Shop is now called PyPi, but I liked the Monty Python Cheese Shop sketch enough to honor its memory.

[†]

Update: I've lightly edited this post to be more tactful — I was slightly peeved at the time of its writing. :( See Problems with Python version parsing for more insight.