from __future__ import annotations import sys from pathlib import Path COLLECT_FILES_FUNCTION = """def collect_files(target: Path, *, follow_symlinks: bool = False) -> list[Path]: if target.is_file(): return [target] _EXTENSIONS = { ".py", ".js", ".ts", ".tsx", ".go", ".rs", ".java", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".rb", ".cs", ".kt", ".kts", ".scala", ".php", ".swift", ".lua", ".toc", ".zig", ".ps1", ".m", ".mm", } _SKIP_DIRS = { "venv", ".venv", "env", ".env", "node_modules", "__pycache__", ".git", "dist", "build", "target", "out", "site-packages", "lib64", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox", ".eggs", } def _is_noise_dir(part: str) -> bool: if part in _SKIP_DIRS: return True if part.endswith("_venv") or part.endswith("_env"): return True if part.endswith(".egg-info"): return True return False def _load_graphifyignore(root: Path) -> list[str]: ignore_file = root / ".graphifyignore" if not ignore_file.exists(): return [] patterns: list[str] = [] for line in ignore_file.read_text(errors="ignore").splitlines(): line = line.strip() if line and not line.startswith("#"): patterns.append(line) return patterns def _is_ignored(path: Path, root: Path, patterns: list[str]) -> bool: if not patterns: return False try: rel = str(path.relative_to(root)) except ValueError: return False rel = rel.replace(os.sep, "/") parts = rel.split("/") for pattern in patterns: p = pattern.strip("/") if not p: continue if fnmatch.fnmatch(rel, p): return True if fnmatch.fnmatch(path.name, p): return True for i, part in enumerate(parts): if fnmatch.fnmatch(part, p): return True if fnmatch.fnmatch("/".join(parts[: i + 1]), p): return True return False ignore_patterns = _load_graphifyignore(target) # Walk with symlink following + cycle detection results = [] for dirpath, dirnames, filenames in os.walk(target, followlinks=follow_symlinks): dp = Path(dirpath) dirnames[:] = [ d for d in dirnames if not d.startswith(".") and not _is_noise_dir(d) and not _is_ignored(dp / d, target, ignore_patterns) ] if os.path.islink(dirpath): real = os.path.realpath(dirpath) parent_real = os.path.realpath(os.path.dirname(dirpath)) if parent_real == real or parent_real.startswith(real + os.sep): dirnames.clear() continue if any(part.startswith(".") for part in dp.parts): continue for fname in filenames: p = dp / fname if fname.startswith("."): continue if _is_ignored(p, target, ignore_patterns): continue if p.suffix.lower() in _EXTENSIONS: results.append(p) return sorted(results) """ def _patch_extract_file(path: Path) -> bool: source = path.read_text(encoding="utf-8") changed = False if "import fnmatch" not in source: marker = "from __future__ import annotations\n" if marker in source: source = source.replace(marker, marker + "import fnmatch\n", 1) changed = True else: raise RuntimeError("Cannot patch extract.py: import marker not found.") start = source.find("def collect_files(target: Path, *, follow_symlinks: bool = False) -> list[Path]:") end = source.find('\n\nif __name__ == "__main__":', start) if start == -1 or end == -1: raise RuntimeError("Cannot patch extract.py: collect_files function block not found.") current_block = source[start:end] if current_block.strip() != COLLECT_FILES_FUNCTION.strip(): source = source[:start] + COLLECT_FILES_FUNCTION + source[end:] changed = True if changed: path.write_text(source, encoding="utf-8") return changed def _patch_watch_file(path: Path) -> bool: source = path.read_text(encoding="utf-8") target = '(out / "GRAPH_REPORT.md").write_text(report)' patched = '(out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8")' if target not in source: if patched in source: return False raise RuntimeError("Cannot patch watch.py: write_text(report) call not found.") source = source.replace(target, patched, 1) path.write_text(source, encoding="utf-8") return True def main() -> int: try: import graphify # noqa: PLC0415 except Exception as exc: # pragma: no cover print(f"[graphify-hotfix] graphify import failed: {exc}", file=sys.stderr) return 1 package_dir = Path(graphify.__file__).resolve().parent extract_path = package_dir / "extract.py" watch_path = package_dir / "watch.py" if not extract_path.exists() or not watch_path.exists(): print("[graphify-hotfix] graphify package files not found", file=sys.stderr) return 1 changed_extract = _patch_extract_file(extract_path) changed_watch = _patch_watch_file(watch_path) status = [] status.append("extract.py: patched" if changed_extract else "extract.py: already patched") status.append("watch.py: patched" if changed_watch else "watch.py: already patched") print("[graphify-hotfix] " + " | ".join(status)) return 0 if __name__ == "__main__": raise SystemExit(main())