mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:07:24 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
"""Repo-local exemptions from the 250-line cap: a `.file-length-exempt` file.
|
|
|
|
The shared tables in :mod:`file_length._tables` hold exemptions that are true
|
|
everywhere (generated, vendored, wordlists). A fork of a third-party project
|
|
can also carry data that is code only by extension -- e.g. a fork's
|
|
``exh/eh/tags/*.kt`` are 6000-line string tables that upstream regenerates --
|
|
and that knowledge belongs next to the data, in the repo, not in a shared
|
|
list every other repo loads.
|
|
|
|
Format, one entry per line, glob relative to the repo root::
|
|
|
|
app/src/main/java/exh/eh/tags/*.kt # tag data tables mirrored from upstream
|
|
|
|
The reason is mandatory. An entry without one is a suppression, so the gate
|
|
fails closed on it (exit 2) instead of honouring it silently.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from functools import cache
|
|
from pathlib import Path
|
|
|
|
#: Basename of the per-repo exemption list, looked up in the gate's cwd.
|
|
EXEMPT_FILE = ".file-length-exempt"
|
|
|
|
|
|
class ExemptFileError(ValueError):
|
|
"""`.file-length-exempt` is malformed; the gate must not guess."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Exemption:
|
|
"""One glob plus the human reason it is exempt."""
|
|
|
|
pattern: str
|
|
reason: str
|
|
|
|
|
|
def parse(text: str) -> list[Exemption]:
|
|
"""Parse the file body; every non-comment entry must carry a reason."""
|
|
entries: list[Exemption] = []
|
|
for number, raw in enumerate(text.splitlines(), start=1):
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
pattern, sep, reason = line.partition("#")
|
|
pattern, reason = pattern.strip(), reason.strip()
|
|
if not sep or not reason or not pattern:
|
|
msg = f"{EXEMPT_FILE}:{number}: every entry is '<glob> # <reason>'"
|
|
raise ExemptFileError(msg)
|
|
entries.append(Exemption(pattern, reason))
|
|
return entries
|
|
|
|
|
|
def glob_to_regex(pattern: str) -> re.Pattern[str]:
|
|
"""A repo-relative glob as an anchored regex.
|
|
|
|
`**` spans directories, `*` and `?` stop at a slash. Hand-rolled rather
|
|
than `PurePath.full_match` because that needs Python 3.13 and the gate
|
|
runs on whatever `python3` a CI runner ships (3.12 broke it in the
|
|
a fork's first CI run).
|
|
"""
|
|
out: list[str] = []
|
|
i = 0
|
|
while i < len(pattern):
|
|
char = pattern[i]
|
|
if pattern.startswith("**", i):
|
|
out.append(".*")
|
|
i += 2
|
|
if pattern.startswith("/", i):
|
|
out[-1] = "(?:.*/)?"
|
|
i += 1
|
|
continue
|
|
if char == "*":
|
|
out.append("[^/]*")
|
|
elif char == "?":
|
|
out.append("[^/]")
|
|
else:
|
|
out.append(re.escape(char))
|
|
i += 1
|
|
return re.compile("^" + "".join(out) + "$")
|
|
|
|
|
|
@cache
|
|
def load(root: Path) -> tuple[Exemption, ...]:
|
|
"""The exemptions for the repo at `root`, empty when it has no file."""
|
|
path = root / EXEMPT_FILE
|
|
if not path.is_file():
|
|
return ()
|
|
return tuple(parse(path.read_text(encoding="utf-8")))
|
|
|
|
|
|
def repo_exempt_reason(path: Path, root: Path) -> str | None:
|
|
"""The repo-local reason `path` is exempt, or None if no entry matches.
|
|
|
|
`path` must be absolute (see `check.absolutize`); anything outside `root`
|
|
is not the repo's to exempt and gets None.
|
|
"""
|
|
entries = load(root)
|
|
if not entries:
|
|
return None
|
|
try:
|
|
relative = path.relative_to(root).as_posix()
|
|
except ValueError:
|
|
return None
|
|
for entry in entries:
|
|
if glob_to_regex(entry.pattern).match(relative):
|
|
return f"repo exemption: {entry.reason}"
|
|
return None
|