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 (e.g., "alpha", "git"), the shorter release is considered greater.

  • 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.

  • 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 with unambiguous “newer than release” semantics across multiple ecosystems belong here. Candidates like rev or p are excluded because they can also mean “revision” (Gentoo -r0) or “pre-release patchlevel” (FreeBSD p1), depending on context.

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]