Repository navigation
Expand file tree
/
Copy pathtest_dependency_pins.py
More file actions
69 lines (54 loc) · 2.45 KB
/
Copy pathtest_dependency_pins.py
File metadata and controls
69 lines (54 loc) · 2.45 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
"""Learners install a course's own requirements.txt; CI verifies the course
with requirements-dev.lock.txt. Both must resolve to the same versions, or CI
would pass on a stack learners never get (the pandas 3 risk in PR #94)."""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PIN = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==([^\s;]+)\s*(;.*)?$")
def _normalize(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()
def _requirement_lines(path: Path) -> list[str]:
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.split("#", 1)[0].strip()
if line:
lines.append(line)
return lines
def _learner_requirement_files() -> list[Path]:
tracked = subprocess.run(
["git", "-C", str(ROOT), "ls-files", "courses/*/requirements.txt", "paths/**/requirements.txt"],
check=True,
capture_output=True,
text=True,
).stdout.split()
return [ROOT / name for name in sorted(tracked)]
def _dev_lock_pins() -> dict[str, list[tuple[str, str | None]]]:
pins: dict[str, list[tuple[str, str | None]]] = {}
for line in _requirement_lines(ROOT / "requirements-dev.lock.txt"):
match = PIN.match(line)
if match:
name, version, marker = match.groups()
pins.setdefault(_normalize(name), []).append((version, marker))
return pins
def test_learner_requirement_files_exist() -> None:
assert _learner_requirement_files(), "expected course/path requirements.txt files"
def test_learner_requirements_match_the_ci_lock() -> None:
lock = _dev_lock_pins()
problems = []
for path in _learner_requirement_files():
rel = path.relative_to(ROOT)
for line in _requirement_lines(path):
match = PIN.match(line)
if not match or match.group(3):
problems.append(f"{rel}: '{line}' must be an exact, unconditional == pin")
continue
name, version, _ = match.groups()
locked = lock.get(_normalize(name))
if locked is None:
problems.append(f"{rel}: {name} is not in requirements-dev.lock.txt")
elif locked != [(version, None)]:
found = ", ".join(v + (f" ({m.lstrip('; ')})" if m else "") for v, m in locked)
problems.append(f"{rel}: {name}=={version}, but the CI lock has {found}")
assert problems == []