meta_package_manager.version module

Helpers and utilities to parse and compare version numbers.

mpm wraps dozens of package managers, each with its own versioning scheme: semver, PEP 440, calendar versioning, Debian epochs, Gentoo suffixes, and others. Rather than implementing format-specific parsers, this module provides a universal tokenizer that produces good-enough ordering across all of them.

Design

The tokenizer splits version strings into alternating digit and letter tokens at every digit/letter boundary and every non-alphanumeric separator. Tokens that parse as integers are compared numerically; the rest are compared as lowercase strings. This gives natural sort order where (2019, 0, 1) > (9, 3) — something neither pure-string nor pure-numeric comparison achieves.

Key rules:

  • Epochs dominate. A leading integer joined by : (Debian, RPM, pacman) or ! (PEP 440) is an epoch: a version-space reset that outranks the rest of the string. 2:1.0 > 9.0 and 1!1.0 > 2.0 because epoch 2/1 beats the implicit epoch 0. Versions without an epoch default to 0, so they compare unchanged.

  • Integers outrank strings. A numeric token always sorts higher than a string token at the same position. This makes 3.12.0 > 3.12.0a4 (release beats alpha) and 0.1 > 0.beta2 work without understanding PEP 440 or semver pre-release semantics.

  • Trailing zeros are padding. 6.2 and 6.2.0 compare equal. When one token tuple is a prefix of the other and all extra tokens are zero integers, the versions are equivalent.

  • Pre-release suffixes lose. When a release version is a prefix of a longer version whose first significant extra token is a string (like "alpha" or "rc"), the shorter release is considered greater. The exceptions are the post-release tags, which invert the rule: post and patch whatever introduces them, plus the Gentoo and Alpine suffixes of UNDERSCORE_POST_RELEASE_TAGS behind an underscore, so that 1.0_p1 > 1.0.

  • Hex hashes stay whole. A contiguous run of 7+ hex characters with interleaved digits and letters (at least one letter-then-digit and one digit-then-letter adjacency) is kept as a single opaque token. Without this, g6cd4c31 would shatter into ("g", 6, "cd", 4, "c", 31). The 7-character floor matches git’s default abbreviated hash length (core.abbrev, the de facto standard on GitHub/GitLab/Bitbucket). The interleaving requirement rejects coincidental hex strings like asciified Unicode (eeaccee231), that have only one transition direction.

  • Digit/letter splitting is essential. Splitting ubuntu1 into ("ubuntu", 1) enables natural numeric ordering of embedded version numbers: a4 < a10 compares correctly because 4 and 10 become integer tokens. Without this split, "a4" > "a10" lexicographically.

Limitations

This is a heuristic comparator, not a format-specific parser.

  • PEP 440 ordering is richer than what we implement. .devN ordering relative to pre-releases is not handled. Use packaging.version for strict PEP 440 compliance. Epochs (1!) are handled — see the epoch rule above.

  • Perl floating-point versions (1.1 == 1.10) are treated as (1, 1) vs (1, 10) — not equal. The Gentoo three-digit-group conversion scheme is not implemented.

  • A pre-release tag facing a post-release tag at the same position is ordered alphabetically, which is not their release order: 1.0_rc1 compares greater than 1.0_p1 because "rc" > "post", where apk answers <. The prefix rule of _compare_tuples() covers a suffix meeting a bare release, which is the common case; two suffixed versions of one base reach plain Token comparison, and that class carries no notion of a pre or post rank. Sorting them would mean ranking every string token for every manager, so it is left undone.

  • Format-specific separators like Java build metadata (,) or Perl-style floats (.) are treated as plain delimiters, which can produce wrong comparison results when the separator carries structural meaning. The epoch separators : and ! are recognized.

References

  • PEP 440 — Python’s version identification spec. Defines a/b/rc suffix ordering that our integer-outranks-string rule approximates.

  • Falsehoods about versions — 25 assumptions that break in practice. Validates our approach of not assuming any single format (falsehoods 4, 8, 13) and handling mixed numeric/string tokens (falsehoods 2, 3).

  • Gentoo Perl version scheme — illustrates how two incompatible formats (dotted-decimal and floating-point) require careful mapping. A reminder that version comparison cannot be reduced to “split on dots, compare integers.”

  • univers — scheme-aware version parsing and comparison (PEP 440, semver, Debian, RPM, Gentoo ebuild, and more) plus the vers range spec, from the same AboutCode team maintaining purl. The reference implementation to evaluate if this heuristic comparator ever needs per-scheme accuracy.

meta_package_manager.version.ALNUM_EXTRACTOR_CI = re.compile('(\n    (?= [0-9a-f]* [a-f] [0-9] )\n    (?= [0-9a-f]* [0-9] [a-f] )\n    [0-9a-f]{7,}\n    | \\d+\n    | [a-z]+\n)', re.IGNORECASE|re.VERBOSE)

Case-insensitive variant used to split the original string and preserve case.

meta_package_manager.version.TOKEN_ALIASES: dict[str, str] = {'alpha': 'a', 'beta': 'b', 'c': 'rc', 'preview': 'rc'}

Canonical short forms for pre-release tag spellings.

PEP 440 defines alpha/a, beta/b, and c/rc/preview as equivalent aliases. These appear across ecosystems: Debian uses ~alpha, npm uses -alpha, Homebrew uses alpha/beta. The long forms are always interchangeable with the short forms, so normalizing at tokenization time is safe. Normalization only affects comparison tokens, not the original string or pretty_print() output.

meta_package_manager.version.POST_RELEASE_TAGS: frozenset[str] = frozenset({'patch', 'post'})

Suffixes that indicate a version newer than the base release.

PEP 440 defines .postN as a post-release. patch carries the same semantics in some ecosystems (e.g., 1.0-patch1). Without this set, the prefix-comparison rule treats all string suffixes as pre-release indicators, which wrongly makes 1.0 > 1.0.post1.

This set is deliberately small. Only tags whose spelling alone carries “newer than release” semantics across ecosystems belong here. A tag whose meaning depends on the separator introducing it goes in UNDERSCORE_POST_RELEASE_TAGS instead.

meta_package_manager.version.UNDERSCORE_POST_RELEASE_TAGS: frozenset[str] = frozenset({'cvs', 'git', 'hg', 'p', 'svn'})

Post-release suffixes of the Gentoo and Alpine scheme, recognized only behind an underscore.

Both distributions split a suffix into a pre-release half (_alpha, _beta, _pre, _rc), which the generic string-loses-to-release rule already orders correctly, and a post-release half listed here: 1.0_p1 is a patch level of 1.0, and 1.0_git20240101 a snapshot taken after it. Alpine’s own comparator agrees, apk version -t 1.0_p1 1.0 answering >.

The underscore is what makes these safe to recognize. apk parses the suffixes in this spelling alone, refusing 9.6p1 outright, and every other reading of the same words arrives behind a different separator: 2.1.1-git-* is a build identifier of 2.1.1, not a release after it. Gating on the separator therefore fixes the two schemes that spell it this way and leaves every other one untouched.

Ordering within the set is not modelled. apk ranks the suffixes cvs < svn < git < hg < p, which no separator-blind tokenizer can reproduce, so all five normalize to post and compare equal to each other. That keeps the fix inside this module’s remit, a heuristic comparator rather than a per-format parser.

class meta_package_manager.version.Token(value)[source]

Bases: object

A normalized word, persisting its lossless integer variant.

Supports natural comparison with str and int types. Used to compare versions and package IDs.

Instantiates a Token from an alphanumeric string or a non-negative integer.

static str_to_int(value)[source]

Convert a str or an int to a (string, integer) couple.

Returns together the original string and its integer representation if conversion is successful and lossless. Else, returns the original value and None.

Return type:

tuple[str, int | None]

string: str
integer: int | None = None
property isint: bool

Does the Token got an equivalent pure integer representation?

class meta_package_manager.version.TokenizedString(value)[source]

Bases: object

Tokenize a string for user-friendly sorting.

Essentially a wrapper around a list of Token instances.

Parse and tokenize the provided raw value.

string: str
tokens: tuple[Token, ...] = ()
separators: tuple[str, ...] = ()
original_segments: tuple[str, ...] = ()

Original-case token strings for lossless pretty_print().

epoch: int = 0

Leading epoch (N: or N!); dominates comparison, 0 when absent.

release: tuple[Token, ...] = ()

Comparison tokens with the epoch removed. See _split_epoch().

pretty_print()[source]

Reconstruct the tokenized string using original-case segments and separators.

Return type:

str

static tokenize(string)[source]

Tokenize a string: ignore case and split at each non-alphanumeric characters.

Returns a tuple of Token instances, separator strings between consecutive tokens, and original-case segment strings for lossless display.

re.split() with a capturing group alternates non-matching segments (even indices) and captured matches (odd indices):

ALNUM_EXTRACTOR.split("4.2.1-5666.3")
['', '4', '.', '2', '.', '1', '-', '5666', '.', '3', '']
 pre   m   sep   m   sep   m   sep    m     sep   m   suf
Return type:

tuple[tuple[Token, ...], tuple[str, ...], tuple[str, ...]]

meta_package_manager.version.parse_version

Alias for TokenizedString used in version-comparison contexts.

meta_package_manager.version.OPERATOR_MAP: dict[str, Callable[[TokenizedString, TokenizedString], bool]] = {'!=': <built-in function ne>, '<': <built-in function lt>, '<=': <built-in function le>, '==': <built-in function eq>, '>': <built-in function gt>, '>=': <built-in function ge>}

Comparison operators recognized in a version range, mapped to their callable.

meta_package_manager.version.RANGE_OPERATOR = re.compile('(?P<op>>=|<=|==|!=|>|<)\\s*(?P<version>.+)')

Matches a comparison operator prefix followed by a version string.

class meta_package_manager.version.VersionRange(spec)[source]

Bases: object

A set of version constraints parsed from a comma-separated specifier string.

Each constraint is an (operator, version) pair. A version satisfies the range only if it satisfies every constraint.

Bare version strings (no operator prefix) are treated as >=.

meta_package_manager.version.is_version(string)[source]

Returns True if the string looks like a version.

Heuristics: at least one token is an integer, or there is only one non-integer token.

Return type:

bool

meta_package_manager.version.diff_versions(old, new, prefix_fg='bright_black', old_fg='red', new_fg='green')[source]

Color the common prefix gray, the old suffix red, the new suffix green.

The split point snaps to the nearest separator boundary so the full diverging token and its preceding separator are highlighted. For 2.1.1774638290 vs 2.1.1774896198, the common part is 2.1 and the diff includes .1774638290 / .1774896198. It does not snap when the divergence already sits on a boundary, one version being the other plus a whole new token: 14ubuntu6 and 14ubuntu6.1 highlight .1 alone.

The GNOME Shell extension carries a second implementation of this, as diffVersions() in gnome-shell/mpm@kdeldycke.github.io/mpm.js. The two are held together by tests/version-diff-cases.json, the corpus both test suites assert against: a split changed here has to be changed there too.

prefix_fg, old_fg and new_fg override the common-prefix, old-suffix and new-suffix colors, in any form accepted by click_extra.style() (a named ANSI color or an xterm-256 palette index). Renderers whose consumer maps the named defaults poorly, like the bar plugin on a light translucent menu, pass their own.

Return type:

tuple[str, str]