Accept str paths in the semantic extract entry points (#1386)

extract_corpus_parallel and extract_files_direct are typed list[Path] but
crashed with AttributeError on str paths (f.suffix in slicing/partition, f.parent
in packing). Coerce files = [Path(f) for f in files] at both public entry points;
the AST extract() already coerced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Safi
2026-06-19 10:01:31 +01:00
co-authored by Claude Opus 4.8
parent 8883991c2e
commit 5038129c7e
2 changed files with 61 additions and 0 deletions
+9
View File
@@ -1303,7 +1303,12 @@ def extract_files_direct(
Returns dict with nodes, edges, hyperedges, input_tokens, output_tokens.
Raises ValueError for unknown backends or when no API key is configured.
Raises ImportError if SDK missing.
Accepts ``str`` paths as well as ``Path``; string entries are coerced up
front so downstream helpers (``_partition_semantic_files``, ``_read_files``,
``_build_image_refs``) can rely on ``Path`` semantics (#1386).
"""
files = [Path(f) for f in files]
if backend is None:
backend = detect_backend()
if backend is None:
@@ -1728,7 +1733,11 @@ def extract_corpus_parallel(
Returns merged dict with nodes, edges, hyperedges, input_tokens,
output_tokens. Failed chunks are logged to stderr and skipped — one bad
chunk does not abort the run.
Accepts ``str`` paths as well as ``Path``; string entries are coerced up
front so packing/slicing helpers can rely on ``Path`` semantics (#1386).
"""
files = [Path(f) for f in files]
# Split oversized splittable documents into slices that cover the whole file
# before packing, so content past _FILE_CHAR_CAP is extracted instead of
# silently dropped (#1369). Files at/under the cap pass through unchanged.
+52
View File
@@ -132,6 +132,58 @@ def test_missing_gemini_key_names_both_supported_env_vars(monkeypatch):
assert "GEMINI_API_KEY or GOOGLE_API_KEY" in str(exc.value)
# ---------------------------------------------------------------------------
# #1386: public entry points accept str paths, not just pathlib.Path
# ---------------------------------------------------------------------------
def test_extract_files_direct_accepts_str_paths(tmp_path, monkeypatch):
_clear_backend_env(monkeypatch)
monkeypatch.setenv("GOOGLE_API_KEY", "google-key")
source = tmp_path / "note.md"
source.write_text("# Architecture\n\nThe runner emits a snapshot.\n")
result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1}
# str path must not raise AttributeError: 'str' object has no attribute 'suffix'
with patch("graphify.llm._call_openai_compat", return_value=result):
assert llm.extract_files_direct([str(source)], backend="gemini", root=tmp_path) is result
def test_extract_corpus_parallel_accepts_str_and_mixed_paths(tmp_path, monkeypatch):
_clear_backend_env(monkeypatch)
monkeypatch.setenv("GOOGLE_API_KEY", "google-key")
f1 = tmp_path / "a.md"
f1.write_text("# A\n\nNode one.\n")
f2 = tmp_path / "b.md"
f2.write_text("# B\n\nNode two.\n")
result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1}
with patch("graphify.llm._call_openai_compat", return_value=result):
# all-str, all-Path, and mixed must each pack + run without AttributeError
for files in ([str(f1), str(f2)], [f1, f2], [str(f1), f2]):
merged = llm.extract_corpus_parallel(
files, backend="gemini", root=tmp_path, max_concurrency=1
)
assert merged["failed_chunks"] == 0
def test_str_path_entry_points_handle_edge_cases(tmp_path, monkeypatch):
_clear_backend_env(monkeypatch)
monkeypatch.setenv("GOOGLE_API_KEY", "google-key")
result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1}
with patch("graphify.llm._call_openai_compat", return_value=result):
# empty list: no chunks, nothing to extract, no crash
empty = llm.extract_corpus_parallel([], backend="gemini", root=tmp_path)
assert empty["nodes"] == [] and empty["failed_chunks"] == 0
# a Path subclass is still a Path and must pass through unchanged
class _SubPath(type(Path())): # concrete OS-specific Path subclass
pass
sub = _SubPath(tmp_path / "c.md")
sub.write_text("# C\n\nNode.\n")
assert llm.extract_files_direct([sub], backend="gemini", root=tmp_path) is result
# ---------------------------------------------------------------------------
# Adaptive retry: context-window overflow recovery
# ---------------------------------------------------------------------------