mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
3529 invalid comments in 528 files, grandfathered in while the linter only saw the working-tree diff. Removal ran through the linter's own collectors, so detection and deletion cannot drift: a comment whose opener line is blank before it and whose closer line is blank after it loses all its lines, anything else loses only the comment region, and a line left holding a bare delimiter goes with it. Five files keep their comments behind a nocheck marker instead. Four are upstream configs whose inline docs are upstream's own (mautrix, systemd journald, nextcloud-oidc-login, MediaWiki). In littlejs the commented-out entries ARE the deactivation mechanism for an example tile, so deleting them would delete configuration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
284 lines
9.8 KiB
Python
284 lines
9.8 KiB
Python
"""Lint that pins ``PROJECT_ROOT`` to a single canonical source.
|
|
|
|
Three rules are enforced:
|
|
|
|
1. Every Python file in the repo (``*.py``) MUST NOT compute the
|
|
project root locally. The forbidden patterns are:
|
|
|
|
* a top-level assignment that climbs ``parents[N]`` from
|
|
``__file__`` into a ``*_ROOT``-style variable,
|
|
* a bare ``Path(__file__).…parents`` chain (no ``[N]`` index)
|
|
used to iterate upward in search of the root,
|
|
* any chain of ``..`` segments (string literals or ``os.pardir``
|
|
references) used to walk up to the root,
|
|
* a function such as ``repo_root()`` / ``project_root()`` (or
|
|
their underscore-prefixed ``_repo_root`` / ``_project_root``
|
|
siblings) that searches upward for ``pyproject.toml``.
|
|
|
|
The single legitimate definition site is the package's own
|
|
``__init__.py``. Consumers MUST import ``PROJECT_ROOT`` from there
|
|
(relatively as ``from . import PROJECT_ROOT`` or absolutely as
|
|
``from <pkg> import PROJECT_ROOT``) so a future move of the file
|
|
does not silently break a hard-coded ``parents[N]`` index.
|
|
|
|
2. ``PROJECT_ROOT`` itself MUST only be assigned inside an
|
|
``__init__.py``. A ``PROJECT_ROOT = …`` line anywhere else (test
|
|
modules, ``__main__.py`` shims, helper modules, …) fails the lint
|
|
even when the right-hand side does not climb ``parents[N]`` —
|
|
re-binding the constant locally defeats the single-source guarantee
|
|
rule 1 was added to enforce.
|
|
|
|
3. Every ``__init__.py`` that DOES define ``PROJECT_ROOT`` MUST point
|
|
at a directory containing ``pyproject.toml``. A wrong
|
|
``parents[N]`` index turns the constant into a silent footgun;
|
|
the lint catches it the moment the index drifts off the repo root.
|
|
|
|
Suppression
|
|
-----------
|
|
|
|
Bootstrap files that prepend the repo root to ``sys.path`` before any
|
|
package import resolves (the ``__main__`` shims under ``cli/``) MUST
|
|
mark the offending line with ``# nocheck: project-root-import`` and
|
|
explain why the local computation is unavoidable. The same applies to
|
|
standalone scripts under ``roles/<role>/files/`` that have no
|
|
package container to import from. See
|
|
[suppression.md](../../../docs/contributing/actions/testing/suppression.md).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils.annotations.suppress import is_suppressed_at
|
|
from utils.cache.files import iter_non_ignored_files, read_text
|
|
|
|
SUPPRESS_RULE: str = "project-root-import"
|
|
|
|
|
|
# with `# nocheck: project-root-import` and document why.
|
|
_FORBIDDEN_PATTERNS: tuple[tuple[str, re.Pattern[str], str], ...] = (
|
|
(
|
|
"`parents[N]` index expression",
|
|
re.compile(r"\.parents\[\s*\d+\s*\]"),
|
|
"stripped",
|
|
),
|
|
(
|
|
"chained `Path(__file__)…parent.parent.…` upward walk",
|
|
re.compile(
|
|
r"Path\(\s*__file__\s*\)(?:\s*\.\s*[A-Za-z_]\w*\([^)]*\))*"
|
|
r"(?:\s*\.\s*parent\b){2,}"
|
|
),
|
|
"stripped",
|
|
),
|
|
(
|
|
"`Path(__file__).…parents` upward iteration",
|
|
re.compile(
|
|
r"Path\(\s*__file__\s*\)(?:\s*\.\s*[A-Za-z_]\w*\([^)]*\))*"
|
|
r"\s*\.\s*parents\b(?!\s*\[)"
|
|
),
|
|
"stripped",
|
|
),
|
|
(
|
|
"`os.pardir` reference",
|
|
re.compile(r"\bos\.pardir\b"),
|
|
"stripped",
|
|
),
|
|
(
|
|
# nocheck: project-root-import catalog entry mentions the literal pattern by design
|
|
'".." path segment in a path-construction context',
|
|
re.compile(r"""["']\.\.(?:/+\.\.)*/?["']"""),
|
|
"no_docs",
|
|
),
|
|
(
|
|
"function searching upward for pyproject.toml",
|
|
re.compile(r"^\s*def\s+_*(?:repo_root|project_root)\s*\("),
|
|
"stripped",
|
|
),
|
|
)
|
|
|
|
_PROJECT_ROOT_ASSIGN_RE = re.compile(r"^\s*PROJECT_ROOT(?:\s*:\s*[^=]+?)?\s*=(?!=)")
|
|
|
|
_INIT_PROJECT_ROOT_RE = re.compile(
|
|
r"^\s*PROJECT_ROOT(?:\s*:\s*[A-Za-z_][\w\[\], ]*)?\s*=\s*"
|
|
r"Path\(\s*__file__\s*\)\s*\.resolve\(\)\.parents\[\s*(\d+)\s*\]"
|
|
)
|
|
|
|
|
|
def _is_init_file(path: Path) -> bool:
|
|
return path.name == "__init__.py"
|
|
|
|
|
|
def _strip_strings_and_comments(text: str) -> str:
|
|
"""Return *text* with every string literal and `#` comment replaced
|
|
by whitespace that preserves line numbers and column offsets.
|
|
|
|
The forbidden patterns must only fire on real code, never on
|
|
documentation that mentions the patterns by name (the catalog
|
|
rule definitions in this very file, the dictionary docstring of
|
|
`utils/cache/__init__.py`, etc.).
|
|
"""
|
|
return _strip(text, keep_inline_strings=False)
|
|
|
|
|
|
def _strip_docs_and_comments(text: str) -> str:
|
|
"""Like :func:`_strip_strings_and_comments`, but preserve regular
|
|
single-line ``"…"`` and ``'…'`` literals.
|
|
|
|
Used by the ``".."`` rule, which deliberately scans the *content*
|
|
of path strings: with the inline strings stripped (the default
|
|
behaviour above) the regex would never match a real
|
|
``Path(… / ".." / …)`` callsite, while a docstring or
|
|
``# comment`` mentioning ``..`` should still be ignored.
|
|
"""
|
|
return _strip(text, keep_inline_strings=True)
|
|
|
|
|
|
def _strip(text: str, *, keep_inline_strings: bool) -> str:
|
|
out: list[str] = []
|
|
i = 0
|
|
n = len(text)
|
|
while i < n:
|
|
ch = text[i]
|
|
if text[i : i + 3] in ('"""', "'''"):
|
|
quote = text[i : i + 3]
|
|
end = text.find(quote, i + 3)
|
|
if end == -1:
|
|
out.append("\n" * text[i:].count("\n"))
|
|
break
|
|
block = text[i : end + 3]
|
|
out.append("".join("\n" if c == "\n" else " " for c in block))
|
|
i = end + 3
|
|
continue
|
|
if ch in ('"', "'"):
|
|
quote = ch
|
|
j = i + 1
|
|
while j < n and text[j] != quote:
|
|
if text[j] == "\\" and j + 1 < n:
|
|
j += 2
|
|
continue
|
|
if text[j] == "\n":
|
|
break
|
|
j += 1
|
|
block = text[i : j + 1]
|
|
if keep_inline_strings:
|
|
out.append(block)
|
|
else:
|
|
out.append("".join("\n" if c == "\n" else " " for c in block))
|
|
i = j + 1
|
|
continue
|
|
if ch == "#":
|
|
j = text.find("\n", i)
|
|
if j == -1:
|
|
out.append(" " * (n - i))
|
|
break
|
|
out.append(" " * (j - i))
|
|
i = j
|
|
continue
|
|
out.append(ch)
|
|
i += 1
|
|
return "".join(out)
|
|
|
|
|
|
def _scan_file(path: Path) -> list[str]:
|
|
"""Return human-readable failure descriptions for *path*.
|
|
|
|
Empty list means the file is clean. Suppression markers are
|
|
consulted on the offending line and the line directly above
|
|
(`same-or-above` semantics matching the rest of the suppression
|
|
catalog).
|
|
"""
|
|
try:
|
|
text = read_text(str(path))
|
|
except Exception:
|
|
return []
|
|
|
|
raw_lines = text.splitlines()
|
|
code_lines = _strip_strings_and_comments(text).splitlines()
|
|
no_docs_lines = _strip_docs_and_comments(text).splitlines()
|
|
failures: list[str] = []
|
|
|
|
is_init = _is_init_file(path)
|
|
|
|
for lineno in range(1, len(raw_lines) + 1):
|
|
stripped = code_lines[lineno - 1] if lineno - 1 < len(code_lines) else ""
|
|
no_docs = no_docs_lines[lineno - 1] if lineno - 1 < len(no_docs_lines) else ""
|
|
for label, pattern, scan_mode in _FORBIDDEN_PATTERNS:
|
|
target = stripped if scan_mode == "stripped" else no_docs
|
|
if not pattern.search(target):
|
|
continue
|
|
if is_init and _INIT_PROJECT_ROOT_RE.match(stripped):
|
|
continue
|
|
if is_suppressed_at(raw_lines, lineno, SUPPRESS_RULE, mode="same-or-above"):
|
|
continue
|
|
failures.append(f"{path}:{lineno}: {label}")
|
|
|
|
if (
|
|
not is_init
|
|
and _PROJECT_ROOT_ASSIGN_RE.match(stripped)
|
|
and not is_suppressed_at(
|
|
raw_lines, lineno, SUPPRESS_RULE, mode="same-or-above"
|
|
)
|
|
):
|
|
failures.append(
|
|
f"{path}:{lineno}: `PROJECT_ROOT` assignment outside "
|
|
f"`__init__.py` (import via `from . import PROJECT_ROOT` "
|
|
f"or `from <pkg> import PROJECT_ROOT` instead)"
|
|
)
|
|
|
|
return failures
|
|
|
|
|
|
def _check_init_project_root(path: Path) -> list[str]:
|
|
"""If *path* is an ``__init__.py`` that defines ``PROJECT_ROOT`` at
|
|
top level via ``Path(__file__).resolve().parents[N]``, validate
|
|
that the resolved path actually carries ``pyproject.toml``."""
|
|
if not _is_init_file(path):
|
|
return []
|
|
try:
|
|
text = read_text(str(path))
|
|
except Exception:
|
|
return []
|
|
|
|
failures: list[str] = []
|
|
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
m = _INIT_PROJECT_ROOT_RE.match(line)
|
|
if not m:
|
|
continue
|
|
depth = int(m.group(1))
|
|
try:
|
|
resolved = path.resolve().parents[depth]
|
|
except IndexError:
|
|
failures.append(
|
|
f"{path}:{lineno}: PROJECT_ROOT parents[{depth}] is out of "
|
|
f"range for this file's depth."
|
|
)
|
|
continue
|
|
if not (resolved / "pyproject.toml").is_file():
|
|
failures.append(
|
|
f"{path}:{lineno}: PROJECT_ROOT resolves to {resolved}, "
|
|
f"which does not contain pyproject.toml; fix the "
|
|
f"parents[{depth}] index."
|
|
)
|
|
return failures
|
|
|
|
|
|
class TestProjectRootImport(unittest.TestCase):
|
|
def test_no_local_project_root_computation(self):
|
|
offenders: list[str] = []
|
|
for path_str in iter_non_ignored_files(extensions=(".py",)):
|
|
path = Path(path_str)
|
|
offenders.extend(_scan_file(path))
|
|
offenders.extend(_check_init_project_root(path))
|
|
|
|
if offenders:
|
|
self.fail(
|
|
f"{len(offenders)} project-root violation(s):\n"
|
|
+ "\n".join(f" - {o}" for o in offenders)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|