mirror of
https://github.com/home-assistant/core.git
synced 2026-05-22 16:30:27 +01:00
b724e52408
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
96 lines
2.8 KiB
Python
96 lines
2.8 KiB
Python
"""Parse a unified diff for changes to requirements files."""
|
|
|
|
from dataclasses import dataclass
|
|
from fnmatch import fnmatchcase
|
|
import re
|
|
|
|
from unidiff import PatchSet
|
|
|
|
from .models import PackageChange
|
|
|
|
# Glob patterns; kept in sync with the `paths:`
|
|
# filter of the deterministic workflow in
|
|
# `.github/workflows/check-requirements-deterministic.yml`.
|
|
# `pyproject.toml` is intentionally NOT tracked: hassfest enforces that
|
|
# every dependency declared there is mirrored into the generated
|
|
# requirements files, so the requirements files are the single source
|
|
# of truth for pinned package changes.
|
|
TRACKED_PATTERNS = (
|
|
"requirements*.txt",
|
|
"homeassistant/package_constraints.txt",
|
|
)
|
|
|
|
|
|
def _is_tracked(path: str) -> bool:
|
|
return any(fnmatchcase(path, pattern) for pattern in TRACKED_PATTERNS)
|
|
|
|
|
|
_PIN_RE = re.compile(
|
|
r"^([A-Za-z0-9][A-Za-z0-9._-]*)"
|
|
r"(?:\[[A-Za-z0-9,_-]+\])?"
|
|
r"\s*==\s*"
|
|
r"([A-Za-z0-9][A-Za-z0-9.+!*-]*)"
|
|
)
|
|
|
|
|
|
def _normalize(name: str) -> str:
|
|
"""PEP 503 canonical name."""
|
|
return re.sub(r"[-_.]+", "-", name).lower()
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class _Pin:
|
|
name: str # PEP 503 canonical
|
|
raw_name: str # original casing
|
|
version: str
|
|
|
|
|
|
def _parse_pin(line: str) -> _Pin | None:
|
|
body = line.split(";", 1)[0].strip()
|
|
m = _PIN_RE.match(body)
|
|
if not m:
|
|
return None
|
|
return _Pin(name=_normalize(m.group(1)), raw_name=m.group(1), version=m.group(2))
|
|
|
|
|
|
def parse_diff(diff_text: str) -> list[PackageChange]:
|
|
"""Return one PackageChange per package whose exact-pin changed in the diff.
|
|
|
|
A package that appears in both '-' and '+' is a bump; only in '+' is new.
|
|
"""
|
|
added: dict[str, _Pin] = {}
|
|
removed: dict[str, _Pin] = {}
|
|
for patched_file in PatchSet(diff_text):
|
|
if not _is_tracked(patched_file.path):
|
|
continue
|
|
for hunk in patched_file:
|
|
for line in hunk:
|
|
if not (line.is_added or line.is_removed):
|
|
continue
|
|
pin = _parse_pin(line.value)
|
|
if pin is None:
|
|
continue
|
|
bucket = added if line.is_added else removed
|
|
bucket.setdefault(pin.name, pin)
|
|
|
|
changes: list[PackageChange] = []
|
|
for name, add in added.items():
|
|
rem = removed.get(name)
|
|
if rem is None:
|
|
changes.append(
|
|
PackageChange(
|
|
name=add.raw_name,
|
|
old_version=None,
|
|
new_version=add.version,
|
|
)
|
|
)
|
|
elif rem.version != add.version:
|
|
changes.append(
|
|
PackageChange(
|
|
name=add.raw_name,
|
|
old_version=rem.version,
|
|
new_version=add.version,
|
|
)
|
|
)
|
|
return sorted(changes, key=lambda c: c.name.lower())
|