Skip to content

Commit 4b79342

Browse files
OmarAlJarrahclaude
andcommitted
test(core): guard against Sphinx roles in docstrings
Add a test that scans every package source and test file and fails if a Sphinx cross-reference role appears, enforcing the project's plain-backtick docstring convention. Neither ruff nor mypy inspects docstring prose, so this is the only mechanical guard keeping the convention from rotting back in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5d46e02 commit 4b79342

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

packages/dexpace-sdk-core/tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _clean_context_store() -> Iterator[None]:
2929

3030

3131
class FakeClock:
32-
"""Deterministic :class:`~dexpace.sdk.core.util.Clock` for unit tests.
32+
"""Deterministic `Clock` for unit tests.
3333
3434
Wall-clock (``now``) and monotonic readings are tracked independently so
3535
tests can model the real divergence between them (system clock jumps vs
@@ -72,5 +72,5 @@ def advance(self, duration: float) -> None:
7272

7373
@pytest.fixture
7474
def fake_clock() -> FakeClock:
75-
"""Provide a fresh :class:`FakeClock` starting at ``t=0``."""
75+
"""Provide a fresh `FakeClock` starting at ``t=0``."""
7676
return FakeClock()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright (c) 2026 dexpace and Omar Aljarrah.
2+
# Licensed under the MIT License. See LICENSE.md in the repository root for details.
3+
4+
"""Docstring-style guardrail: no Sphinx cross-reference roles.
5+
6+
The project convention (CLAUDE.md) mandates Google-style docstrings with plain
7+
backticks for code/type references — never Sphinx roles such as
8+
``:class:`Foo```. Neither ``ruff`` nor ``mypy`` inspects docstring prose, so a
9+
one-time conversion sweep would silently rot without a mechanical guard. This
10+
test scans every package source and test file and fails if any role reappears,
11+
keeping the convention enforceable rather than aspirational.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import re
17+
from pathlib import Path
18+
19+
_REPO_ROOT = Path(__file__).resolve().parents[3]
20+
_SELF = Path(__file__).resolve()
21+
22+
# Assembled from parts (and an explicit backtick via ``chr(96)``) so this file
23+
# contains no literal Sphinx role and never flags itself.
24+
_ROLE_NAMES = ("class", "meth", "func", "data", "attr", "mod", "exc", "obj", "paramref", "term")
25+
_ROLE_RE = re.compile(r":(?:" + "|".join(_ROLE_NAMES) + r"):" + chr(96))
26+
27+
28+
def _python_sources() -> list[Path]:
29+
"""Return every ``.py`` file under each package's ``src`` and ``tests``."""
30+
files: list[Path] = []
31+
packages = _REPO_ROOT / "packages"
32+
for pkg in sorted(packages.iterdir()):
33+
for sub in ("src", "tests"):
34+
root = pkg / sub
35+
if root.is_dir():
36+
files.extend(p for p in root.rglob("*.py") if p.resolve() != _SELF)
37+
return files
38+
39+
40+
def test_no_sphinx_roles_in_docstrings() -> None:
41+
"""No source or test file may use a Sphinx cross-reference role."""
42+
offenders: list[str] = []
43+
for path in _python_sources():
44+
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
45+
if _ROLE_RE.search(line):
46+
offenders.append(f"{path.relative_to(_REPO_ROOT)}:{lineno}: {line.strip()}")
47+
assert not offenders, (
48+
"Sphinx cross-reference roles are forbidden; use plain backticks "
49+
"(e.g. ':class:`Foo`' becomes '`Foo`'). Offenders:\n" + "\n".join(offenders)
50+
)
51+
52+
53+
def test_guard_detects_a_role() -> None:
54+
"""The matcher itself works (so a green run means real coverage)."""
55+
assert _ROLE_RE.search(":meth:" + chr(96) + "Foo.bar" + chr(96)) is not None
56+
assert _ROLE_RE.search("plain `Foo` reference") is None

0 commit comments

Comments
 (0)