codmap 0.0.13__tar.gz → 0.0.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codmap-0.0.13/codmap.egg-info → codmap-0.0.15}/PKG-INFO +2 -2
- {codmap-0.0.13 → codmap-0.0.15}/README.md +1 -1
- {codmap-0.0.13 → codmap-0.0.15}/codemap/arch.py +25 -4
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/griffe_extractor.py +7 -5
- {codmap-0.0.13 → codmap-0.0.15}/codemap/query.py +33 -7
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/architecture.py +26 -8
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/audit.py +7 -5
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/check.py +36 -19
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/livingdocs.py +7 -3
- {codmap-0.0.13 → codmap-0.0.15/codmap.egg-info}/PKG-INFO +2 -2
- {codmap-0.0.13 → codmap-0.0.15}/codmap.egg-info/SOURCES.txt +1 -0
- {codmap-0.0.13 → codmap-0.0.15}/pyproject.toml +1 -1
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m16_architecture.py +3 -1
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m1_query.py +3 -1
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c29_lazy_imports.py +2 -2
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c30f2_check_scope.py +11 -3
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c48_type_checking_imports.py +14 -8
- codmap-0.0.15/tests/test_r1c49_type_only_cycles.py +233 -0
- {codmap-0.0.13 → codmap-0.0.15}/LICENSE +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/__init__.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/apidiff.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/cli.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/diagnostics.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/roots.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/extract/union.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/freshness.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/incremental.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/base.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/model.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/provenance.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/scope.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/impact.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/limits.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/pack.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/rag.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/review.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/scip.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/server.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/session.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/serve/vault.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/store.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/tomlio.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codemap/watch.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/setup.cfg +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_epistemic.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_integrations.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m15_review.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m2_views.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_m9_family.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_scip_export.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_scope.py +0 -0
- {codmap-0.0.13 → codmap-0.0.15}/tests/test_subsystems.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.15
|
|
4
4
|
Summary: Static analyzer that turns a package's source into a queryable code graph.
|
|
5
5
|
Author-email: kogriv <kogriv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,7 +85,7 @@ layers core 9 · data 12 · indicators 16 · analysis 42 · visualizatio
|
|
|
85
85
|
analysis → core 38 edges · indicators → core 22 · data → core 13
|
|
86
86
|
violation ⚠ analysis ↔ core — one backward edge, written inside a function
|
|
87
87
|
cycle pipeline → cache → pipeline — the classic Python import-order landmine
|
|
88
|
-
lazy cycles 40 more, closed only by a
|
|
88
|
+
lazy cycles 40 more, closed only by a function-local import — not import-time failures,
|
|
89
89
|
still mutual coupling: neither module can be extracted without the other
|
|
90
90
|
coupling core.logging_config Ca 96 — a breaking change here reaches 96 modules
|
|
91
91
|
concentration ZoneVisualizer 35 methods, worst function CC 66 / MI 12.5
|
|
@@ -49,7 +49,7 @@ layers core 9 · data 12 · indicators 16 · analysis 42 · visualizatio
|
|
|
49
49
|
analysis → core 38 edges · indicators → core 22 · data → core 13
|
|
50
50
|
violation ⚠ analysis ↔ core — one backward edge, written inside a function
|
|
51
51
|
cycle pipeline → cache → pipeline — the classic Python import-order landmine
|
|
52
|
-
lazy cycles 40 more, closed only by a
|
|
52
|
+
lazy cycles 40 more, closed only by a function-local import — not import-time failures,
|
|
53
53
|
still mutual coupling: neither module can be extracted without the other
|
|
54
54
|
coupling core.logging_config Ca 96 — a breaking change here reaches 96 modules
|
|
55
55
|
concentration ZoneVisualizer 35 methods, worst function CC 66 / MI 12.5
|
|
@@ -21,6 +21,7 @@ integration gate reads). All rules operate on the **core** module import graph
|
|
|
21
21
|
# also gate the coupling a lazy import hides: cycles closed only by an import
|
|
22
22
|
# written inside a function. Off by default — see below.
|
|
23
23
|
no_lazy_cycles = false
|
|
24
|
+
no_type_only_cycles = false
|
|
24
25
|
# every core module's layer must appear in `layers` (catches a new,
|
|
25
26
|
# undeclared top-level package slipping in).
|
|
26
27
|
exhaustive = false
|
|
@@ -54,6 +55,7 @@ class ArchitectureContract:
|
|
|
54
55
|
forbidden: tuple[tuple[str, str], ...] = ()
|
|
55
56
|
no_cycles: bool = False
|
|
56
57
|
no_lazy_cycles: bool = False
|
|
58
|
+
no_type_only_cycles: bool = False
|
|
57
59
|
exhaustive: bool = False
|
|
58
60
|
error: str | None = None
|
|
59
61
|
# R1-C35: the file this contract was looked for in. "No contract found" is only
|
|
@@ -63,7 +65,8 @@ class ArchitectureContract:
|
|
|
63
65
|
|
|
64
66
|
def is_empty(self) -> bool:
|
|
65
67
|
return not (self.layers or self.independent or self.forbidden
|
|
66
|
-
or self.no_cycles or self.no_lazy_cycles
|
|
68
|
+
or self.no_cycles or self.no_lazy_cycles
|
|
69
|
+
or self.no_type_only_cycles or self.exhaustive)
|
|
67
70
|
|
|
68
71
|
|
|
69
72
|
@dataclass(frozen=True)
|
|
@@ -71,7 +74,7 @@ class Violation:
|
|
|
71
74
|
"""One broken rule, with the concrete import edges (or cycle) that break it."""
|
|
72
75
|
|
|
73
76
|
rule: str # layered | independent | forbidden | no_cycles | no_lazy_cycles
|
|
74
|
-
# | exhaustive
|
|
77
|
+
# | no_type_only_cycles | exhaustive
|
|
75
78
|
summary: str # human one-liner
|
|
76
79
|
edges: tuple[tuple[str, str], ...] = field(default=()) # offending (importer, imported)
|
|
77
80
|
modules: tuple[str, ...] = field(default=()) # for exhaustive / cycles
|
|
@@ -112,6 +115,7 @@ def parse_contract(section: dict) -> ArchitectureContract:
|
|
|
112
115
|
forbidden=tuple(forbidden),
|
|
113
116
|
no_cycles=bool(section.get("no_cycles", False)),
|
|
114
117
|
no_lazy_cycles=bool(section.get("no_lazy_cycles", False)),
|
|
118
|
+
no_type_only_cycles=bool(section.get("no_type_only_cycles", False)),
|
|
115
119
|
exhaustive=bool(section.get("exhaustive", False)),
|
|
116
120
|
)
|
|
117
121
|
|
|
@@ -207,8 +211,25 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
|
|
|
207
211
|
worst = sorted(lazy, key=lambda c: (len(c), c))
|
|
208
212
|
violations.append(Violation(
|
|
209
213
|
"no_lazy_cycles",
|
|
210
|
-
f"{len(lazy)} dependency cycle(s) closed only by a
|
|
211
|
-
|
|
214
|
+
f"{len(lazy)} dependency cycle(s) closed only by a function-local import",
|
|
215
|
+
modules=tuple(" → ".join(c) + " → " + c[0] for c in worst),
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
# -- no_type_only_cycles: the third kind, and the one with no runtime dependency ----
|
|
219
|
+
# R1-C49 (issue #18): a cycle that needs an import under `if TYPE_CHECKING:` cannot
|
|
220
|
+
# break at import time and does not couple the modules at run time either — the two
|
|
221
|
+
# only name each other's types. Gating it is a style choice about the type layer, so
|
|
222
|
+
# it is opt-in and separate: the consumer who asked for this had `no_lazy_cycles` on
|
|
223
|
+
# against lazy imports used as a way around `no_cycles`, and the standard typing
|
|
224
|
+
# idiom is not that.
|
|
225
|
+
if contract.no_type_only_cycles:
|
|
226
|
+
type_only = query.type_only_import_cycles()
|
|
227
|
+
if type_only:
|
|
228
|
+
worst = sorted(type_only, key=lambda c: (len(c), c))
|
|
229
|
+
violations.append(Violation(
|
|
230
|
+
"no_type_only_cycles",
|
|
231
|
+
f"{len(type_only)} dependency cycle(s) closed only by an import under "
|
|
232
|
+
f"`if TYPE_CHECKING:`",
|
|
212
233
|
modules=tuple(" → ".join(c) + " → " + c[0] for c in worst),
|
|
213
234
|
))
|
|
214
235
|
|
|
@@ -481,11 +481,13 @@ def _emit_decorated_by(graph, obj) -> None:
|
|
|
481
481
|
|
|
482
482
|
# -- pass 2: resolve export + import edges against known nodes ----------------
|
|
483
483
|
|
|
484
|
-
#: How
|
|
485
|
-
#: R1-C48 D2). ``module`` runs at import time, ``
|
|
486
|
-
#:
|
|
487
|
-
#:
|
|
488
|
-
|
|
484
|
+
#: How strongly an import scope reaches its target: an edge carries the strongest (R1-C29
|
|
485
|
+
#: D2, R1-C48 D2). ``module`` runs at import time, ``function`` when the function runs,
|
|
486
|
+
#: ``type_checking`` never. A pair reached by both a function-local and a ``TYPE_CHECKING``
|
|
487
|
+
#: import is a **function-local** dependency: it can execute, and labelling it by the
|
|
488
|
+
#: import that cannot would let a tree launder runtime coupling into the type layer
|
|
489
|
+
#: (R1-C49, and the test that says so).
|
|
490
|
+
_SCOPE_RANK = {"module": 0, "function": 1, "type_checking": 2}
|
|
489
491
|
|
|
490
492
|
|
|
491
493
|
def _resolve_edges(graph, target_pkg, aliases, imports) -> None:
|
|
@@ -119,20 +119,27 @@ class Query:
|
|
|
119
119
|
# made an incomplete map read as a safety property.
|
|
120
120
|
self._imports = nx.DiGraph()
|
|
121
121
|
self._imports_eager = nx.DiGraph()
|
|
122
|
+
# R1-C49: between the two — everything that can execute. A `TYPE_CHECKING` import
|
|
123
|
+
# never does, so a cycle that needs one is not runtime coupling at all.
|
|
124
|
+
self._imports_runtime = nx.DiGraph()
|
|
122
125
|
self._import_scopes = {"module": 0, "function": 0, "type_checking": 0}
|
|
123
126
|
for n in graph.nodes.values():
|
|
124
127
|
if n.kind == "module":
|
|
125
128
|
self._imports.add_node(n.id)
|
|
126
129
|
self._imports_eager.add_node(n.id)
|
|
130
|
+
self._imports_runtime.add_node(n.id)
|
|
127
131
|
for e in graph.edges:
|
|
128
132
|
if e.type == "imports":
|
|
129
133
|
self._imports.add_edge(e.source, e.target)
|
|
130
134
|
scope = e.extras.get("scope")
|
|
131
135
|
if scope in ("function", "type_checking"):
|
|
132
136
|
# R1-C29: runs when the function runs; R1-C48: never runs. Neither
|
|
133
|
-
# is an import-time edge.
|
|
137
|
+
# is an import-time edge — but only the first is a runtime one.
|
|
134
138
|
self._import_scopes[scope] += 1
|
|
139
|
+
if scope == "function":
|
|
140
|
+
self._imports_runtime.add_edge(e.source, e.target)
|
|
135
141
|
else:
|
|
142
|
+
self._imports_runtime.add_edge(e.source, e.target)
|
|
136
143
|
self._imports_eager.add_edge(e.source, e.target)
|
|
137
144
|
self._import_scopes["module"] += 1
|
|
138
145
|
# export edges: name -> [target definition paths]
|
|
@@ -888,16 +895,35 @@ class Query:
|
|
|
888
895
|
return [c for c in nx.simple_cycles(self._imports_eager)]
|
|
889
896
|
|
|
890
897
|
def lazy_import_cycles(self) -> list[list[str]]:
|
|
891
|
-
"""Dependency cycles that close **only** through a
|
|
892
|
-
written inside a function (R1-C29) or under ``if TYPE_CHECKING:`` (R1-C48).
|
|
898
|
+
"""Dependency cycles that close **only** through a function-local import.
|
|
893
899
|
|
|
894
900
|
Not an import-time failure, and not nothing: the modules still cannot be
|
|
895
|
-
separated, and the lazy import is the evidence someone already hit
|
|
896
|
-
R1-C29 these were invisible — on the target this project benchmarks
|
|
897
|
-
the nine cycles present were of this kind and the report said "1".
|
|
901
|
+
separated at run time, and the lazy import is the evidence someone already hit
|
|
902
|
+
this. Before R1-C29 these were invisible — on the target this project benchmarks
|
|
903
|
+
on, eight of the nine cycles present were of this kind and the report said "1".
|
|
904
|
+
|
|
905
|
+
R1-C49 narrowed this back after R1-C48 briefly widened it: a cycle that needs an
|
|
906
|
+
import under ``if TYPE_CHECKING:`` is not runtime coupling at all and is returned
|
|
907
|
+
by :meth:`type_only_import_cycles`. The partition is by the **weakest scope that
|
|
908
|
+
closes the cycle**, not by which scopes appear in it — a pair coupled through a
|
|
909
|
+
function-local import stays *lazy* however many type imports also run between
|
|
910
|
+
them, or a tree could hide real coupling by adding one.
|
|
898
911
|
"""
|
|
899
912
|
eager = {frozenset(c) for c in nx.simple_cycles(self._imports_eager)}
|
|
900
|
-
return [c for c in nx.simple_cycles(self.
|
|
913
|
+
return [c for c in nx.simple_cycles(self._imports_runtime)
|
|
914
|
+
if frozenset(c) not in eager]
|
|
915
|
+
|
|
916
|
+
def type_only_import_cycles(self) -> list[list[str]]:
|
|
917
|
+
"""Dependency cycles that close **only** with an import under ``if TYPE_CHECKING:``.
|
|
918
|
+
|
|
919
|
+
The third kind (R1-C49, issue #18). These modules name each other's types and have
|
|
920
|
+
**no runtime dependency whatever**: neither import pulls the other at any moment of
|
|
921
|
+
execution. That is why they are not gated by ``no_lazy_cycles`` — which exists
|
|
922
|
+
against a lazy import used to walk *around* ``no_cycles`` — and get their own
|
|
923
|
+
opt-in rule instead.
|
|
924
|
+
"""
|
|
925
|
+
runtime = {frozenset(c) for c in nx.simple_cycles(self._imports_runtime)}
|
|
926
|
+
return [c for c in nx.simple_cycles(self._imports) if frozenset(c) not in runtime]
|
|
901
927
|
|
|
902
928
|
def import_map(self) -> dict:
|
|
903
929
|
"""How much of the import graph each scope contributed (R1-C29).
|
|
@@ -18,9 +18,10 @@ from codemap.query import Query
|
|
|
18
18
|
def build_architecture(query: Query) -> dict:
|
|
19
19
|
"""Structured whole-system overview (cycles + layers + coupling + hotspots).
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
``if TYPE_CHECKING:`` (
|
|
21
|
+
Three kinds, by the weakest scope that closes the cycle (R1-C49): ``cycles`` break at
|
|
22
|
+
import time, ``lazy_cycles`` need a function-local import (runtime coupling), and
|
|
23
|
+
``type_only_cycles`` need one under ``if TYPE_CHECKING:`` (no runtime dependency at
|
|
24
|
+
all). Splitting them is the point — a lazy
|
|
24
25
|
import is how a developer *fixes* an import cycle, so folding the two together would
|
|
25
26
|
report someone's fix as their bug, while dropping the second (what this tool did
|
|
26
27
|
until issue #11) hides that the modules are still inseparable. ``import_map`` is
|
|
@@ -31,6 +32,7 @@ def build_architecture(query: Query) -> dict:
|
|
|
31
32
|
"target": query.graph.target,
|
|
32
33
|
"cycles": query.import_cycles(),
|
|
33
34
|
"lazy_cycles": query.lazy_import_cycles(),
|
|
35
|
+
"type_only_cycles": query.type_only_import_cycles(),
|
|
34
36
|
"import_map": query.import_map(),
|
|
35
37
|
"layers": query.layers(),
|
|
36
38
|
"coupling": query.coupling(),
|
|
@@ -85,19 +87,35 @@ def render_architecture(query: Query) -> str:
|
|
|
85
87
|
f"an import under `if TYPE_CHECKING:` never runs._")
|
|
86
88
|
out.append("")
|
|
87
89
|
if a["lazy_cycles"]:
|
|
88
|
-
out.append(f"### Dependency cycles closed only by a
|
|
89
|
-
f"
|
|
90
|
+
out.append(f"### Dependency cycles closed only by a function-local import: "
|
|
91
|
+
f"{len(a['lazy_cycles'])}")
|
|
90
92
|
out.append("")
|
|
91
|
-
out.append("_These do **not** break at import time — the
|
|
93
|
+
out.append("_These do **not** break at import time — the lazy import is what "
|
|
92
94
|
"prevents that, and is usually deliberate. They are listed because "
|
|
93
|
-
"the modules are still mutually dependent: neither can be
|
|
94
|
-
"without the other._")
|
|
95
|
+
"the modules are still mutually dependent at run time: neither can be "
|
|
96
|
+
"extracted without the other._")
|
|
95
97
|
out.append("")
|
|
96
98
|
out.extend(f"- {' → '.join(c)} → {c[0]}" for c in
|
|
97
99
|
sorted(a["lazy_cycles"], key=lambda c: (len(c), c))[:20])
|
|
98
100
|
if len(a["lazy_cycles"]) > 20:
|
|
99
101
|
out.append(f"- _… {len(a['lazy_cycles']) - 20} more_")
|
|
100
102
|
out.append("")
|
|
103
|
+
if a["type_only_cycles"]:
|
|
104
|
+
# R1-C49: the third kind, kept apart from the second because the difference is the
|
|
105
|
+
# whole point — these modules have no runtime dependency on each other at all.
|
|
106
|
+
out.append(f"### Dependency cycles closed only by an import under "
|
|
107
|
+
f"`if TYPE_CHECKING:`: {len(a['type_only_cycles'])}")
|
|
108
|
+
out.append("")
|
|
109
|
+
out.append("_Neither module pulls the other at any moment of execution — they name "
|
|
110
|
+
"each other's types. Not an import-time failure and not runtime coupling; "
|
|
111
|
+
"`no_type_only_cycles = true` gates them if the type layer must not close "
|
|
112
|
+
"a cycle either._")
|
|
113
|
+
out.append("")
|
|
114
|
+
out.extend(f"- {' → '.join(c)} → {c[0]}" for c in
|
|
115
|
+
sorted(a["type_only_cycles"], key=lambda c: (len(c), c))[:20])
|
|
116
|
+
if len(a["type_only_cycles"]) > 20:
|
|
117
|
+
out.append(f"- _… {len(a['type_only_cycles']) - 20} more_")
|
|
118
|
+
out.append("")
|
|
101
119
|
|
|
102
120
|
# -- coupling -----------------------------------------------------------
|
|
103
121
|
out.append("## Coupling (top by afferent Ca)")
|
|
@@ -18,6 +18,7 @@ def render_dependencies(query: Query) -> str:
|
|
|
18
18
|
|
|
19
19
|
cycles = query.import_cycles()
|
|
20
20
|
lazy = query.lazy_import_cycles()
|
|
21
|
+
type_only = query.type_only_import_cycles()
|
|
21
22
|
im = query.import_map()
|
|
22
23
|
lines.append(f"## Import cycles: {len(cycles)}")
|
|
23
24
|
lines.append("")
|
|
@@ -31,11 +32,10 @@ def render_dependencies(query: Query) -> str:
|
|
|
31
32
|
lines.append("")
|
|
32
33
|
lines.append(f"_Read {im['module_level']} module-level, {im['function_local']} "
|
|
33
34
|
f"function-local and {im['type_checking']} `TYPE_CHECKING` import(s); only "
|
|
34
|
-
f"the first run at import time. "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
else "No cycle closes through a non-eager import._"))
|
|
35
|
+
f"the first run at import time. {len(lazy)} further cycle(s) close through "
|
|
36
|
+
f"a function-local import (runtime coupling, not an import-time failure) "
|
|
37
|
+
f"and {len(type_only)} through an import under `if TYPE_CHECKING:` (no "
|
|
38
|
+
f"runtime dependency at all)._")
|
|
39
39
|
lines.append("")
|
|
40
40
|
|
|
41
41
|
lines.append("## Most-depended-on modules (top 15)")
|
|
@@ -63,6 +63,8 @@ def build_dependencies(query: Query) -> dict:
|
|
|
63
63
|
"import_cycles": [list(c) for c in sorted(query.import_cycles(), key=lambda c: (len(c), c))],
|
|
64
64
|
"lazy_import_cycles": [list(c) for c in
|
|
65
65
|
sorted(query.lazy_import_cycles(), key=lambda c: (len(c), c))],
|
|
66
|
+
"type_only_import_cycles": [list(c) for c in
|
|
67
|
+
sorted(query.type_only_import_cycles(), key=lambda c: (len(c), c))],
|
|
66
68
|
"most_depended_on": [{"module": m, "imported_by": g.in_degree(m)}
|
|
67
69
|
for m in ranked[:15] if g.in_degree(m)],
|
|
68
70
|
"diagnostics": diagnostics(query.graph),
|
|
@@ -27,18 +27,29 @@ def _not_judged(query, contract: ArchitectureContract) -> list[dict]:
|
|
|
27
27
|
a field that appears only when there is something to say forces a machine consumer to
|
|
28
28
|
tell "nothing was skipped" from "this build does not report skips", and it cannot.
|
|
29
29
|
"""
|
|
30
|
-
if not contract.no_cycles
|
|
31
|
-
return []
|
|
32
|
-
|
|
30
|
+
if not contract.no_cycles:
|
|
31
|
+
return [] # not enforced
|
|
32
|
+
if contract.no_lazy_cycles and contract.no_type_only_cycles:
|
|
33
|
+
return [] # the contract judges all three kinds
|
|
34
|
+
# R1-C49: two kinds may be left out, and they are different things. Whichever of them
|
|
35
|
+
# this contract does not gate is what the reader must be told about — reporting one
|
|
36
|
+
# sum over both is the defect the third consumer filed.
|
|
37
|
+
lazy = [] if contract.no_lazy_cycles else query.lazy_import_cycles()
|
|
38
|
+
type_only = [] if contract.no_type_only_cycles else query.type_only_import_cycles()
|
|
33
39
|
return [{
|
|
34
40
|
"rule": "no_cycles",
|
|
35
41
|
"judged": "the eager import graph — imports that run at import time",
|
|
36
|
-
"not_judged": "dependency cycles
|
|
42
|
+
"not_judged": "dependency cycles that do not close at import time",
|
|
43
|
+
"lazy": len(lazy),
|
|
44
|
+
"type_only": len(type_only),
|
|
45
|
+
"lazy_gated": contract.no_lazy_cycles,
|
|
46
|
+
"type_only_gated": contract.no_type_only_cycles,
|
|
37
47
|
"type_checking_imports": query.import_map()["type_checking"],
|
|
38
|
-
"count": len(lazy),
|
|
39
|
-
"note": ("a
|
|
40
|
-
"
|
|
41
|
-
"
|
|
48
|
+
"count": len(lazy) + len(type_only),
|
|
49
|
+
"note": ("`lazy` needs a function-local import — runtime coupling, gated by "
|
|
50
|
+
"`no_lazy_cycles = true`. `type_only` needs an import under "
|
|
51
|
+
"`if TYPE_CHECKING:` — no runtime dependency at all, gated by "
|
|
52
|
+
"`no_type_only_cycles = true`. `report architecture` lists both."),
|
|
42
53
|
}]
|
|
43
54
|
|
|
44
55
|
|
|
@@ -93,6 +104,10 @@ def render_check(query, contract: ArchitectureContract, violations: list[Violati
|
|
|
93
104
|
"and this exits 0. Use `--require-contract` to make a missing contract a "
|
|
94
105
|
"failure, or `--root DIR` if the file lives elsewhere._\n")
|
|
95
106
|
if not violations:
|
|
107
|
+
# R1-C49-f1 (consumer, issue #18): every enforced rule must name itself here. A
|
|
108
|
+
# rule that ran and stayed silent is the R1-C30-f2 defect from the other side —
|
|
109
|
+
# there the reader concluded more than was checked, here less. `tests/
|
|
110
|
+
# test_r1c49_type_only_cycles.py` fails if a new rule is added and not listed.
|
|
96
111
|
rules = []
|
|
97
112
|
if contract.layers:
|
|
98
113
|
rules.append(f"layered ({len(contract.layers)})")
|
|
@@ -104,6 +119,8 @@ def render_check(query, contract: ArchitectureContract, violations: list[Violati
|
|
|
104
119
|
rules.append("no_cycles")
|
|
105
120
|
if contract.no_lazy_cycles:
|
|
106
121
|
rules.append("no_lazy_cycles")
|
|
122
|
+
if contract.no_type_only_cycles:
|
|
123
|
+
rules.append("no_type_only_cycles")
|
|
107
124
|
if contract.exhaustive:
|
|
108
125
|
rules.append("exhaustive")
|
|
109
126
|
return (f"# Architecture check — `{target}`\n\n"
|
|
@@ -132,15 +149,15 @@ def _render_scope(query, contract: ArchitectureContract) -> str:
|
|
|
132
149
|
if not scope:
|
|
133
150
|
return ""
|
|
134
151
|
s = scope[0]
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
parts = []
|
|
153
|
+
if not s["lazy_gated"]:
|
|
154
|
+
parts.append(f"**{s['lazy']}** cycle(s) closed only by a function-local import "
|
|
155
|
+
f"(runtime coupling — `no_lazy_cycles = true` gates them)")
|
|
156
|
+
if not s["type_only_gated"]:
|
|
157
|
+
parts.append(f"**{s['type_only']}** closed only by an import under "
|
|
158
|
+
f"`if TYPE_CHECKING:` (no runtime dependency at all — "
|
|
159
|
+
f"`no_type_only_cycles = true` gates them)")
|
|
140
160
|
return (f"\n_`no_cycles` judged **the eager import graph only** — imports that run at "
|
|
141
|
-
f"import time.
|
|
142
|
-
f"
|
|
143
|
-
f"
|
|
144
|
-
f"`report architecture` lists them; `no_lazy_cycles = true` gates them. "
|
|
145
|
-
f"{s['type_checking_imports']} import(s) under `TYPE_CHECKING` read as never "
|
|
146
|
-
f"running._\n")
|
|
161
|
+
f"import time. Not judged: {' and '.join(parts)}. `report architecture` lists "
|
|
162
|
+
f"them. {s['type_checking_imports']} import(s) under `TYPE_CHECKING` read as "
|
|
163
|
+
f"never running._\n")
|
|
@@ -144,6 +144,7 @@ def render_docs(query: Query) -> str:
|
|
|
144
144
|
# -- architecture caveats (the honest health section) -------------------
|
|
145
145
|
cycles = query.import_cycles()
|
|
146
146
|
lazy = query.lazy_import_cycles()
|
|
147
|
+
type_only = query.type_only_import_cycles()
|
|
147
148
|
lay = query.layers()
|
|
148
149
|
gods = query.hotspots()["god_classes"]
|
|
149
150
|
out.append("## Architecture notes")
|
|
@@ -156,9 +157,12 @@ def render_docs(query: Query) -> str:
|
|
|
156
157
|
# proof of acyclicity, which this map cannot support.
|
|
157
158
|
out.append("- No import cycle found in the eager import graph.")
|
|
158
159
|
if lazy:
|
|
159
|
-
out.append(f"- **{len(lazy)} dependency cycle(s) closed only by a
|
|
160
|
-
f"import**
|
|
161
|
-
|
|
160
|
+
out.append(f"- **{len(lazy)} dependency cycle(s) closed only by a function-local "
|
|
161
|
+
f"import** — deliberate, and still runtime coupling.")
|
|
162
|
+
if type_only:
|
|
163
|
+
out.append(f"- **{len(type_only)} dependency cycle(s) closed only by an import under "
|
|
164
|
+
f"`if TYPE_CHECKING:`** — the modules name each other's types and have no "
|
|
165
|
+
f"runtime dependency.")
|
|
162
166
|
if lay["violations"]:
|
|
163
167
|
out.append("- **Layer violations (mutual dependency):** "
|
|
164
168
|
+ ", ".join(f"{a} ↔ {b}" for a, b in lay["violations"]))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.15
|
|
4
4
|
Summary: Static analyzer that turns a package's source into a queryable code graph.
|
|
5
5
|
Author-email: kogriv <kogriv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,7 +85,7 @@ layers core 9 · data 12 · indicators 16 · analysis 42 · visualizatio
|
|
|
85
85
|
analysis → core 38 edges · indicators → core 22 · data → core 13
|
|
86
86
|
violation ⚠ analysis ↔ core — one backward edge, written inside a function
|
|
87
87
|
cycle pipeline → cache → pipeline — the classic Python import-order landmine
|
|
88
|
-
lazy cycles 40 more, closed only by a
|
|
88
|
+
lazy cycles 40 more, closed only by a function-local import — not import-time failures,
|
|
89
89
|
still mutual coupling: neither module can be extracted without the other
|
|
90
90
|
coupling core.logging_config Ca 96 — a breaking change here reaches 96 modules
|
|
91
91
|
concentration ZoneVisualizer 35 methods, worst function CC 66 / MI 12.5
|
|
@@ -117,6 +117,7 @@ tests/test_r1c45_repeat_union.py
|
|
|
117
117
|
tests/test_r1c46_shadowed_definitions.py
|
|
118
118
|
tests/test_r1c47_incremental_resample.py
|
|
119
119
|
tests/test_r1c48_type_checking_imports.py
|
|
120
|
+
tests/test_r1c49_type_only_cycles.py
|
|
120
121
|
tests/test_r1c4_complexity.py
|
|
121
122
|
tests/test_r1c5_apidiff.py
|
|
122
123
|
tests/test_r1c6_pack.py
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
# version. README says this plainly, since `pip install codmap` / `import codemap` is a
|
|
10
10
|
# seam a reader will otherwise hit by surprise.
|
|
11
11
|
name = "codmap"
|
|
12
|
-
version = "0.0.
|
|
12
|
+
version = "0.0.15"
|
|
13
13
|
description = "Static analyzer that turns a package's source into a queryable code graph."
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
# M20/D1: measured, not assumed. Each release of the declared range runs the full suite in
|
|
@@ -77,8 +77,10 @@ def test_build_architecture_has_all_sections(q):
|
|
|
77
77
|
a = build_architecture(q)
|
|
78
78
|
# R1-C29 added two: `lazy_cycles` (dependency cycles closed only by a function-local
|
|
79
79
|
# import — real coupling, not an import-time failure) and `import_map` (how many
|
|
80
|
+
# imports each scope contributed). R1-C49 added `type_only_cycles`, the third kind —
|
|
81
|
+
# closed only by an import under `if TYPE_CHECKING:`, i.e. no runtime dependency at all —
|
|
80
82
|
# imports of each scope the map was built from, always, zero included).
|
|
81
|
-
assert set(a) == {"target", "cycles", "lazy_cycles", "import_map",
|
|
83
|
+
assert set(a) == {"target", "cycles", "lazy_cycles", "type_only_cycles", "import_map",
|
|
82
84
|
"layers", "coupling", "hotspots"}
|
|
83
85
|
assert a["target"] == "dispatchpkg"
|
|
84
86
|
|
|
@@ -70,7 +70,9 @@ def test_cycle_detection(q):
|
|
|
70
70
|
pair = frozenset({PIPELINE, "bquant.analysis.zones.cache"})
|
|
71
71
|
assert pair not in {frozenset(c) for c in q.import_cycles()}, \
|
|
72
72
|
"an import under TYPE_CHECKING must not close an eager cycle"
|
|
73
|
-
assert pair in {frozenset(c) for c in q.lazy_import_cycles()}
|
|
73
|
+
assert pair not in {frozenset(c) for c in q.lazy_import_cycles()}, \
|
|
74
|
+
"nor a runtime one: R1-C49, the pair has no runtime dependency at all"
|
|
75
|
+
assert pair in {frozenset(c) for c in q.type_only_import_cycles()}
|
|
74
76
|
assert q.import_map()["type_checking"] >= 1
|
|
75
77
|
|
|
76
78
|
|
|
@@ -163,13 +163,13 @@ def test_architecture_never_claims_acyclic(lazy_cycle):
|
|
|
163
163
|
assert "acyclic" not in md, "a partial map cannot support a property claim"
|
|
164
164
|
assert "none found in the eager import graph" in md
|
|
165
165
|
assert "function-local and 0 `TYPE_CHECKING` import(s)" in md
|
|
166
|
-
assert "Dependency cycles closed only by a
|
|
166
|
+
assert "Dependency cycles closed only by a function-local import: 1" in md
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
def test_dependencies_report_never_claims_acyclic(lazy_cycle):
|
|
170
170
|
md = render_dependencies(Query(extract(str(lazy_cycle))))
|
|
171
171
|
assert "acyclic" not in md
|
|
172
|
-
assert "close through a
|
|
172
|
+
assert "close through a function-local import" in md
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
def test_architecture_payload_carries_both_kinds_and_the_counts(lazy_cycle):
|
|
@@ -75,7 +75,7 @@ def test_the_passing_report_names_what_was_not_judged(lazy_cycle):
|
|
|
75
75
|
out = render_check(lazy_cycle, contract, violations)
|
|
76
76
|
assert "Contract satisfied" in out
|
|
77
77
|
assert "eager import graph only" in out
|
|
78
|
-
assert "**1**
|
|
78
|
+
assert "**1** cycle(s) closed only by a function-local import" in out
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def test_the_structured_payload_carries_it_too(lazy_cycle):
|
|
@@ -92,8 +92,9 @@ def test_it_is_declared_even_when_there_is_nothing_to_declare(eager_only):
|
|
|
92
92
|
contract, violations = _run(eager_only, {"no_cycles": True})
|
|
93
93
|
scope = build_check(eager_only, contract, violations)["scope"]
|
|
94
94
|
assert len(scope) == 1 and scope[0]["count"] == 0
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
md = render_check(eager_only, contract, violations)
|
|
96
|
+
assert "**0** cycle(s) closed only by a function-local import" in md
|
|
97
|
+
assert "**0** closed only by an import under `if TYPE_CHECKING:`" in md
|
|
97
98
|
|
|
98
99
|
|
|
99
100
|
def test_a_failing_run_carries_the_same_disclosure(lazy_cycle):
|
|
@@ -129,7 +130,14 @@ def test_the_opt_in_alone_is_a_contract(lazy_cycle):
|
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
def test_opting_in_removes_the_disclaimer_because_nothing_is_left_out(lazy_cycle):
|
|
133
|
+
"""R1-C49: there are three kinds now, so "nothing is left out" needs all three rules.
|
|
134
|
+
With only `no_lazy_cycles` added, the type-only cycles are still unjudged and the
|
|
135
|
+
disclosure must keep saying so — a gated kind drops out of it, the others do not."""
|
|
132
136
|
contract, violations = _run(lazy_cycle, {"no_cycles": True, "no_lazy_cycles": True})
|
|
137
|
+
scope = build_check(lazy_cycle, contract, violations)["scope"]
|
|
138
|
+
assert len(scope) == 1 and scope[0]["lazy_gated"] is True and scope[0]["lazy"] == 0
|
|
139
|
+
contract, violations = _run(lazy_cycle, {"no_cycles": True, "no_lazy_cycles": True,
|
|
140
|
+
"no_type_only_cycles": True})
|
|
133
141
|
assert build_check(lazy_cycle, contract, violations)["scope"] == []
|
|
134
142
|
assert "not** judged" not in render_check(lazy_cycle, contract, violations)
|
|
135
143
|
|
|
@@ -127,10 +127,13 @@ def test_a_pair_imported_both_ways_reads_eager(tree):
|
|
|
127
127
|
|
|
128
128
|
# -- D4 / D5: the graphs and the map ----------------------------------------------------
|
|
129
129
|
|
|
130
|
-
def
|
|
130
|
+
def test_the_cycle_is_not_eager(tree):
|
|
131
|
+
"""R1-C49 moved it one further: not eager (this fix), and not lazy either — the pair
|
|
132
|
+
has no runtime dependency at all, so it is the third kind."""
|
|
131
133
|
q = tree[2]
|
|
132
134
|
assert q.import_cycles() == [], "the gate's defect: this used to be the one eager cycle"
|
|
133
|
-
assert
|
|
135
|
+
assert q.lazy_import_cycles() == []
|
|
136
|
+
assert [sorted(c) for c in q.type_only_import_cycles()] == [["tcpkg.cache", "tcpkg.pipeline"]]
|
|
134
137
|
|
|
135
138
|
|
|
136
139
|
def test_import_map_names_the_scope_always(tree):
|
|
@@ -148,9 +151,9 @@ def test_every_consumer_says_which_imports_it_did_not_judge(tree):
|
|
|
148
151
|
assert a["import_map"]["type_checking"] == 3
|
|
149
152
|
md = render_architecture(q)
|
|
150
153
|
assert "3 `TYPE_CHECKING` import(s)" in md
|
|
151
|
-
assert "closed only by
|
|
154
|
+
assert "closed only by an import under `if TYPE_CHECKING:`: 1" in md
|
|
152
155
|
assert "`TYPE_CHECKING` import(s)" in render_dependencies(q)
|
|
153
|
-
assert "
|
|
156
|
+
assert "closed only by an import under `if TYPE_CHECKING:`" in render_docs(q)
|
|
154
157
|
|
|
155
158
|
|
|
156
159
|
def test_the_gate_is_green_and_names_what_it_read(tree):
|
|
@@ -161,15 +164,18 @@ def test_the_gate_is_green_and_names_what_it_read(tree):
|
|
|
161
164
|
payload = build_check(q, contract, violations)
|
|
162
165
|
assert payload["ok"] is True
|
|
163
166
|
assert payload["scope"][0]["count"] == 1
|
|
167
|
+
assert payload["scope"][0]["type_only"] == 1 and payload["scope"][0]["lazy"] == 0
|
|
164
168
|
assert payload["scope"][0]["type_checking_imports"] == 3
|
|
165
169
|
md = render_check(q, contract, violations)
|
|
166
170
|
assert "3 import(s) under `TYPE_CHECKING` read as never running" in md
|
|
167
171
|
|
|
168
172
|
|
|
169
|
-
def
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
assert [
|
|
173
|
+
def test_the_type_only_rule_gates_the_coupling(tree):
|
|
174
|
+
"""R1-C49: `no_lazy_cycles` is about a lazy import used to walk around `no_cycles`,
|
|
175
|
+
which this is not; the opt-in rule for the type layer is the one that fires."""
|
|
176
|
+
assert arch.check_contract(tree[2], arch.ArchitectureContract(no_lazy_cycles=True)) == []
|
|
177
|
+
v = arch.check_contract(tree[2], arch.ArchitectureContract(no_type_only_cycles=True))
|
|
178
|
+
assert [x.rule for x in v] == ["no_type_only_cycles"]
|
|
173
179
|
assert "under `if TYPE_CHECKING:`" in v[0].summary
|
|
174
180
|
|
|
175
181
|
|