Files changed: - CHANGES.md - VERSION - tools/chemenu/tests/test_source_hygiene.py
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""Guard against a renamed stage leaving its old path standing in source.
|
|
|
|
`wiki/` was renamed to `kb/` on 2026-08-21. The directory moved; the string did
|
|
not, in 33 places - error messages, `--help` text, docstrings and the lint
|
|
report's own header, all naming a directory that no longer exists. Nothing
|
|
caught it, because no check reads a path literal in source.
|
|
|
|
This is that check. It is deliberately a plain substring scan over the source
|
|
tree rather than a `docs verify` check: `docs verify` reads `shipped_prose()`,
|
|
which is markdown only, and the bulk of the defect sat in `.py` strings.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
TOOLS_DIR = Path(__file__).resolve().parents[2]
|
|
|
|
# Retired stage path -> what replaced it. A future rename adds a row here in the
|
|
# same change that does the renaming, which is what makes the next occurrence a
|
|
# test failure instead of a string nobody reads for a year.
|
|
RETIRED_STAGE_PATHS = {
|
|
"wiki/": "kb/",
|
|
}
|
|
|
|
# Occurrences that are not stage paths at all. Kept as an explicit list with a
|
|
# reason rather than dodged by a cleverer regex: an exception a reader can see
|
|
# is worth more than one a pattern hides.
|
|
ALLOWED = {
|
|
# A fixture release URL, where `wiki` is a repository name in `owner/repo`.
|
|
("tests/test_dist_cmd.py", "https://example/torben/wiki/releases/tag/v0.3.1"),
|
|
}
|
|
|
|
|
|
SELF = Path(__file__).resolve()
|
|
|
|
|
|
def _scanned_files() -> list[Path]:
|
|
"""Every source file in `tools/` a stale path literal could hide in.
|
|
|
|
This module is excluded, and has to be: it is the one file whose job is to
|
|
name the retired paths, so scanning it would make the guard fail on its own
|
|
declaration.
|
|
"""
|
|
candidates = [*(TOOLS_DIR / "chemenu").rglob("*.py"), TOOLS_DIR / "wikitool"]
|
|
return sorted(path for path in candidates if path.resolve() != SELF)
|
|
|
|
|
|
def test_no_retired_stage_path_survives_in_source():
|
|
findings = []
|
|
for path in _scanned_files():
|
|
relative = path.relative_to(TOOLS_DIR / "chemenu" if path.suffix == ".py" else TOOLS_DIR)
|
|
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
for retired, replacement in RETIRED_STAGE_PATHS.items():
|
|
if retired not in line:
|
|
continue
|
|
if any(key == str(relative) and excerpt in line for key, excerpt in ALLOWED):
|
|
continue
|
|
findings.append(
|
|
f"{relative}:{number} names the retired path `{retired}` "
|
|
f"(now `{replacement}`): {line.strip()}"
|
|
)
|
|
assert findings == [], "Retired stage paths still in source:\n" + "\n".join(findings)
|
|
|
|
|
|
def test_the_guard_actually_scans_something():
|
|
"""A scan that silently matches no file passes for the wrong reason."""
|
|
scanned = _scanned_files()
|
|
assert len(scanned) > 40
|
|
assert TOOLS_DIR / "wikitool" in scanned
|
|
assert SELF not in scanned
|