v0.158.0
 1from __future__ import annotations
 2
 3import re
 4
 5
 6def parse_version(version_str: str) -> tuple[int, ...]:
 7    """Parse a version string into a tuple of integers for comparison.
 8
 9    Lenient on purpose — callers compare versions people typed into config
10    files, so a string we can't fully parse should sort rather than raise: a
11    leading `v` is dropped, and each dot-separated segment contributes its
12    leading integer (`0-rc` → `0`), or `0` if it has none.
13
14    This only understands release versions. A pre-release like `1.75.0-rc.1`
15    parses to `(1, 75, 0, 1)` and so sorts *after* `1.75.0` — fine for the
16    release-to-release comparisons here, wrong if you need PEP 440 ordering.
17    """
18    clean_version = version_str.lstrip("v")
19    parts = []
20    for part in clean_version.split("."):
21        numeric_part = re.match(r"\d+", part)
22        parts.append(int(numeric_part.group()) if numeric_part else 0)
23    return tuple(parts)
24
25
26def compare_versions(v1: str, v2: str) -> int:
27    """Compare two version strings: -1 if v1 < v2, 0 if equal, 1 if v1 > v2.
28
29    The shorter side is zero-padded, so `1.75` compares equal to `1.75.0`
30    rather than older than it.
31    """
32    parsed_v1 = parse_version(v1)
33    parsed_v2 = parse_version(v2)
34
35    max_len = max(len(parsed_v1), len(parsed_v2))
36    parsed_v1 += (0,) * (max_len - len(parsed_v1))
37    parsed_v2 += (0,) * (max_len - len(parsed_v2))
38
39    if parsed_v1 < parsed_v2:
40        return -1
41    elif parsed_v1 > parsed_v2:
42        return 1
43    else:
44        return 0