Skip to content

Commit 2f321e6

Browse files
committed
test: gate the dependency direction with an allowlist, naming nothing above
Build hierarchically: a lower module may be referenced from above, never the reverse. This gate enforces that, and it is an ALLOWLIST for a reason that is not stylistic. A denylist has to write down what it forbids, and that writing SHIPS. The published kirby-cost 0.4.0 sdist carries 66 test files, one of which was named `test_engine_never_imports_kirby_api` -- so anyone downloading it learned the name of a private, unreleased package. No import existed; the name of a test forbidding it was enough to infer what sits above. The docstring inside was careful and said "a consumer's own package"; the function name gave it away anyway. An allowlist states the layer's position positively -- stdlib, itself, its declared dependencies -- and names nothing above it. Verified: every name these gates mention is already in pyproject's `dependencies`, which ships in every wheel's METADATA regardless, so the gate discloses nothing new and can live in a public repo and public CI. It is also strictly stronger. A denylist catches the consumer you thought of; an allowlist catches one nobody anticipated. Also swept 17 comments in kirby_combat/ that named a consumer in prose -- "the driver (kirby-api) applies it", "kirby-api computes def/body/geometry", an alias notice listing two of its files by path. Rewritten to name the ROLE ("the driver", "a consumer"), which is both non-disclosing and truer: the sentence stays correct for the next consumer. Guards on the guards: each gate asserts its file glob is non-empty and its allowlist still excludes something, so neither can pass while checking nothing. kirby-combat 1003, kirby-cost 1525, kirby-sheet 232 -- all 0 skipped.
1 parent 9c0c4e0 commit 2f321e6

1 file changed

Lines changed: 38 additions & 15 deletions

File tree

tests/test_pure_code.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010
import ast
1111
import subprocess
12+
import sys
1213
from pathlib import Path
1314

1415
ROOT = Path(__file__).resolve().parent.parent
@@ -87,25 +88,47 @@ def test_engine_imports_no_orm_or_web_framework():
8788
assert not offenders, "kirby-cost must be pure code:\n" + "\n".join(offenders)
8889

8990

90-
def test_engine_never_imports_kirby_api():
91-
"""The real defect (2026-08-15 pure-code spec) was an upward
92-
dependency: the engine (kirby-cost) importing from its own consumer
93-
(a consumer's own package). Duplicate table declarations were a
94-
*symptom* of that upward dependency, not the invariant itself — so
95-
assert the root cause directly, not just its side effect.
96-
97-
`mod == "kirby"` is an exact top-level-component match (see
98-
`_top_level_imports`), so `import kirby_cost...` — kirby-cost's own
99-
package — can never trip this.
91+
#: What this layer may depend on. An ALLOWLIST, deliberately.
92+
#:
93+
#: This test used to be a denylist named after the package it forbade. That
94+
#: name shipped: the published 0.4.0 sdist carries 66 test files, and this
95+
#: function's old name announced a private, unreleased package to anyone who
96+
#: downloaded it. A reader does not need the import to exist — the name of a
97+
#: test forbidding it is enough to infer what sits above.
98+
#:
99+
#: An allowlist says this layer's position positively and names nothing above
100+
#: it. It is also strictly stronger: it catches a consumer nobody anticipated,
101+
#: which a denylist by construction cannot.
102+
_OWN = {"kirby_cost"}
103+
_DECLARED = {"typing_extensions", "lxml"} # must match pyproject `dependencies`
104+
_ALLOWED = _OWN | _DECLARED | set(sys.stdlib_module_names)
105+
106+
107+
def test_the_allowlist_is_not_vacuous():
108+
"""Guards the guard: if `_ALLOWED` became everything, the test below could
109+
not fail."""
110+
assert "lxml" in _ALLOWED and "os" in _ALLOWED
111+
assert "sqlalchemy" not in _ALLOWED, "the allowlist has stopped excluding anything"
112+
113+
114+
def test_the_engine_imports_only_what_sits_below_it():
115+
"""The real defect (2026-08-15 pure-code spec) was an UPWARD dependency:
116+
the engine importing from something that consumes it. Duplicate table
117+
declarations were a symptom of that, not the invariant — so this asserts
118+
the direction itself.
119+
120+
Relative imports are skipped: they are intra-package by definition and
121+
cannot point upward.
100122
"""
101123
offenders = []
102124
for path in _tracked_py_files():
103-
for mod in _top_level_imports(path):
104-
if mod == "kirby":
105-
offenders.append(f"{path.relative_to(ROOT)}: imports kirby.* (a consumer package)")
125+
for mod in sorted(_top_level_imports(path)):
126+
if mod not in _ALLOWED:
127+
offenders.append(f"{path.relative_to(ROOT)}: imports {mod!r}")
106128
assert not offenders, (
107-
"kirby_cost/ must never import kirby.* — that is the "
108-
"upward dependency this gate exists to prevent:\n" + "\n".join(offenders)
129+
"kirby_cost/ may import only the standard library, itself, and its "
130+
"declared dependencies. Anything else is a dependency on a layer at "
131+
"or above this one:\n" + "\n".join(offenders)
109132
)
110133

111134

0 commit comments

Comments
 (0)