forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdiff.py
More file actions
97 lines (79 loc) · 2.91 KB
/
Copy pathdiff.py
File metadata and controls
97 lines (79 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""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",
"**/requirements*.txt",
"homeassistant/package_constraints.txt",
)
def is_tracked(path: str) -> bool:
"""Return True if `path` is a requirement file the checks care about."""
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())