mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-21 19:31:17 +00:00
fix(extractors): share the in-flight scan root via base, not graphify.extract (#3666)
`extractors/markdown.py` read the scan root of the extraction in flight with a
function-local `import graphify.extract` + `getattr(_extract,
"_XAML_ACTIVE_EXTRACT_ROOT", None)`. That violates MIGRATION.md invariant #4
("never import from graphify.extract inside this package; direction is
extract.py -> extractors/"), and the `getattr(..., None)` meant a rename of the
module global would silently disable Markdown wikilink (Obsidian vault)
resolution rather than fail loudly.
Move the ambient scan root onto `extractors/base.py` — where the extractors
legitimately live — as `set_active_scan_root()` / `active_scan_root()`.
`_safe_extract_with_xaml_root` now publishes the root there (still saving and
restoring for nested extraction), the two XAML readers and the Markdown vault
lookup read it through the accessor, and `markdown.py` imports it in the correct
direction. The `_XAML_ACTIVE_EXTRACT_ROOT` module global in extract.py is gone;
nothing else referenced it.
Behavior is unchanged — the same process-global root is set around each
extraction and read by the same sites. Tests assert the extractor no longer
imports graphify.extract (fails on the old code), that the set/get/restore
contract nests correctly, and that markdown's accessor IS the base accessor.
The markdown, XAML/dotnet, and language suites stay green; the only failures on
this machine are the pre-existing v8 baseline set (c-include determinism,
wikilink-unicode, two watch cwd-deleted cases), which fail identically on clean
v8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 86721f765d)
This commit is contained in:
1 parent
5fec9d32e8
commit
97922b9c66
4 files changed
+118
-17
No files matched your search
+12
-8
@@ -44,6 +44,8 @@ from graphify.extractors.base import ( # noqa: F401
|
||||
_read_source_bytes,
|
||||
_read_source_text,
|
||||
_read_text,
|
||||
active_scan_root,
|
||||
set_active_scan_root,
|
||||
)
|
||||
from graphify.extractors.apex import extract_apex # noqa: F401
|
||||
from graphify.extractors.bash import extract_bash # noqa: F401
|
||||
@@ -8134,9 +8136,10 @@ def _xaml_project_root(path: Path) -> Path:
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if _XAML_ACTIVE_EXTRACT_ROOT is None:
|
||||
active_root = active_scan_root()
|
||||
if active_root is None:
|
||||
return root
|
||||
boundary = _XAML_ACTIVE_EXTRACT_ROOT.resolve()
|
||||
boundary = active_root.resolve()
|
||||
try:
|
||||
root.resolve().relative_to(boundary)
|
||||
return root
|
||||
@@ -8147,7 +8150,7 @@ def _xaml_project_root(path: Path) -> Path:
|
||||
def _xaml_csharp_class_nodes(path: Path) -> dict[str, list[dict]]:
|
||||
from graphify.detect import _is_ignored, _is_noise_dir, _load_graphifyignore
|
||||
root = _xaml_project_root(path)
|
||||
cache_key = str(root.resolve()) if _XAML_ACTIVE_EXTRACT_ROOT is not None else None
|
||||
cache_key = str(root.resolve()) if active_scan_root() is not None else None
|
||||
if cache_key and cache_key in _XAML_CSHARP_CLASS_CACHE:
|
||||
return _XAML_CSHARP_CLASS_CACHE[cache_key]
|
||||
classes: dict[str, list[dict]] = {}
|
||||
@@ -8205,7 +8208,6 @@ def _xaml_pascal_name(name: str) -> str | None:
|
||||
|
||||
_XAML_TOOLKIT_FIELD_RE = re.compile(r"\b(?P<name>_?m?_?[A-Za-z_]\w*)\s*(?:=.*)?;")
|
||||
_XAML_TOOLKIT_METHOD_RE = re.compile(r"\b(?P<name>[A-Za-z_]\w*)\s*\(")
|
||||
_XAML_ACTIVE_EXTRACT_ROOT: Path | None = None
|
||||
_XAML_CSHARP_CLASS_CACHE: dict[str, dict[str, list[dict]]] = {}
|
||||
|
||||
|
||||
@@ -8839,13 +8841,15 @@ def _get_extractor(path: Path) -> Any | None:
|
||||
|
||||
|
||||
def _safe_extract_with_xaml_root(extractor, path: Path, root: Path) -> dict:
|
||||
global _XAML_ACTIVE_EXTRACT_ROOT
|
||||
previous_root = _XAML_ACTIVE_EXTRACT_ROOT
|
||||
_XAML_ACTIVE_EXTRACT_ROOT = root.resolve()
|
||||
# Publish the in-flight scan root on extractors.base (the correct import
|
||||
# direction) so root-dependent extractors — Markdown link resolution, the
|
||||
# XAML C# project clamp — can read it without reaching back into this module
|
||||
# (#3666). Despite the XAML-era name this wraps every extractor.
|
||||
previous_root = set_active_scan_root(root.resolve())
|
||||
try:
|
||||
return _safe_extract(extractor, path, scan_root=root)
|
||||
finally:
|
||||
_XAML_ACTIVE_EXTRACT_ROOT = previous_root
|
||||
set_active_scan_root(previous_root)
|
||||
|
||||
|
||||
def _worker_init(language_overrides: dict[str, str]) -> None:
|
||||
|
||||
@@ -6,6 +6,34 @@ from pathlib import Path
|
||||
|
||||
from graphify.ids import make_id
|
||||
|
||||
# Ambient scan root of the extraction in flight. extract.py's
|
||||
# _safe_extract_with_xaml_root sets it around every per-file extraction; the
|
||||
# extractors that need the corpus boundary read it (Markdown link resolution and
|
||||
# the XAML C# project-root clamp). It lives here rather than in graphify.extract
|
||||
# so those extractors read it in the correct import direction (extract.py ->
|
||||
# extractors/, never back), per MIGRATION.md invariant #4 — the old
|
||||
# function-local `import graphify.extract` + getattr(..., None) was a package ->
|
||||
# parent dependency that also failed silently if the module global were renamed
|
||||
# (#3666). None outside extract().
|
||||
_ACTIVE_SCAN_ROOT: "Path | None" = None
|
||||
|
||||
|
||||
def set_active_scan_root(root: "Path | None") -> "Path | None":
|
||||
"""Set the in-flight scan root, returning the previous value so the caller
|
||||
can restore it — extraction nests (a XAML shard re-enters extract())."""
|
||||
global _ACTIVE_SCAN_ROOT
|
||||
previous = _ACTIVE_SCAN_ROOT
|
||||
_ACTIVE_SCAN_ROOT = root
|
||||
return previous
|
||||
|
||||
|
||||
def active_scan_root() -> "Path | None":
|
||||
"""The scan root of the extraction in flight, or None outside extract().
|
||||
|
||||
A direct ``extract_markdown()`` / ``extract_xaml()`` call (no surrounding
|
||||
``extract()``) sees None and the root-dependent behavior stays off."""
|
||||
return _ACTIVE_SCAN_ROOT
|
||||
|
||||
# Language built-in globals that AST may classify as call targets when used as
|
||||
# constructors or coercion functions (e.g. String(x), Number(x), Boolean(x)).
|
||||
# Without this filter they become god-nodes accumulating spurious edges from
|
||||
|
||||
@@ -139,15 +139,11 @@ def _parse_frontmatter_fallback(fm_lines: list[str]) -> dict:
|
||||
_MD_LINK_INDEX_CACHE: "dict[str, dict[str, list[tuple[int, str, Path]]]]" = {}
|
||||
|
||||
|
||||
def _active_scan_root() -> "Path | None":
|
||||
"""The scan root of the extraction in flight, or None outside extract().
|
||||
|
||||
_safe_extract_with_xaml_root sets this for every extraction despite its
|
||||
XAML-era name; a direct extract_markdown() call has no root and the
|
||||
fallback stays off.
|
||||
"""
|
||||
import graphify.extract as _extract
|
||||
return getattr(_extract, "_XAML_ACTIVE_EXTRACT_ROOT", None)
|
||||
# The in-flight scan root now lives on extractors.base (correct import
|
||||
# direction: extract.py -> extractors/, never back), so a rename can no longer
|
||||
# silently disable link resolution the way the old getattr(graphify.extract, …,
|
||||
# None) could (#3666).
|
||||
from graphify.extractors.base import active_scan_root as _active_scan_root
|
||||
|
||||
|
||||
def _nfc(s: str) -> str:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""The in-flight scan root is shared through extractors.base, not by reaching
|
||||
back into graphify.extract — MIGRATION.md invariant #4 (#3666)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extractors import base as base_mod
|
||||
from graphify.extractors import markdown as markdown_mod
|
||||
|
||||
|
||||
def _imports_graphify_extract(module) -> bool:
|
||||
"""True if the module's source imports graphify.extract anywhere — a
|
||||
top-level or a function-local `import graphify.extract` / `from
|
||||
graphify.extract import ...`."""
|
||||
source = Path(module.__file__).read_text(encoding="utf-8")
|
||||
for node in ast.walk(ast.parse(source)):
|
||||
if isinstance(node, ast.Import):
|
||||
if any(a.name == "graphify.extract" or a.name.startswith("graphify.extract.")
|
||||
for a in node.names):
|
||||
return True
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module == "graphify.extract" or (node.module or "").startswith("graphify.extract."):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_markdown_extractor_does_not_import_graphify_extract():
|
||||
"""The Markdown extractor read the in-flight scan root via a function-local
|
||||
`import graphify.extract` + getattr, a package->parent dependency that
|
||||
violated invariant #4 and degraded silently on a rename (#3666). It must now
|
||||
reach the shared context through extractors.base instead."""
|
||||
assert not _imports_graphify_extract(markdown_mod), (
|
||||
"extractors/markdown.py must not import graphify.extract — read the scan "
|
||||
"root via extractors.base.active_scan_root() (MIGRATION.md invariant #4)"
|
||||
)
|
||||
|
||||
|
||||
def test_active_scan_root_defaults_to_none():
|
||||
# A fresh process (no extraction in flight) has no active root.
|
||||
assert base_mod.active_scan_root() is None
|
||||
|
||||
|
||||
def test_active_scan_root_set_get_restore_nested():
|
||||
"""set_active_scan_root returns the previous value so nested extraction can
|
||||
restore it; active_scan_root reflects the current top of that stack."""
|
||||
assert base_mod.active_scan_root() is None
|
||||
outer = Path("/repo").resolve()
|
||||
inner = Path("/repo/vendored").resolve()
|
||||
|
||||
prev_outer = base_mod.set_active_scan_root(outer)
|
||||
try:
|
||||
assert prev_outer is None
|
||||
assert base_mod.active_scan_root() == outer
|
||||
|
||||
prev_inner = base_mod.set_active_scan_root(inner)
|
||||
try:
|
||||
assert prev_inner == outer
|
||||
assert base_mod.active_scan_root() == inner
|
||||
finally:
|
||||
base_mod.set_active_scan_root(prev_inner)
|
||||
|
||||
assert base_mod.active_scan_root() == outer
|
||||
finally:
|
||||
base_mod.set_active_scan_root(prev_outer)
|
||||
|
||||
assert base_mod.active_scan_root() is None
|
||||
|
||||
|
||||
def test_markdown_active_scan_root_alias_is_base_accessor():
|
||||
"""The Markdown extractor's _active_scan_root is the base accessor, so the
|
||||
scan root extract() publishes is exactly what link resolution reads."""
|
||||
assert markdown_mod._active_scan_root is base_mod.active_scan_root
|
||||
Reference in new issue
Block a user