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.0and1!1.0 > 2.0because epoch2/1beats the implicit epoch0. Versions without an epoch default to0, 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) and0.1 > 0.beta2work without understanding PEP 440 or semver pre-release semantics.Trailing zeros are padding.
6.2and6.2.0compare 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:postandpatchwhatever introduces them, plus the Gentoo and Alpine suffixes ofUNDERSCORE_POST_RELEASE_TAGSbehind an underscore, so that1.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,
g6cd4c31would shatter into("g", 6, "cd", 4, "c", 31). The 7-character floor matchesgit’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
ubuntu1into("ubuntu", 1)enables natural numeric ordering of embedded version numbers:a4 < a10compares correctly because4and10become 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.
.devNordering relative to pre-releases is not handled. Usepackaging.versionfor 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_rc1compares greater than1.0_p1because"rc" > "post", whereapkanswers<. 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 plainTokencomparison, 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/rcsuffix 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
versrange 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, andc/rc/previewas equivalent aliases. These appear across ecosystems: Debian uses~alpha, npm uses-alpha, Homebrew usesalpha/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 orpretty_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
.postNas a post-release.patchcarries 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 makes1.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_TAGSinstead.
- 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_p1is a patch level of1.0, and1.0_git20240101a snapshot taken after it. Alpine’s own comparator agrees,apk version -t 1.0_p1 1.0answering>.The underscore is what makes these safe to recognize.
apkparses the suffixes in this spelling alone, refusing9.6p1outright, and every other reading of the same words arrives behind a different separator:2.1.1-git-*is a build identifier of2.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.
apkranks the suffixescvs < svn < git < hg < p, which no separator-blind tokenizer can reproduce, so all five normalize topostand 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:
objectA normalized word, persisting its lossless integer variant.
Supports natural comparison with
strandinttypes. Used to compare versions and package IDs.Instantiates a
Tokenfrom an alphanumeric string or a non-negative integer.
- class meta_package_manager.version.TokenizedString(value)[source]¶
Bases:
objectTokenize a string for user-friendly sorting.
Essentially a wrapper around a list of
Tokeninstances.Parse and tokenize the provided raw
value.- pretty_print()[source]¶
Reconstruct the tokenized string using original-case segments and separators.
- Return type:
- static tokenize(string)[source]¶
Tokenize a string: ignore case and split at each non-alphanumeric characters.
Returns a tuple of
Tokeninstances, 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
- meta_package_manager.version.parse_version¶
Alias for
TokenizedStringused 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:
objectA 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
Trueif the string looks like a version.Heuristics: at least one token is an integer, or there is only one non-integer token.
- Return type:
- 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.1774638290vs2.1.1774896198, the common part is2.1and 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:14ubuntu6and14ubuntu6.1highlight.1alone.The GNOME Shell extension carries a second implementation of this, as
diffVersions()ingnome-shell/mpm@kdeldycke.github.io/mpm.js. The two are held together bytests/version-diff-cases.json, the corpus both test suites assert against: a split changed here has to be changed there too.prefix_fg,old_fgandnew_fgoverride the common-prefix, old-suffix and new-suffix colors, in any form accepted byclick_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.