Source code for rs_metadata.normalize.versions
"""Version normalization.
Ecosystems spell the same release differently: a leading ``v``, a trailing
``.0``, R's dash before the final component. A version comparison that fired
on those would be useless, and one that ignored real differences would be
worse.
"""
from __future__ import annotations
import re
from typing import Any
from .scalars import normalize_text
__all__ = ["COMMIT_PREFIX_LENGTH", "normalize_version"]
#: How many characters of a commit hash are kept as the comparison key. A
#: short hash and the full hash it abbreviates then produce the same key by
#: plain equality, with a collision risk that is irrelevant at this scale.
COMMIT_PREFIX_LENGTH = 7
_VERSION_RE = re.compile(r"^v?(\d+(?:[._-]\d+)*)(.*)$", re.IGNORECASE)
_HEX_RE = re.compile(r"^[0-9a-f]+$", re.IGNORECASE)
[docs]
def looks_like_commit(text: str) -> bool:
"""Distinguish a Git commit hash from a calendar version.
A bare run of hex digits is ambiguous: ``20240101`` is a plausible CalVer
release and a plausible abbreviated hash. Requiring either the full
40 characters or at least one ``a``–``f`` resolves it in favor of the
interpretation that is almost always right.
Parameters
----------
text : str
A version string.
Returns
-------
bool
Whether to treat it as a commit hash.
Examples
--------
>>> looks_like_commit("a3f8c2d1b4e7f0291a3f8c2d1b4e7f0291a3f8c2")
True
>>> looks_like_commit("a3f8c2d")
True
A CalVer release is not mistaken for a hash, even though it is valid hex:
>>> looks_like_commit("20240101")
False
Nor is anything too short to be one, or not hex at all:
>>> looks_like_commit("1.2.0")
False
>>> looks_like_commit("abc")
False
"""
if not _HEX_RE.match(text):
return False
if len(text) == 40:
return True
return 7 <= len(text) < 40 and any(char in "abcdefABCDEF" for char in text)
[docs]
def normalize_version(raw: Any) -> str | None:
"""Normalize a version so equivalent spellings compare equal.
Parameters
----------
raw : Any
A version string or number.
Returns
-------
str or None
A comparison key, prefixed ``sha:`` for commit hashes, or ``None`` if
empty.
Notes
-----
Trailing zero components are dropped because they carry no information:
``1.2.0`` and ``1.2`` name one release. Pre-release and build suffixes are
kept, lower-cased with punctuation removed, because they *do* distinguish
releases.
Examples
--------
A leading ``v``, a trailing ``.0`` and R's dash are all cosmetic:
>>> normalize_version("1.2.0") == normalize_version("v1.2.0")
True
>>> normalize_version("1.2.0") == normalize_version("1.2")
True
>>> normalize_version("1.2-3") == normalize_version("1.2.3")
True
Genuinely different releases stay different — note that ``1.20`` is not
``1.2``, which is the trap behind an unquoted YAML version:
>>> normalize_version("1.2") == normalize_version("1.20")
False
>>> normalize_version("1.0.0") == normalize_version("1.0.0-rc1")
False
A short commit hash matches the full hash it abbreviates:
>>> full = "a3f8c2d1b4e7f0291a3f8c2d1b4e7f0291a3f8c2"
>>> normalize_version(full) == normalize_version(full[:7])
True
>>> normalize_version(full)
'sha:a3f8c2d'
Empty input is absence:
>>> normalize_version("") is None
True
"""
if raw is None:
return None
text = str(raw).strip()
if not text:
return None
if looks_like_commit(text):
return f"sha:{text[:COMMIT_PREFIX_LENGTH].lower()}"
match = _VERSION_RE.match(text)
if not match:
return normalize_text(text)
numbers = [int(part) for part in re.split(r"[._-]", match.group(1))]
while len(numbers) > 1 and numbers[-1] == 0:
numbers.pop()
suffix = re.sub(r"[^a-z0-9]", "", match.group(2).lower())
joined = ".".join(str(number) for number in numbers)
return f"{joined}-{suffix}" if suffix else joined