codmap 0.0.18__tar.gz → 0.0.19__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.18 → codmap-0.0.19}/PKG-INFO +2 -2
- {codmap-0.0.18 → codmap-0.0.19}/README.md +1 -1
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/griffe_extractor.py +85 -7
- {codmap-0.0.18 → codmap-0.0.19}/codemap/query.py +63 -6
- {codmap-0.0.18 → codmap-0.0.19}/codemap/scope.py +4 -1
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/api_surface.py +45 -2
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/architecture.py +6 -5
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/audit.py +5 -5
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/check.py +4 -3
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/livingdocs.py +3 -3
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/session.py +5 -0
- {codmap-0.0.18 → codmap-0.0.19}/codmap.egg-info/PKG-INFO +2 -2
- {codmap-0.0.18 → codmap-0.0.19}/codmap.egg-info/SOURCES.txt +3 -0
- {codmap-0.0.18 → codmap-0.0.19}/pyproject.toml +1 -1
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m1_query.py +16 -4
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c29_lazy_imports.py +3 -3
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c48_type_checking_imports.py +5 -5
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c49_type_only_cycles.py +5 -4
- codmap-0.0.19/tests/test_r1c55_override_grade.py +118 -0
- codmap-0.0.19/tests/test_r1c56_stub_files.py +166 -0
- codmap-0.0.19/tests/test_r1c57_facade_package.py +136 -0
- {codmap-0.0.18 → codmap-0.0.19}/LICENSE +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/__init__.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/apidiff.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/arch.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/cli.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/diagnostics.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/roots.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/extract/union.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/freshness.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/incremental.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/base.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/model.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/provenance.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/impact.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/limits.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/pack.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/rag.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/review.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/scip.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/server.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/serve/vault.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/store.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/tomlio.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codemap/watch.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/setup.cfg +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_epistemic.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_integrations.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m15_review.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m2_views.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_m9_family.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c39_edge_resolution.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c40_flow_reach.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c50_flow_entry_points.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c53_narrowing_declared.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c54_deterministic_rendering.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_scip_export.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/tests/test_scope.py +0 -0
- {codmap-0.0.18 → codmap-0.0.19}/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.19
|
|
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
|
|
@@ -44,7 +44,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
44
44
|
|
|
45
45
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **961 tests with no failures on
|
|
48
48
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
49
49
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
50
50
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -8,7 +8,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
10
10
|
|
|
11
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
11
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **961 tests with no failures on
|
|
12
12
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
13
13
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
14
14
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -80,6 +80,63 @@ def _assert_is_the_target(loaded, pkg_dir: Path, module_name: str) -> None:
|
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
#: How many sibling packages one load may pull in before we stop trying (R1-C57).
|
|
84
|
+
_MAX_SIBLING_LOADS = 8
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_with_siblings(module_name: str, search_path: Path):
|
|
88
|
+
"""``griffe.load`` the target, pulling in siblings its re-exports point at (R1-C57).
|
|
89
|
+
|
|
90
|
+
The facade shape — a public package whose whole job is to re-export a private one,
|
|
91
|
+
as ``pytest``/``_pytest`` and ``attrs``/``attr`` do — used to **crash the build**:
|
|
92
|
+
griffe merges a ``.pyi`` beside ``__init__.py``, that merge resolves aliases, and an
|
|
93
|
+
alias into a package nobody loaded raises ``AliasResolutionError``. Exit code 1, no
|
|
94
|
+
graph, for a six-file package that is valid and published.
|
|
95
|
+
|
|
96
|
+
An unresolvable re-export is partiality, not a crash. So: load the sibling the alias
|
|
97
|
+
names into the *same* collection and retry. The sibling has to be there — it sits on
|
|
98
|
+
the same search path, which is how Python finds it too — and loading it does not put
|
|
99
|
+
it in the graph: the walk starts from the target's own root, and targets outside it
|
|
100
|
+
stay external, exactly as they were before.
|
|
101
|
+
|
|
102
|
+
Returns ``(root, siblings)``; ``siblings`` is what had to be pulled in, and it is
|
|
103
|
+
recorded in the input report so a reader can see the load was not self-contained.
|
|
104
|
+
"""
|
|
105
|
+
collection = griffe.ModulesCollection()
|
|
106
|
+
siblings: list[str] = []
|
|
107
|
+
for _ in range(_MAX_SIBLING_LOADS + 1):
|
|
108
|
+
try:
|
|
109
|
+
return griffe.load(module_name, search_paths=[str(search_path)],
|
|
110
|
+
try_relative_path=False,
|
|
111
|
+
modules_collection=collection), siblings
|
|
112
|
+
except griffe.AliasResolutionError as exc:
|
|
113
|
+
top = exc.alias.target_path.split(".")[0]
|
|
114
|
+
if top == module_name or top in siblings:
|
|
115
|
+
raise # retrying would loop on the same alias
|
|
116
|
+
if not _package_on_path(top, search_path):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"`{module_name}` re-exports `{exc.alias.target_path}`, and "
|
|
119
|
+
f"`{top}` is not beside it in {search_path}. This is the facade "
|
|
120
|
+
f"layout (a public package re-exporting a private one): build from "
|
|
121
|
+
f"a tree that holds both, or point codemap at the implementation "
|
|
122
|
+
f"package instead."
|
|
123
|
+
) from exc
|
|
124
|
+
griffe.load(top, search_paths=[str(search_path)], try_relative_path=False,
|
|
125
|
+
modules_collection=collection)
|
|
126
|
+
siblings.append(top)
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"`{module_name}` still had unresolved re-exports after loading "
|
|
129
|
+
f"{len(siblings)} sibling package(s): {', '.join(siblings)}."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _package_on_path(name: str, search_path: Path) -> bool:
|
|
134
|
+
"""True when ``name`` is importable as a package/module from ``search_path``."""
|
|
135
|
+
d = search_path / name
|
|
136
|
+
return (d / "__init__.py").is_file() or (d / "__init__.pyi").is_file() \
|
|
137
|
+
or (search_path / f"{name}.py").is_file() or (search_path / f"{name}.pyi").is_file()
|
|
138
|
+
|
|
139
|
+
|
|
83
140
|
def build_structural(package_path: str | Path):
|
|
84
141
|
"""The cheap, deterministic base: griffe load + definition nodes + structural
|
|
85
142
|
edges (contains / imports / inherits / decorated_by / export). No behavioral
|
|
@@ -99,7 +156,7 @@ def build_structural(package_path: str | Path):
|
|
|
99
156
|
# a repo whose root holds `pkg/`, `build /elsewhere/pkg` then silently analysed the
|
|
100
157
|
# local `pkg` — same shape of answer, different code. We always know the directory we
|
|
101
158
|
# were handed, so the name must resolve through `search_paths` and nowhere else.
|
|
102
|
-
root =
|
|
159
|
+
root, siblings = _load_with_siblings(module_name, search_path)
|
|
103
160
|
# Defence in depth: whatever the finder does next (a .pth file, a namespace package,
|
|
104
161
|
# a future default), a graph must describe the directory that was asked for. A wrong
|
|
105
162
|
# answer here is invisible downstream — it is well-formed, complete and about the
|
|
@@ -402,13 +459,18 @@ def _collect(graph, obj, root, target_pkg, walk) -> None:
|
|
|
402
459
|
# carry, or carries under the wrong scope, in one parse.
|
|
403
460
|
nested, eager = _source_import_targets(obj)
|
|
404
461
|
type_checking = {tgt for tgt, scope in nested if scope == "type_checking"}
|
|
462
|
+
# R1-C56/D1: a `.pyi` is a declaration — Python never executes it, so *none* of
|
|
463
|
+
# its imports run, whatever they look like. Recorded as a fourth scope rather
|
|
464
|
+
# than borrowed from `type_checking`: the mechanism is a different one, and the
|
|
465
|
+
# cycle classes are named by mechanism.
|
|
466
|
+
stub = _is_stub(obj)
|
|
405
467
|
for name, tgt in (obj.imports or {}).items():
|
|
406
468
|
# griffe files an import under `if TYPE_CHECKING:` as module-level; it never
|
|
407
469
|
# runs. Demote it unless the same target is also imported eagerly (D2).
|
|
408
470
|
scope = "type_checking" if tgt in type_checking and tgt not in eager else "module"
|
|
409
|
-
walk.imports.append((obj.canonical_path, tgt, scope))
|
|
471
|
+
walk.imports.append((obj.canonical_path, tgt, "stub" if stub else scope))
|
|
410
472
|
for tgt, scope in nested:
|
|
411
|
-
walk.imports.append((obj.canonical_path, tgt, scope))
|
|
473
|
+
walk.imports.append((obj.canonical_path, tgt, "stub" if stub else scope))
|
|
412
474
|
for name, member in obj.members.items():
|
|
413
475
|
if member.is_alias:
|
|
414
476
|
# capture ALL re-exports (public flag kept) — a symbol can be importable
|
|
@@ -487,7 +549,7 @@ def _emit_decorated_by(graph, obj) -> None:
|
|
|
487
549
|
#: import is a **function-local** dependency: it can execute, and labelling it by the
|
|
488
550
|
#: import that cannot would let a tree launder runtime coupling into the type layer
|
|
489
551
|
#: (R1-C49, and the test that says so).
|
|
490
|
-
_SCOPE_RANK = {"module": 0, "function": 1, "type_checking": 2}
|
|
552
|
+
_SCOPE_RANK = {"module": 0, "function": 1, "type_checking": 2, "stub": 3}
|
|
491
553
|
|
|
492
554
|
|
|
493
555
|
def _resolve_edges(graph, target_pkg, aliases, imports) -> None:
|
|
@@ -506,7 +568,18 @@ def _resolve_edges(graph, target_pkg, aliases, imports) -> None:
|
|
|
506
568
|
# no edge at all. Same narrow gate as pass B: only when the head names a
|
|
507
569
|
# module sitting beside the re-exporter, and labelled as the inference it is.
|
|
508
570
|
if _flat_sibling(parent_module, target_path, known_modules) is None:
|
|
509
|
-
|
|
571
|
+
# R1-C57: `import numpy as np` is not this package's API and stays out.
|
|
572
|
+
# A **public** alias to an outside definition is the opposite case: it is
|
|
573
|
+
# exactly what the package exposes, and dropping it made the facade layout
|
|
574
|
+
# (`pytest` re-exporting 90 names from `_pytest`) report a public surface
|
|
575
|
+
# of one symbol. Kept, with the target left as written and marked external
|
|
576
|
+
# — the definition is outside this graph, and the edge says so rather than
|
|
577
|
+
# pretending to resolve it.
|
|
578
|
+
if not is_public:
|
|
579
|
+
continue
|
|
580
|
+
extras["external"] = True
|
|
581
|
+
graph.add_edge(Edge("export", parent_module, target_path, extras=extras))
|
|
582
|
+
continue
|
|
510
583
|
target_path = f"{parent_module.rsplit('.', 1)[0]}.{target_path}"
|
|
511
584
|
extras["resolution"] = "flat"
|
|
512
585
|
graph.add_edge(Edge("export", parent_module, target_path, extras=extras))
|
|
@@ -599,6 +672,12 @@ def _add_node(graph, obj, root) -> None:
|
|
|
599
672
|
)
|
|
600
673
|
|
|
601
674
|
|
|
675
|
+
def _is_stub(obj) -> bool:
|
|
676
|
+
"""True when ``obj`` lives in a ``.pyi`` — a declaration Python never executes."""
|
|
677
|
+
f = module_file(obj)
|
|
678
|
+
return f is not None and f.suffix == ".pyi"
|
|
679
|
+
|
|
680
|
+
|
|
602
681
|
def _stub_marked(extras: dict, obj) -> dict:
|
|
603
682
|
"""Label a symbol that exists only in a ``.pyi`` stub (R1-C23 / design D5).
|
|
604
683
|
|
|
@@ -607,8 +686,7 @@ def _stub_marked(extras: dict, obj) -> dict:
|
|
|
607
686
|
a stubs distribution, and leaving them unmarked presents a function that does not
|
|
608
687
|
exist as if it did. Consumers that reason about execution (dead-code) exclude them.
|
|
609
688
|
"""
|
|
610
|
-
|
|
611
|
-
if f is not None and f.suffix == ".pyi":
|
|
689
|
+
if _is_stub(obj):
|
|
612
690
|
extras = {**extras, "stub": True}
|
|
613
691
|
return extras
|
|
614
692
|
|
|
@@ -153,7 +153,10 @@ class Query:
|
|
|
153
153
|
# R1-C49: between the two — everything that can execute. A `TYPE_CHECKING` import
|
|
154
154
|
# never does, so a cycle that needs one is not runtime coupling at all.
|
|
155
155
|
self._imports_runtime = nx.DiGraph()
|
|
156
|
-
|
|
156
|
+
# R1-C56: `stub` is the fourth scope — a `.pyi` is never executed, so none of its
|
|
157
|
+
# imports run. Counted apart from `type_checking` (different mechanism, same
|
|
158
|
+
# consequence) and, like it, kept out of both the eager and the runtime graph.
|
|
159
|
+
self._import_scopes = {"module": 0, "function": 0, "type_checking": 0, "stub": 0}
|
|
157
160
|
for n in graph.nodes.values():
|
|
158
161
|
if n.kind == "module":
|
|
159
162
|
self._imports.add_node(n.id)
|
|
@@ -163,9 +166,9 @@ class Query:
|
|
|
163
166
|
if e.type == "imports":
|
|
164
167
|
self._imports.add_edge(e.source, e.target)
|
|
165
168
|
scope = e.extras.get("scope")
|
|
166
|
-
if scope in ("function", "type_checking"):
|
|
167
|
-
# R1-C29: runs when the function runs; R1-C48: never runs.
|
|
168
|
-
# is an import-time edge — but only the first is a runtime one.
|
|
169
|
+
if scope in ("function", "type_checking", "stub"):
|
|
170
|
+
# R1-C29: runs when the function runs; R1-C48 and R1-C56: never runs.
|
|
171
|
+
# Neither is an import-time edge — but only the first is a runtime one.
|
|
169
172
|
self._import_scopes[scope] += 1
|
|
170
173
|
if scope == "function":
|
|
171
174
|
self._imports_runtime.add_edge(e.source, e.target)
|
|
@@ -665,10 +668,57 @@ class Query:
|
|
|
665
668
|
out = [c for c in out if _CONFIDENCE_RANK[c["confidence"]] >= floor]
|
|
666
669
|
return out
|
|
667
670
|
|
|
671
|
+
def _overridden_base(self, n: Node) -> tuple[str, int] | None:
|
|
672
|
+
"""The ancestor method ``n`` overrides, and that ancestor's inbound calls (R1-C55).
|
|
673
|
+
|
|
674
|
+
An override is not reached by its own name: the base is called and dynamic
|
|
675
|
+
dispatch lands here, so "no inbound calls" is a statement about the *name*, not
|
|
676
|
+
about the body. Measured on Pillow, where 40 of 63 ``high`` candidates were
|
|
677
|
+
``_open`` implementations of a template method the graph itself records as
|
|
678
|
+
called from ``ImageFile.__init__`` — the strongest grade, on the most ordinary
|
|
679
|
+
shape in object-oriented Python.
|
|
680
|
+
|
|
681
|
+
Walks ``inherits`` transitively and weighs **every** ancestor that declares the
|
|
682
|
+
name, not just the nearest: in a three-deep chain the middle link is an override
|
|
683
|
+
too, so it has no inbound call of its own, and stopping there would report "the
|
|
684
|
+
base is itself uncalled" while the call sits one level further up. An ancestor
|
|
685
|
+
with inbound calls therefore wins; failing that, the nearest one is named.
|
|
686
|
+
"""
|
|
687
|
+
cls, _, meth = n.id.rpartition(".")
|
|
688
|
+
if not cls or cls not in self._inherits:
|
|
689
|
+
return None
|
|
690
|
+
nearest: tuple[str, int] | None = None
|
|
691
|
+
seen, queue = {cls}, [cls]
|
|
692
|
+
while queue: # breadth-first, so `nearest` is the closest declaration
|
|
693
|
+
for base in self.bases(queue.pop(0)):
|
|
694
|
+
if base in seen:
|
|
695
|
+
continue
|
|
696
|
+
seen.add(base)
|
|
697
|
+
queue.append(base)
|
|
698
|
+
cand = f"{base}.{meth}"
|
|
699
|
+
if cand not in self.graph.nodes:
|
|
700
|
+
continue
|
|
701
|
+
in_calls = self._calls.in_degree(cand) if cand in self._calls else 0
|
|
702
|
+
if in_calls:
|
|
703
|
+
return cand, in_calls
|
|
704
|
+
nearest = nearest or (cand, 0)
|
|
705
|
+
return nearest
|
|
706
|
+
|
|
668
707
|
def _grade_dead(self, n: Node) -> dict:
|
|
669
708
|
"""Score one uncalled-private candidate → {id, confidence, root, reasons}."""
|
|
670
709
|
refs = self.references_to(n.id) # inbound of every kind, across roots
|
|
671
710
|
registry = n.extras.get("registry")
|
|
711
|
+
override = self._overridden_base(n)
|
|
712
|
+
if override and not refs:
|
|
713
|
+
# R1-C55: the name is uncalled; the body is reachable through the base.
|
|
714
|
+
base, in_calls = override
|
|
715
|
+
if in_calls:
|
|
716
|
+
return {"id": n.id, "confidence": "low", "root": self.root_of(n.id),
|
|
717
|
+
"reasons": [f"overrides {base}, which has {in_calls} inbound "
|
|
718
|
+
f"call(s) — reached by dispatch, not by name"]}
|
|
719
|
+
return {"id": n.id, "confidence": "medium", "root": self.root_of(n.id),
|
|
720
|
+
"reasons": [f"overrides {base}, which is itself uncalled here — "
|
|
721
|
+
f"dead only if the base is"]}
|
|
672
722
|
if refs:
|
|
673
723
|
by: dict[tuple[str, str], int] = {}
|
|
674
724
|
for r in refs:
|
|
@@ -1110,13 +1160,19 @@ class Query:
|
|
|
1110
1160
|
if frozenset(c) not in eager)
|
|
1111
1161
|
|
|
1112
1162
|
def type_only_import_cycles(self) -> list[list[str]]:
|
|
1113
|
-
"""Dependency cycles that close **only** with an import
|
|
1163
|
+
"""Dependency cycles that close **only** with an import that never executes.
|
|
1114
1164
|
|
|
1115
1165
|
The third kind (R1-C49, issue #18). These modules name each other's types and have
|
|
1116
1166
|
**no runtime dependency whatever**: neither import pulls the other at any moment of
|
|
1117
1167
|
execution. That is why they are not gated by ``no_lazy_cycles`` — which exists
|
|
1118
1168
|
against a lazy import used to walk *around* ``no_cycles`` — and get their own
|
|
1119
1169
|
opt-in rule instead.
|
|
1170
|
+
|
|
1171
|
+
Two mechanisms, one consequence (R1-C56/D2): an import under ``if TYPE_CHECKING:``
|
|
1172
|
+
and an import written in a ``.pyi``, which Python does not execute at all. The
|
|
1173
|
+
class partitions by consequence — what a reader needs is whether the import can
|
|
1174
|
+
break — while the edge keeps the mechanism in ``extras.scope``. Measured on
|
|
1175
|
+
Pillow, whose *only* "hard" cycle was a stub declaring the module that imports it.
|
|
1120
1176
|
"""
|
|
1121
1177
|
runtime = {frozenset(c) for c in nx.simple_cycles(self._imports_runtime)}
|
|
1122
1178
|
return _canonical_cycles(c for c in nx.simple_cycles(self._imports)
|
|
@@ -1132,7 +1188,8 @@ class Query:
|
|
|
1132
1188
|
"""
|
|
1133
1189
|
return {"module_level": self._import_scopes["module"],
|
|
1134
1190
|
"function_local": self._import_scopes["function"],
|
|
1135
|
-
"type_checking": self._import_scopes["type_checking"]
|
|
1191
|
+
"type_checking": self._import_scopes["type_checking"],
|
|
1192
|
+
"stub": self._import_scopes["stub"]}
|
|
1136
1193
|
|
|
1137
1194
|
def orphan_modules(self, root: str | None = None) -> list[str]:
|
|
1138
1195
|
"""Modules with no incoming imports (dead-code candidates — heuristic).
|
|
@@ -19,7 +19,10 @@ import subprocess
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
21
|
# What codemap actually consumes as input (source + docs it indexes as references).
|
|
22
|
-
|
|
22
|
+
# R1-C56/D3: `.pyi` is here because griffe reads it. Leaving it out made the manifest
|
|
23
|
+
# describe a different input than the graph was built from, so `scope_id` — and with it
|
|
24
|
+
# `--incremental` and `watch` — read as unknown on every tree that ships stubs.
|
|
25
|
+
DEFAULT_INCLUDE = ("*.py", "*.pyi", "*.md")
|
|
23
26
|
# fs-mode default excludes (git mode gets these for free via .gitignore).
|
|
24
27
|
DEFAULT_EXCLUDE_DIRS = frozenset({
|
|
25
28
|
"__pycache__", ".git", ".venv", "venv", "node_modules", "build", "dist",
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from collections import defaultdict
|
|
11
11
|
|
|
12
|
+
from codemap.diagnostics import diagnostics, render_lines
|
|
12
13
|
from codemap.model import Graph
|
|
13
14
|
|
|
14
15
|
_SYMBOL_KINDS = {"class", "function", "attribute"}
|
|
@@ -41,16 +42,39 @@ def build_api_surface(graph: Graph) -> dict:
|
|
|
41
42
|
"doc": _first_line(n.docstring), "file": n.file, "lineno": n.lineno}
|
|
42
43
|
for n in symbols
|
|
43
44
|
]})
|
|
45
|
+
reexported = _reexported_from_outside(graph)
|
|
44
46
|
return {
|
|
45
47
|
"kind": "api-surface",
|
|
46
48
|
"target": graph.target,
|
|
47
49
|
"totals": {"symbols": sum(len(m["symbols"]) for m in modules),
|
|
48
50
|
"modules_with_symbols": len(modules),
|
|
49
|
-
"public_modules": len(public_modules)
|
|
51
|
+
"public_modules": len(public_modules),
|
|
52
|
+
# R1-C57: always present, zero included — a reader must not have to
|
|
53
|
+
# tell "this package re-exports nothing" from "we did not look".
|
|
54
|
+
"reexported_from_outside": len(reexported)},
|
|
50
55
|
"modules": modules,
|
|
56
|
+
"reexported_from_outside": reexported,
|
|
57
|
+
"diagnostics": diagnostics(graph),
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
|
|
61
|
+
def _reexported_from_outside(graph: Graph) -> list[dict]:
|
|
62
|
+
"""Public names this package exposes whose definition lives outside it (R1-C57).
|
|
63
|
+
|
|
64
|
+
The facade layout — `pytest` re-exporting 90 names from `_pytest`, `attrs` from
|
|
65
|
+
`attr` — has almost no symbols of its own, so counting nodes reported a public
|
|
66
|
+
surface of **one** for a package whose whole purpose is its API. These names are
|
|
67
|
+
the surface; the definitions are simply not in this graph, and the answer says so
|
|
68
|
+
instead of omitting them.
|
|
69
|
+
"""
|
|
70
|
+
out = []
|
|
71
|
+
for e in graph.edges:
|
|
72
|
+
if e.type == "export" and e.extras.get("external") and e.extras.get("public"):
|
|
73
|
+
out.append({"name": e.extras.get("as", ""), "module": e.source,
|
|
74
|
+
"defined_at": e.target})
|
|
75
|
+
return sorted(out, key=lambda r: (r["module"], r["name"]))
|
|
76
|
+
|
|
77
|
+
|
|
54
78
|
def render_api_surface(graph: Graph) -> str:
|
|
55
79
|
"""Render the public API surface of ``graph`` as Markdown."""
|
|
56
80
|
by_module: dict[str, list] = defaultdict(list)
|
|
@@ -61,12 +85,24 @@ def render_api_surface(graph: Graph) -> str:
|
|
|
61
85
|
by_module[module].append(node)
|
|
62
86
|
|
|
63
87
|
lines = [f"# API surface — `{graph.target}`", ""]
|
|
88
|
+
# R1-C57: every other report carried the build's diagnostics and this one did not, so
|
|
89
|
+
# a facade package printed its surface with no hint that the import graph behind it was
|
|
90
|
+
# empty. A warning that reaches four reports out of five is a warning the reader can
|
|
91
|
+
# miss by asking the wrong question.
|
|
92
|
+
lines.extend(render_lines(graph))
|
|
64
93
|
public_modules = sorted(
|
|
65
94
|
n.id for n in graph.nodes.values() if n.kind == "module" and n.visibility == "public"
|
|
66
95
|
)
|
|
67
96
|
total = sum(len(v) for v in by_module.values())
|
|
68
|
-
|
|
97
|
+
reexported = _reexported_from_outside(graph)
|
|
98
|
+
lines.append(f"_{total} public symbols across {len(public_modules)} modules; "
|
|
99
|
+
f"{len(reexported)} more re-exported from outside this root._")
|
|
69
100
|
lines.append("")
|
|
101
|
+
if reexported:
|
|
102
|
+
lines.append(f"> ⚠ This package exposes **{len(reexported)}** name(s) it does not "
|
|
103
|
+
f"define — the facade layout. Their definitions are in another root "
|
|
104
|
+
f"and are **not judged here**; build that package to see them.")
|
|
105
|
+
lines.append("")
|
|
70
106
|
|
|
71
107
|
for module in public_modules:
|
|
72
108
|
symbols = sorted(by_module.get(module, []), key=lambda n: n.id)
|
|
@@ -84,6 +120,13 @@ def render_api_surface(graph: Graph) -> str:
|
|
|
84
120
|
lines.append(f" - {doc}")
|
|
85
121
|
lines.append("")
|
|
86
122
|
|
|
123
|
+
if reexported:
|
|
124
|
+
lines.append("## Re-exported from outside this root")
|
|
125
|
+
lines.append("")
|
|
126
|
+
for r in reexported:
|
|
127
|
+
lines.append(f"- **`{r['name']}`** → `{r['defined_at']}` (via `{r['module']}`)")
|
|
128
|
+
lines.append("")
|
|
129
|
+
|
|
87
130
|
return "\n".join(lines).rstrip() + "\n"
|
|
88
131
|
|
|
89
132
|
|
|
@@ -82,9 +82,10 @@ def render_architecture(query: Query) -> str:
|
|
|
82
82
|
or ["_none found in the eager import graph._"])
|
|
83
83
|
out.append("")
|
|
84
84
|
out.append(f"_Read {im['module_level']} module-level, {im['function_local']} "
|
|
85
|
-
f"function-local
|
|
86
|
-
f"module-level imports run at import time, so only
|
|
87
|
-
f"an import under `if TYPE_CHECKING:` never runs
|
|
85
|
+
f"function-local, {im['type_checking']} `TYPE_CHECKING` and {im['stub']} "
|
|
86
|
+
f"`.pyi` import(s). Only module-level imports run at import time, so only "
|
|
87
|
+
f"they can break on import; an import under `if TYPE_CHECKING:` never runs, "
|
|
88
|
+
f"and a `.pyi` is not executed at all._")
|
|
88
89
|
out.append("")
|
|
89
90
|
if a["lazy_cycles"]:
|
|
90
91
|
out.append(f"### Dependency cycles closed only by a function-local import: "
|
|
@@ -103,8 +104,8 @@ def render_architecture(query: Query) -> str:
|
|
|
103
104
|
if a["type_only_cycles"]:
|
|
104
105
|
# R1-C49: the third kind, kept apart from the second because the difference is the
|
|
105
106
|
# whole point — these modules have no runtime dependency on each other at all.
|
|
106
|
-
out.append(f"### Dependency cycles closed only by an import
|
|
107
|
-
f"`if TYPE_CHECKING
|
|
107
|
+
out.append(f"### Dependency cycles closed only by an import that never runs "
|
|
108
|
+
f"(`if TYPE_CHECKING:` or a `.pyi`): {len(a['type_only_cycles'])}")
|
|
108
109
|
out.append("")
|
|
109
110
|
out.append("_Neither module pulls the other at any moment of execution — they name "
|
|
110
111
|
"each other's types. Not an import-time failure and not runtime coupling; "
|
|
@@ -32,11 +32,11 @@ def render_dependencies(query: Query) -> str:
|
|
|
32
32
|
lines.append("_none found in the eager import graph._")
|
|
33
33
|
lines.append("")
|
|
34
34
|
lines.append(f"_Read {im['module_level']} module-level, {im['function_local']} "
|
|
35
|
-
f"function-local
|
|
36
|
-
f"the first run at import time. {len(lazy)} further
|
|
37
|
-
f"a function-local import (runtime coupling, not an
|
|
38
|
-
f"and {len(type_only)} through an import
|
|
39
|
-
f"runtime dependency at all)._")
|
|
35
|
+
f"function-local, {im['type_checking']} `TYPE_CHECKING` and {im['stub']} "
|
|
36
|
+
f"`.pyi` import(s); only the first run at import time. {len(lazy)} further "
|
|
37
|
+
f"cycle(s) close through a function-local import (runtime coupling, not an "
|
|
38
|
+
f"import-time failure) and {len(type_only)} through an import that never "
|
|
39
|
+
f"runs — `if TYPE_CHECKING:` or a `.pyi` (no runtime dependency at all)._")
|
|
40
40
|
lines.append("")
|
|
41
41
|
|
|
42
42
|
lines.append("## Most-depended-on modules (top 15)")
|
|
@@ -47,9 +47,10 @@ def _not_judged(query, contract: ArchitectureContract) -> list[dict]:
|
|
|
47
47
|
"type_checking_imports": query.import_map()["type_checking"],
|
|
48
48
|
"count": len(lazy) + len(type_only),
|
|
49
49
|
"note": ("`lazy` needs a function-local import — runtime coupling, gated by "
|
|
50
|
-
"`no_lazy_cycles = true`. `type_only` needs an import
|
|
51
|
-
"`if TYPE_CHECKING
|
|
52
|
-
"`no_type_only_cycles = true`.
|
|
50
|
+
"`no_lazy_cycles = true`. `type_only` needs an import that never runs "
|
|
51
|
+
"(`if TYPE_CHECKING:`, or a `.pyi`, which Python does not execute) — no "
|
|
52
|
+
"runtime dependency at all, gated by `no_type_only_cycles = true`. "
|
|
53
|
+
"`report architecture` lists both."),
|
|
53
54
|
}]
|
|
54
55
|
|
|
55
56
|
|
|
@@ -160,9 +160,9 @@ def render_docs(query: Query) -> str:
|
|
|
160
160
|
out.append(f"- **{len(lazy)} dependency cycle(s) closed only by a function-local "
|
|
161
161
|
f"import** — deliberate, and still runtime coupling.")
|
|
162
162
|
if type_only:
|
|
163
|
-
out.append(f"- **{len(type_only)} dependency cycle(s) closed only by an import
|
|
164
|
-
f"`if TYPE_CHECKING
|
|
165
|
-
f"runtime dependency.")
|
|
163
|
+
out.append(f"- **{len(type_only)} dependency cycle(s) closed only by an import that "
|
|
164
|
+
f"never runs** (`if TYPE_CHECKING:` or a `.pyi`) — the modules name each "
|
|
165
|
+
f"other's types and have no runtime dependency.")
|
|
166
166
|
if lay["violations"]:
|
|
167
167
|
out.append("- **Layer violations (mutual dependency):** "
|
|
168
168
|
+ ", ".join(f"{a} ↔ {b}" for a, b in lay["violations"]))
|
|
@@ -191,6 +191,11 @@ def _match(q: Query, n) -> dict:
|
|
|
191
191
|
(posargs / kwargs / splat per call site), which no node field can know.
|
|
192
192
|
"""
|
|
193
193
|
e = {"id": n.id, "kind": n.kind, "file": n.file, "lines": [n.lineno, n.endlineno]}
|
|
194
|
+
if n.extras.get("stub"):
|
|
195
|
+
# R1-C56/D4: a `.pyi` declaration, not code. Whoever is about to read the body
|
|
196
|
+
# must learn there is none from the answer, not from the file extension — the
|
|
197
|
+
# same rule as the epistemic label and the kind of an empty answer.
|
|
198
|
+
e["stub"] = True
|
|
194
199
|
if n.signature:
|
|
195
200
|
e["signature"] = n.signature
|
|
196
201
|
elif n.kind == "class":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
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
|
|
@@ -44,7 +44,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
44
44
|
|
|
45
45
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **961 tests with no failures on
|
|
48
48
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
49
49
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
50
50
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -124,6 +124,9 @@ tests/test_r1c4_complexity.py
|
|
|
124
124
|
tests/test_r1c50_flow_entry_points.py
|
|
125
125
|
tests/test_r1c53_narrowing_declared.py
|
|
126
126
|
tests/test_r1c54_deterministic_rendering.py
|
|
127
|
+
tests/test_r1c55_override_grade.py
|
|
128
|
+
tests/test_r1c56_stub_files.py
|
|
129
|
+
tests/test_r1c57_facade_package.py
|
|
127
130
|
tests/test_r1c5_apidiff.py
|
|
128
131
|
tests/test_r1c6_pack.py
|
|
129
132
|
tests/test_r1c7_edge_vocab.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.19"
|
|
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
|
|
@@ -79,7 +79,8 @@ def test_cycle_detection(q, graph):
|
|
|
79
79
|
type_only = {frozenset(c) for c in q.type_only_import_cycles()}
|
|
80
80
|
assert not (eager & lazy) and not (eager & type_only) and not (lazy & type_only), \
|
|
81
81
|
"a cycle has exactly one kind — the weakest scope that closes it (R1-C49)"
|
|
82
|
-
assert set(q.import_map()) == {"module_level", "function_local", "type_checking"
|
|
82
|
+
assert set(q.import_map()) == {"module_level", "function_local", "type_checking",
|
|
83
|
+
"stub"}
|
|
83
84
|
assert q.import_map()["module_level"] > 0
|
|
84
85
|
|
|
85
86
|
scopes: dict[tuple[str, str], set[str]] = {}
|
|
@@ -94,13 +95,24 @@ def test_cycle_detection(q, graph):
|
|
|
94
95
|
for c in lazy:
|
|
95
96
|
assert closing(c, "function"), f"a lazy cycle needs a function-local edge: {c}"
|
|
96
97
|
for c in type_only:
|
|
97
|
-
|
|
98
|
+
# R1-C56: two mechanisms never execute — `if TYPE_CHECKING:` and a `.pyi`. The
|
|
99
|
+
# class is defined by the consequence, so either scope may be what closes it.
|
|
100
|
+
assert closing(c, "type_checking") or closing(c, "stub"), \
|
|
101
|
+
f"a type-only cycle needs an import that never runs: {c}"
|
|
98
102
|
|
|
99
103
|
|
|
100
104
|
def test_orphan_modules(q):
|
|
105
|
+
"""An invariant, not a name: `bquant.cli` was the example until the target grew a
|
|
106
|
+
`__main__.py` that imports it, and the assertion started measuring the target
|
|
107
|
+
rather than the query (R1-C25 — the second time in this suite, after
|
|
108
|
+
`test_cycle_detection`). What must hold on any tree: an orphan is a module the
|
|
109
|
+
import graph has no inbound edge for."""
|
|
101
110
|
orphans = q.orphan_modules()
|
|
102
|
-
assert isinstance(orphans, list)
|
|
103
|
-
|
|
111
|
+
assert isinstance(orphans, list) and orphans, "the dogfood tree has orphans"
|
|
112
|
+
for mod in orphans:
|
|
113
|
+
assert q.graph.nodes[mod].kind == "module"
|
|
114
|
+
assert not [e for e in q.graph.edges
|
|
115
|
+
if e.type == "imports" and e.target == mod], f"{mod} is imported"
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
def test_determinism_with_edges():
|
|
@@ -153,7 +153,7 @@ def test_import_map_is_emitted_even_when_nothing_is_lazy(tmp_path):
|
|
|
153
153
|
to tell "no lazy imports here" from "this build did not look for them"."""
|
|
154
154
|
pkg = _pkg(tmp_path, {"a.py": "x = 1\n", "b.py": "from pkg.a import x\n"})
|
|
155
155
|
im = Query(extract(str(pkg))).import_map()
|
|
156
|
-
assert im == {"module_level": 1, "function_local": 0, "type_checking": 0}
|
|
156
|
+
assert im == {"module_level": 1, "function_local": 0, "type_checking": 0, "stub": 0}
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
# -- and no consumer states acyclicity as a property -------------------------
|
|
@@ -162,7 +162,7 @@ def test_architecture_never_claims_acyclic(lazy_cycle):
|
|
|
162
162
|
md = render_architecture(Query(extract(str(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
|
-
assert "function-local
|
|
165
|
+
assert "function-local, 0 `TYPE_CHECKING` and 0 `.pyi` import(s)" in md
|
|
166
166
|
assert "Dependency cycles closed only by a function-local import: 1" in md
|
|
167
167
|
|
|
168
168
|
|
|
@@ -176,7 +176,7 @@ def test_architecture_payload_carries_both_kinds_and_the_counts(lazy_cycle):
|
|
|
176
176
|
a = build_architecture(Query(extract(str(lazy_cycle))))
|
|
177
177
|
assert a["cycles"] == []
|
|
178
178
|
assert [sorted(c) for c in a["lazy_cycles"]] == [["pkg.a", "pkg.b"]]
|
|
179
|
-
assert a["import_map"] == {"module_level": 1, "function_local": 1, "type_checking": 0}
|
|
179
|
+
assert a["import_map"] == {"module_level": 1, "function_local": 1, "type_checking": 0, "stub": 0}
|
|
180
180
|
|
|
181
181
|
|
|
182
182
|
def test_no_renderer_states_acyclicity_anywhere(tmp_path):
|
|
@@ -137,7 +137,7 @@ def test_the_cycle_is_not_eager(tree):
|
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def test_import_map_names_the_scope_always(tree):
|
|
140
|
-
assert tree[2].import_map() == {"module_level": 4, "function_local": 1, "type_checking": 3}
|
|
140
|
+
assert tree[2].import_map() == {"module_level": 4, "function_local": 1, "type_checking": 3, "stub": 0}
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
def test_import_map_says_zero_when_there_is_none(tmp_path):
|
|
@@ -150,10 +150,10 @@ def test_every_consumer_says_which_imports_it_did_not_judge(tree):
|
|
|
150
150
|
a = build_architecture(q)
|
|
151
151
|
assert a["import_map"]["type_checking"] == 3
|
|
152
152
|
md = render_architecture(q)
|
|
153
|
-
assert "3 `TYPE_CHECKING` import(s)" in md
|
|
154
|
-
assert "closed only by an import
|
|
155
|
-
assert "`TYPE_CHECKING` import(s)" in render_dependencies(q)
|
|
156
|
-
assert "closed only by an import
|
|
153
|
+
assert "3 `TYPE_CHECKING` and 0 `.pyi` import(s)" in md
|
|
154
|
+
assert "closed only by an import that never runs (`if TYPE_CHECKING:` or a `.pyi`): 1" in md
|
|
155
|
+
assert "`TYPE_CHECKING` and 0 `.pyi` import(s)" in render_dependencies(q)
|
|
156
|
+
assert "closed only by an import that never runs" in render_docs(q)
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def test_the_gate_is_green_and_names_what_it_read(tree):
|
|
@@ -192,18 +192,19 @@ def test_the_reports_carry_both_kinds_and_say_zero(both, tmp_path):
|
|
|
192
192
|
assert _flat(a["type_only_cycles"]) == [["bp.typed_a", "bp.typed_b"]]
|
|
193
193
|
md = render_architecture(both)
|
|
194
194
|
assert "Dependency cycles closed only by a function-local import: 1" in md
|
|
195
|
-
assert "Dependency cycles closed only by an import
|
|
195
|
+
assert ("Dependency cycles closed only by an import that never runs "
|
|
196
|
+
"(`if TYPE_CHECKING:` or a `.pyi`): 1") in md
|
|
196
197
|
dep = render_dependencies(both)
|
|
197
198
|
assert "1 further cycle(s) close through a function-local import" in dep
|
|
198
|
-
assert "1 through an import
|
|
199
|
+
assert "1 through an import that never runs" in dep
|
|
199
200
|
assert _flat(build_dependencies(both)["type_only_import_cycles"]) == [["bp.typed_a", "bp.typed_b"]]
|
|
200
201
|
docs = render_docs(both)
|
|
201
202
|
assert "closed only by a function-local import" in docs
|
|
202
|
-
assert "closed only by an import
|
|
203
|
+
assert "closed only by an import that never runs" in docs
|
|
203
204
|
|
|
204
205
|
quiet = Query(extract(str(_pkg(tmp_path, {"a": "from .b import x\n", "b": "x = 1\n"}))))
|
|
205
206
|
assert build_architecture(quiet)["type_only_cycles"] == []
|
|
206
|
-
assert "and 0 through an import
|
|
207
|
+
assert "and 0 through an import that never runs" in render_dependencies(quiet)
|
|
207
208
|
|
|
208
209
|
|
|
209
210
|
# -- f1: a rule that ran must name itself ---------------------------------------------------
|