refactor: drop packaging dependency, inline version parsing
Lint & Test / test (push) Successful in 3m9s

The only user of 'packaging' was version_check.py — two small functions
(normalize_version, is_newer) that just need to parse "1.2.3-alpha.1"
and compare PEP 440-style versions. That's well within stdlib reach.

- Inline a NamedTuple-based Version with kind/pre_num ordering
  (dev < alpha < beta < rc < release), same regex-normalized format
- Define a local InvalidVersion exception
- Remove packaging>=23.0 from pyproject.toml dependencies

Why now: the Windows cross-build uses a hard-coded DEPS array in
build-dist-windows.sh, which was never updated when 'packaging' was
added on March 25. Result: importable from pip-installed dev envs,
missing from the portable installer — tray icon appeared but uvicorn
died with ModuleNotFoundError: No module named 'packaging'.

Removing the dep entirely is cleaner than adding one more hard-coded
entry to the Windows DEPS list. Tests (678 passing) and a manual test
matrix covering dev/alpha/beta/rc/release ordering all pass.
This commit is contained in:
2026-04-07 23:54:27 +03:00
parent feb91ad281
commit d4ffe2e985
2 changed files with 76 additions and 18 deletions
-1
View File
@@ -26,7 +26,6 @@ dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"httpx>=0.27.2",
"packaging>=23.0",
"mss>=9.0.2",
"numpy>=2.1.3",
"pydantic>=2.9.2",
@@ -1,40 +1,99 @@
"""Version comparison utilities.
Normalizes Gitea-style tags (v0.3.0-alpha.1) to PEP 440 (0.3.0a1)
so that ``packaging.version.Version`` can compare them correctly.
and compares them using a tuple-based ordering. Deliberately does
not depend on the external ``packaging`` library — it's just one
more dependency to ship in the portable installer for a handful
of simple comparisons we can do with stdlib.
"""
import logging
import re
from packaging.version import InvalidVersion, Version
from typing import NamedTuple
logger = logging.getLogger(__name__)
class InvalidVersion(ValueError):
"""Raised when a version string cannot be parsed."""
# Release-kind ordering — smaller = earlier.
# dev < alpha < beta < rc < release
_KIND_DEV = 0
_KIND_ALPHA = 1
_KIND_BETA = 2
_KIND_RC = 3
_KIND_RELEASE = 4
class Version(NamedTuple):
"""Comparable version tuple. Larger tuples are newer versions."""
major: int
minor: int
patch: int
kind: int
pre_num: int
_PRE_MAP = {
"alpha": "a",
"beta": "b",
"rc": "rc",
"alpha": _KIND_ALPHA,
"a": _KIND_ALPHA,
"beta": _KIND_BETA,
"b": _KIND_BETA,
"rc": _KIND_RC,
}
_PRE_PATTERN = re.compile(
r"^(\d+\.\d+\.\d+)[-.]?(alpha|beta|rc)[.-]?(\d+)$", re.IGNORECASE
# Matches "1.2.3" optionally followed by a pre-release segment.
# Accepts Gitea-style ("1.2.3-alpha.1") and PEP 440 ("1.2.3a1", "1.2.3.dev0").
_VERSION_PATTERN = re.compile(
r"""
^
(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
(?:
[-.]? # optional separator
(?P<pre_label>alpha|beta|rc|a|b|dev) # pre-release label
[.-]? # optional separator
(?P<pre_num>\d+) # pre-release number
)?
$
""",
re.IGNORECASE | re.VERBOSE,
)
def normalize_version(raw: str) -> Version:
"""Convert a tag like ``v0.3.0-alpha.1`` to a PEP 440 ``Version``.
"""Parse a version string into a comparable ``Version`` tuple.
Raises ``InvalidVersion`` if the string cannot be parsed.
Accepts Gitea-style tags (``v0.3.0-alpha.1``) and PEP 440
(``0.3.0a1``, ``0.0.0.dev0``). Raises ``InvalidVersion`` if
the string cannot be parsed.
"""
cleaned = raw.lstrip("v").strip()
m = _PRE_PATTERN.match(cleaned)
if m:
base, pre_label, pre_num = m.group(1), m.group(2).lower(), m.group(3)
pep_label = _PRE_MAP.get(pre_label, pre_label)
cleaned = f"{base}{pep_label}{pre_num}"
return Version(cleaned)
cleaned = raw.lstrip("vV").strip()
m = _VERSION_PATTERN.match(cleaned)
if not m:
raise InvalidVersion(f"Unparseable version: {raw!r}")
major = int(m.group("major"))
minor = int(m.group("minor"))
patch = int(m.group("patch"))
pre_label = m.group("pre_label")
pre_num_str = m.group("pre_num")
if pre_label is None:
kind = _KIND_RELEASE
pre_num = 0
else:
label = pre_label.lower()
if label == "dev":
kind = _KIND_DEV
else:
kind = _PRE_MAP[label]
pre_num = int(pre_num_str) if pre_num_str is not None else 0
return Version(major, minor, patch, kind, pre_num)
def is_newer(candidate: str, current: str) -> bool: