Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
convert_google_workspace_file,
google_workspace_enabled,
)
from graphify.paths import GRAPHIFY_OUT, GRAPHIFY_OUT_NAME, out_path
from graphify.paths import GRAPHIFY_OUT, out_path


class FileType(str, Enum):
Expand Down Expand Up @@ -793,7 +793,7 @@ def count_words(path: Path) -> int:
"site-packages", "lib64",
".pytest_cache", ".mypy_cache", ".ruff_cache",
".tox", ".nox", ".eggs", "*.egg-info", # nox is tox's successor, same .nox/ venv shape (#1804)
"graphify-out", GRAPHIFY_OUT_NAME, # never treat own output as source input (#524); honour GRAPHIFY_OUT (#1423)
"graphify-out", # never treat the default output as source input (#524)
# Coverage/test-artefact dirs — generated, never architecturally meaningful
"coverage", "lcov-report", # Vitest/Istanbul/nyc HTML reports (#870)
"visual-tests", "visual-test", # Playwright/visual-regression bundles (#869)
Expand Down Expand Up @@ -1182,6 +1182,13 @@ def _resolves_under_root(path: Path, root: Path) -> bool:

def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: bool | None = None, extra_excludes: list[str] | None = None, cache_root: Path | None = None, gitignore: bool = True) -> dict:
root = root.resolve()
configured_out_dir = root / GRAPHIFY_OUT
configured_out_names = {configured_out_dir.name}
try:
configured_out_dir = configured_out_dir.resolve()
except (OSError, RuntimeError):
configured_out_dir = configured_out_dir.absolute()
configured_out_names.add(configured_out_dir.name)
# .graphifyinclude support was removed (#2112): its loader and matchers had
# no consumers, so the file has been a silent no-op since dot directories
# became indexed by default (#873). Surface that once per scan so a
Expand Down Expand Up @@ -1295,6 +1302,16 @@ def _on_walk_error(err: OSError) -> None:
# repos for no correctness gain.
kept_dirs: list[str] = []
for d in dirnames:
child = dp / d
is_configured_out = False
if d in configured_out_names:
try:
is_configured_out = child.resolve() == configured_out_dir
except (OSError, RuntimeError):
pass
if is_configured_out:
pruned_noise.append(str(child) + os.sep)
continue
if _is_noise_dir(d, dp):
# Record pruned-as-noise dirs so a wrongly-pruned real
# source dir is at least traceable in the output rather
Expand Down
5 changes: 2 additions & 3 deletions graphify/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,8 @@ def disambiguate_ambiguous_candidates(
{c: candidate_files.get(c, "") for c in survivors},
)

# Bare directory name even when GRAPHIFY_OUT is an absolute path. Used by the
# path guards that walk parents looking for the output dir by name, and by the
# detect scan-exclude so a custom output dir is never re-ingested as source.
# Bare directory name even when GRAPHIFY_OUT is an absolute path. Used by path
# guards that walk parents looking for the output directory by name.
GRAPHIFY_OUT_NAME = os.path.basename(os.path.normpath(GRAPHIFY_OUT))


Expand Down
72 changes: 72 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,78 @@ def test_detect_prunes_venv_names_without_markers(tmp_path):
assert not any(f"{os.sep}{name}{os.sep}" in f for f in all_files), f"{name} must stay pruned"


@pytest.mark.parametrize(
("configured_out", "absolute", "symlink_target"),
[
pytest.param("graphify-out/nlp", False, None, id="default-parent"),
pytest.param("artifacts/nlp", False, None, id="custom-parent"),
pytest.param("artifacts/nlp", True, None, id="absolute"),
pytest.param(
"aliases/output-link",
False,
"artifacts/nlp",
id="in-root-symlink",
),
],
)
def test_nested_graphify_out_prunes_only_configured_path(
tmp_path, configured_out, absolute, symlink_target
):
"""#2273: a nested output basename must not prune same-named source dirs."""
import json
import subprocess
import sys

if absolute:
configured_out = str(tmp_path / configured_out)

source = tmp_path / "src" / "revil" / "nexus" / "nlp" / "core.py"
source.parent.mkdir(parents=True)
source.write_text("def tokenize(text):\n return text.split()\n")

output_dir = (
tmp_path / symlink_target
if symlink_target is not None
else tmp_path / configured_out
)
if symlink_target is not None:
output_dir.mkdir(parents=True)
configured_out_link = tmp_path / configured_out
configured_out_link.parent.mkdir(parents=True)
try:
configured_out_link.symlink_to(output_dir, target_is_directory=True)
except (OSError, NotImplementedError):
pytest.skip("filesystem does not support symlinks")

generated = output_dir / "generated.py"
generated.parent.mkdir(parents=True, exist_ok=True)
generated.write_text("SHOULD_NOT_BE_INDEXED = True\n")

probe = subprocess.run(
[
sys.executable,
"-c",
(
"import json, sys\n"
"from pathlib import Path\n"
"from graphify.detect import detect\n"
"result = detect(Path(sys.argv[1]))\n"
"print(json.dumps(result['files']['code']))\n"
),
str(tmp_path),
],
cwd=Path(__file__).parents[1],
env={**os.environ, "GRAPHIFY_OUT": configured_out},
check=True,
capture_output=True,
text=True,
)
detected = {Path(p).resolve() for p in json.loads(probe.stdout)}

assert source.resolve() in detected
assert generated.resolve() not in detected


def test_detect_records_unclassified_extensionless_files(tmp_path):
# #1692: extensionless, non-shebang project files (Dockerfile, Makefile, ...)
# were considered but left no trace. detect() now lists them under
Expand Down
Loading