pygraph-mcp 0.1.0__py3-none-any.whl

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.
Files changed (54) hide show
  1. pygraph/__init__.py +1 -0
  2. pygraph/__main__.py +3 -0
  3. pygraph/builder.py +561 -0
  4. pygraph/cli.py +308 -0
  5. pygraph/commands/__init__.py +53 -0
  6. pygraph/commands/boundaries.py +18 -0
  7. pygraph/commands/callees.py +13 -0
  8. pygraph/commands/callers.py +12 -0
  9. pygraph/commands/changes.py +49 -0
  10. pygraph/commands/complexity.py +27 -0
  11. pygraph/commands/context.py +41 -0
  12. pygraph/commands/coupling.py +26 -0
  13. pygraph/commands/deps.py +15 -0
  14. pygraph/commands/focus.py +37 -0
  15. pygraph/commands/graph_report.py +50 -0
  16. pygraph/commands/hotspot.py +18 -0
  17. pygraph/commands/impact.py +12 -0
  18. pygraph/commands/imports_cmd.py +13 -0
  19. pygraph/commands/node.py +50 -0
  20. pygraph/commands/opencode_plugin.py +41 -0
  21. pygraph/commands/orphans.py +17 -0
  22. pygraph/commands/path_cmd.py +13 -0
  23. pygraph/commands/plan.py +52 -0
  24. pygraph/commands/public.py +12 -0
  25. pygraph/commands/query_cmd.py +13 -0
  26. pygraph/commands/review.py +63 -0
  27. pygraph/commands/source.py +11 -0
  28. pygraph/commands/stale.py +19 -0
  29. pygraph/commands/trace.py +21 -0
  30. pygraph/config.py +26 -0
  31. pygraph/extractors/__init__.py +0 -0
  32. pygraph/extractors/calls.py +63 -0
  33. pygraph/extractors/decorators.py +0 -0
  34. pygraph/extractors/env.py +81 -0
  35. pygraph/extractors/errors.py +52 -0
  36. pygraph/extractors/flask.py +400 -0
  37. pygraph/extractors/implements.py +61 -0
  38. pygraph/extractors/imports.py +161 -0
  39. pygraph/extractors/symbols.py +256 -0
  40. pygraph/extractors/tests.py +58 -0
  41. pygraph/graph/__init__.py +0 -0
  42. pygraph/graph/boundaries.py +40 -0
  43. pygraph/graph/cache.py +43 -0
  44. pygraph/graph/serialize.py +157 -0
  45. pygraph/graph/types.py +368 -0
  46. pygraph/query.py +975 -0
  47. pygraph/scanner/__init__.py +0 -0
  48. pygraph/scanner/gitignore.py +0 -0
  49. pygraph/scanner/walker.py +138 -0
  50. pygraph/server.py +442 -0
  51. pygraph_mcp-0.1.0.dist-info/METADATA +186 -0
  52. pygraph_mcp-0.1.0.dist-info/RECORD +54 -0
  53. pygraph_mcp-0.1.0.dist-info/WHEEL +4 -0
  54. pygraph_mcp-0.1.0.dist-info/entry_points.txt +2 -0
pygraph/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
pygraph/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pygraph.cli import app
2
+
3
+ app()
pygraph/builder.py ADDED
@@ -0,0 +1,561 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import subprocess
5
+ import sys
6
+ import traceback
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from pygraph.config import get_plugins
11
+ from pygraph.extractors.calls import extract_calls
12
+ from pygraph.extractors.env import extract_env_reads
13
+ from pygraph.extractors.errors import extract_errors
14
+ from pygraph.extractors.flask import extract_flask
15
+ from pygraph.extractors.implements import extract_implements
16
+ from pygraph.extractors.imports import extract_dependencies, extract_imports
17
+ from pygraph.extractors.symbols import extract_symbols
18
+ from pygraph.extractors.tests import extract_test_edges
19
+ from pygraph.graph.cache import BuildCache
20
+ from pygraph.graph.serialize import read_graph, write_graph
21
+ from pygraph.graph.types import (
22
+ BlueprintDef,
23
+ BlueprintRegistration,
24
+ CallEdge,
25
+ EnvRead,
26
+ ErrorEdge,
27
+ ExtensionUsage,
28
+ FileNode,
29
+ Graph,
30
+ HTTPRoute,
31
+ ImplementsEdge,
32
+ ImportEdge,
33
+ SymbolNode,
34
+ TemplateRef,
35
+ TestEdge,
36
+ make_graph,
37
+ make_package_node,
38
+ )
39
+ from pygraph.scanner.walker import ScannedFile, ScanResult, scan_files
40
+
41
+
42
+ def _get_file_mtime_size(path: str) -> tuple[float, int]:
43
+ p = Path(path)
44
+ stat = p.stat()
45
+ return stat.st_mtime_ns, stat.st_size
46
+
47
+
48
+ _ParseResult = tuple[
49
+ list[SymbolNode],
50
+ list[CallEdge],
51
+ list[ImportEdge],
52
+ list[HTTPRoute],
53
+ list[HTTPRoute],
54
+ list[HTTPRoute],
55
+ list[BlueprintDef],
56
+ list[BlueprintRegistration],
57
+ list[TemplateRef],
58
+ list[ExtensionUsage],
59
+ list[EnvRead],
60
+ list[ErrorEdge],
61
+ list[TestEdge],
62
+ list[ImplementsEdge],
63
+ FileNode,
64
+ ]
65
+
66
+
67
+ def _parse_source(
68
+ source: str, relative_path: str, pkg_name: str
69
+ ) -> _ParseResult:
70
+ symbols = extract_symbols(source, relative_path, pkg_name)
71
+ calls = extract_calls(source, relative_path)
72
+ imports = extract_imports(source, relative_path, pkg_name)
73
+ flask = extract_flask(source, relative_path)
74
+ env_reads = extract_env_reads(source, relative_path)
75
+ errors = extract_errors(source, relative_path)
76
+ test_edges = extract_test_edges(source, relative_path)
77
+ implements = extract_implements(source, relative_path)
78
+
79
+ file_node = FileNode(
80
+ id=relative_path,
81
+ path=relative_path,
82
+ package_name=pkg_name,
83
+ lines=len(source.split("\n")),
84
+ generated=False,
85
+ )
86
+
87
+ return (
88
+ symbols,
89
+ calls,
90
+ imports,
91
+ flask["routes"],
92
+ flask["error_handlers"],
93
+ flask["cli_commands"],
94
+ flask["blueprints"],
95
+ flask["blueprint_registrations"],
96
+ flask["template_refs"],
97
+ flask["extensions"],
98
+ env_reads,
99
+ errors,
100
+ test_edges,
101
+ implements,
102
+ file_node,
103
+ )
104
+
105
+
106
+ def _parse_file(
107
+ sf: ScannedFile, pkg_name: str
108
+ ) -> _ParseResult:
109
+ source = Path(sf.path).read_text()
110
+ result = _parse_source(source, sf.relative_path, pkg_name)
111
+ result[-1].generated = sf.is_generated
112
+ return result
113
+
114
+
115
+ def _build_full(root: str, scan_result: ScanResult) -> Graph:
116
+ root_path = Path(root).resolve()
117
+ pkg_name = root_path.name
118
+ py_files = [f for f in scan_result.files if f.kind == "py"]
119
+
120
+ all_symbols: list[SymbolNode] = []
121
+ all_calls: list[CallEdge] = []
122
+ all_imports: list[ImportEdge] = []
123
+ all_routes: list[HTTPRoute] = []
124
+ all_error_handlers: list[HTTPRoute] = []
125
+ all_cli_commands: list[HTTPRoute] = []
126
+ all_blueprints: list[BlueprintDef] = []
127
+ all_blueprint_registrations: list[BlueprintRegistration] = []
128
+ all_template_refs: list[TemplateRef] = []
129
+ all_extensions: list[ExtensionUsage] = []
130
+ all_env_reads: list[EnvRead] = []
131
+ all_errors: list[ErrorEdge] = []
132
+ all_test_edges: list[TestEdge] = []
133
+ all_implements: list[ImplementsEdge] = []
134
+ file_nodes: list[FileNode] = []
135
+
136
+ for sf in py_files:
137
+ try:
138
+ result = _parse_file(sf, pkg_name)
139
+ except OSError:
140
+ continue
141
+
142
+ (
143
+ symbols,
144
+ calls,
145
+ imports,
146
+ routes,
147
+ error_handlers,
148
+ cli_commands,
149
+ blueprints,
150
+ blueprint_registrations,
151
+ template_refs,
152
+ extensions,
153
+ env_reads,
154
+ errors,
155
+ test_edges,
156
+ implements,
157
+ file_node,
158
+ ) = result
159
+
160
+ all_symbols.extend(symbols)
161
+ all_calls.extend(calls)
162
+ all_imports.extend(imports)
163
+ all_routes.extend(routes)
164
+ all_error_handlers.extend(error_handlers)
165
+ all_cli_commands.extend(cli_commands)
166
+ all_blueprints.extend(blueprints)
167
+ all_blueprint_registrations.extend(blueprint_registrations)
168
+ all_template_refs.extend(template_refs)
169
+ all_extensions.extend(extensions)
170
+ all_env_reads.extend(env_reads)
171
+ all_errors.extend(errors)
172
+ all_test_edges.extend(test_edges)
173
+ all_implements.extend(implements)
174
+ file_nodes.append(file_node)
175
+
176
+ all_routes.extend(all_error_handlers)
177
+ all_routes.extend(all_cli_commands)
178
+
179
+ package = make_package_node(
180
+ name=pkg_name,
181
+ import_path_best_effort=pkg_name,
182
+ dir=str(root_path),
183
+ files=[sf.relative_path for sf in scan_result.files],
184
+ )
185
+
186
+ dependencies = extract_dependencies(root)
187
+
188
+ graph = make_graph(project_root=str(root_path))
189
+ graph.packages = [package]
190
+ graph.files = file_nodes
191
+ graph.symbols = all_symbols
192
+ graph.calls = all_calls
193
+ graph.imports = all_imports
194
+ graph.dependencies = dependencies
195
+ graph.routes = all_routes
196
+ graph.blueprints = all_blueprints
197
+ graph.blueprint_registrations = all_blueprint_registrations
198
+ graph.template_refs = all_template_refs
199
+ graph.extensions = all_extensions
200
+ graph.env_reads = all_env_reads
201
+ graph.errors = all_errors
202
+ graph.test_edges = all_test_edges
203
+ graph.implements = all_implements
204
+
205
+ return graph
206
+
207
+
208
+ def _index_by_file(objs: list[Any], file_attr: str) -> dict[str, list[Any]]:
209
+ idx: dict[str, list[Any]] = {}
210
+ for o in objs:
211
+ key = getattr(o, file_attr)
212
+ idx.setdefault(key, []).append(o)
213
+ return idx
214
+
215
+
216
+ def _merge_incremental(
217
+ root_path: Path,
218
+ scan_result: ScanResult,
219
+ old_graph: Graph,
220
+ cache: BuildCache,
221
+ cache_path: Path,
222
+ ) -> Graph:
223
+ pkg_name = root_path.name
224
+ py_files = [f for f in scan_result.files if f.kind == "py"]
225
+ scanned_paths = {sf.relative_path for sf in py_files}
226
+
227
+ old_file_paths = {f.path for f in old_graph.files}
228
+
229
+ changed_paths: set[str] = set()
230
+ unchanged_paths: set[str] = set()
231
+
232
+ for sf in py_files:
233
+ mtime_ns, size = _get_file_mtime_size(sf.path)
234
+ if cache.is_changed(sf.relative_path, mtime_ns, size):
235
+ changed_paths.add(sf.relative_path)
236
+ else:
237
+ unchanged_paths.add(sf.relative_path)
238
+ cache.set(sf.relative_path, mtime_ns, size)
239
+
240
+ deleted_paths = old_file_paths - scanned_paths
241
+ for p in deleted_paths:
242
+ cache.remove(p)
243
+
244
+ old_symbols_by_file = _index_by_file(old_graph.symbols, "file")
245
+ old_calls_by_file = _index_by_file(old_graph.calls, "file")
246
+ old_imports_by_file = _index_by_file(old_graph.imports, "from_file")
247
+ old_routes_by_file = _index_by_file(old_graph.routes, "file")
248
+ old_blueprints_by_file = _index_by_file(old_graph.blueprints, "file")
249
+ old_blueprint_regs_by_file = _index_by_file(
250
+ old_graph.blueprint_registrations, "file"
251
+ )
252
+ old_template_refs_by_file = _index_by_file(old_graph.template_refs, "file")
253
+ old_extensions_by_file = _index_by_file(old_graph.extensions, "file")
254
+ old_env_reads_by_file = _index_by_file(old_graph.env_reads, "file")
255
+ old_errors_by_file = _index_by_file(old_graph.errors, "file")
256
+ old_test_edges_by_file = _index_by_file(old_graph.test_edges, "file")
257
+ old_implements_by_file = _index_by_file(old_graph.implements, "file")
258
+
259
+ all_symbols: list[SymbolNode] = []
260
+ all_calls: list[CallEdge] = []
261
+ all_imports: list[ImportEdge] = []
262
+ all_routes: list[HTTPRoute] = []
263
+ all_blueprints: list[BlueprintDef] = []
264
+ all_blueprint_registrations: list[BlueprintRegistration] = []
265
+ all_template_refs: list[TemplateRef] = []
266
+ all_extensions: list[ExtensionUsage] = []
267
+ all_env_reads: list[EnvRead] = []
268
+ all_errors: list[ErrorEdge] = []
269
+ all_test_edges: list[TestEdge] = []
270
+ all_implements: list[ImplementsEdge] = []
271
+ file_nodes: list[FileNode] = []
272
+
273
+ for sf in py_files:
274
+ rel = sf.relative_path
275
+
276
+ if rel in unchanged_paths:
277
+ all_symbols.extend(old_symbols_by_file.get(rel, []))
278
+ all_calls.extend(old_calls_by_file.get(rel, []))
279
+ all_imports.extend(old_imports_by_file.get(rel, []))
280
+ all_routes.extend(old_routes_by_file.get(rel, []))
281
+ all_blueprints.extend(old_blueprints_by_file.get(rel, []))
282
+ all_blueprint_registrations.extend(
283
+ old_blueprint_regs_by_file.get(rel, [])
284
+ )
285
+ all_template_refs.extend(old_template_refs_by_file.get(rel, []))
286
+ all_extensions.extend(old_extensions_by_file.get(rel, []))
287
+ all_env_reads.extend(old_env_reads_by_file.get(rel, []))
288
+ all_errors.extend(old_errors_by_file.get(rel, []))
289
+ all_test_edges.extend(old_test_edges_by_file.get(rel, []))
290
+ all_implements.extend(old_implements_by_file.get(rel, []))
291
+ old_fn = next(
292
+ (f for f in old_graph.files if f.path == rel), None
293
+ )
294
+ if old_fn:
295
+ file_nodes.append(old_fn)
296
+ continue
297
+
298
+ try:
299
+ result = _parse_file(sf, pkg_name)
300
+ except OSError:
301
+ continue
302
+
303
+ (
304
+ symbols,
305
+ calls,
306
+ imports,
307
+ routes,
308
+ error_handlers,
309
+ cli_commands,
310
+ blueprints,
311
+ blueprint_registrations,
312
+ template_refs,
313
+ extensions,
314
+ env_reads,
315
+ errors,
316
+ test_edges,
317
+ implements,
318
+ file_node,
319
+ ) = result
320
+
321
+ all_symbols.extend(symbols)
322
+ all_calls.extend(calls)
323
+ all_imports.extend(imports)
324
+ all_routes.extend(routes)
325
+ all_routes.extend(error_handlers)
326
+ all_routes.extend(cli_commands)
327
+ all_blueprints.extend(blueprints)
328
+ all_blueprint_registrations.extend(blueprint_registrations)
329
+ all_template_refs.extend(template_refs)
330
+ all_extensions.extend(extensions)
331
+ all_env_reads.extend(env_reads)
332
+ all_errors.extend(errors)
333
+ all_test_edges.extend(test_edges)
334
+ all_implements.extend(implements)
335
+ file_nodes.append(file_node)
336
+
337
+ package = make_package_node(
338
+ name=pkg_name,
339
+ import_path_best_effort=pkg_name,
340
+ dir=str(root_path),
341
+ files=[sf.relative_path for sf in scan_result.files],
342
+ )
343
+
344
+ dependencies = extract_dependencies(str(root_path))
345
+
346
+ graph = make_graph(project_root=str(root_path))
347
+ graph.packages = [package]
348
+ graph.files = file_nodes
349
+ graph.symbols = all_symbols
350
+ graph.calls = all_calls
351
+ graph.imports = all_imports
352
+ graph.dependencies = dependencies
353
+ graph.routes = all_routes
354
+ graph.blueprints = all_blueprints
355
+ graph.blueprint_registrations = all_blueprint_registrations
356
+ graph.template_refs = all_template_refs
357
+ graph.extensions = all_extensions
358
+ graph.env_reads = all_env_reads
359
+ graph.errors = all_errors
360
+ graph.test_edges = all_test_edges
361
+ graph.implements = all_implements
362
+
363
+ cache.save(cache_path)
364
+ return graph
365
+
366
+
367
+ def _run_plugins(graph: Graph, root: str) -> None:
368
+ plugins = get_plugins(root)
369
+ if not plugins:
370
+ return
371
+ root_path = Path(root).resolve()
372
+ for plugin_rel in plugins:
373
+ plugin_path = (root_path / plugin_rel).resolve()
374
+ if not plugin_path.exists():
375
+ print(f"[pygraph] plugin not found: {plugin_path}", file=sys.stderr)
376
+ continue
377
+ try:
378
+ spec = importlib.util.spec_from_file_location(
379
+ f"_pygraph_plugin_{plugin_path.stem}", plugin_path
380
+ )
381
+ if spec is None or spec.loader is None:
382
+ print(f"[pygraph] failed to load plugin: {plugin_path}", file=sys.stderr)
383
+ continue
384
+ mod = importlib.util.module_from_spec(spec)
385
+ sys.modules[mod.__name__] = mod
386
+ spec.loader.exec_module(mod)
387
+ if not hasattr(mod, "run"):
388
+ print(
389
+ f"[pygraph] plugin {plugin_path} has no run(graph) function",
390
+ file=sys.stderr,
391
+ )
392
+ continue
393
+ mod.run(graph)
394
+ except Exception:
395
+ print(
396
+ f"[pygraph] plugin {plugin_path} raised an error:",
397
+ file=sys.stderr,
398
+ )
399
+ traceback.print_exc(file=sys.stderr)
400
+
401
+
402
+ def build_graph(root: str, incremental: bool = True) -> Graph:
403
+ root_path = Path(root).resolve()
404
+ scan_result = scan_files(root)
405
+
406
+ if incremental:
407
+ graph_path = root_path / ".pygraph" / "graph.json"
408
+ cache_path = root_path / ".pygraph" / ".build_cache.json"
409
+
410
+ if graph_path.exists() and cache_path.exists():
411
+ old_graph = read_graph(graph_path)
412
+ old_cache = BuildCache.load(cache_path)
413
+ graph = _merge_incremental(
414
+ root_path, scan_result, old_graph, old_cache, cache_path
415
+ )
416
+ _run_plugins(graph, root)
417
+ return graph
418
+
419
+ graph = _build_full(root, scan_result)
420
+ _run_plugins(graph, root)
421
+ return graph
422
+
423
+
424
+ def build_graph_from_ref(ref: str, root: str) -> Graph:
425
+ root_path = Path(root).resolve()
426
+ if not root_path.exists():
427
+ raise ValueError(f"Root path '{root}' does not exist")
428
+ pkg_name = root_path.name
429
+
430
+ try:
431
+ result = subprocess.run(
432
+ ["git", "ls-tree", "-r", ref, "--name-only"],
433
+ capture_output=True, text=True, timeout=30,
434
+ cwd=str(root_path),
435
+ )
436
+ if result.returncode != 0:
437
+ raise ValueError(f"Could not list files at ref '{ref}'")
438
+ py_files = [f for f in result.stdout.splitlines() if f.endswith(".py")]
439
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
440
+ raise ValueError(f"Could not list files at ref '{ref}'") from None
441
+
442
+ all_symbols: list[SymbolNode] = []
443
+ all_calls: list[CallEdge] = []
444
+ all_imports: list[ImportEdge] = []
445
+ all_routes: list[HTTPRoute] = []
446
+ all_error_handlers: list[HTTPRoute] = []
447
+ all_cli_commands: list[HTTPRoute] = []
448
+ all_blueprints: list[BlueprintDef] = []
449
+ all_blueprint_registrations: list[BlueprintRegistration] = []
450
+ all_template_refs: list[TemplateRef] = []
451
+ all_extensions: list[ExtensionUsage] = []
452
+ all_env_reads: list[EnvRead] = []
453
+ all_errors: list[ErrorEdge] = []
454
+ all_test_edges: list[TestEdge] = []
455
+ all_implements: list[ImplementsEdge] = []
456
+ file_nodes: list[FileNode] = []
457
+
458
+ for rel_path in py_files:
459
+ try:
460
+ content = subprocess.run(
461
+ ["git", "show", f"{ref}:{rel_path}"],
462
+ capture_output=True, text=True, timeout=10,
463
+ cwd=str(root_path),
464
+ )
465
+ if content.returncode != 0 or not content.stdout:
466
+ continue
467
+ source = content.stdout
468
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
469
+ continue
470
+
471
+ parsed = _parse_source(source, rel_path, pkg_name)
472
+
473
+ (
474
+ symbols, calls, imports,
475
+ routes, error_handlers, cli_commands,
476
+ blueprints, blueprint_registrations, template_refs, extensions,
477
+ env_reads, errors, test_edges, implements, file_node,
478
+ ) = parsed
479
+
480
+ all_symbols.extend(symbols)
481
+ all_calls.extend(calls)
482
+ all_imports.extend(imports)
483
+ all_routes.extend(routes)
484
+ all_error_handlers.extend(error_handlers)
485
+ all_cli_commands.extend(cli_commands)
486
+ all_blueprints.extend(blueprints)
487
+ all_blueprint_registrations.extend(blueprint_registrations)
488
+ all_template_refs.extend(template_refs)
489
+ all_extensions.extend(extensions)
490
+ all_env_reads.extend(env_reads)
491
+ all_errors.extend(errors)
492
+ all_test_edges.extend(test_edges)
493
+ all_implements.extend(implements)
494
+ file_nodes.append(file_node)
495
+
496
+ all_routes.extend(all_error_handlers)
497
+ all_routes.extend(all_cli_commands)
498
+
499
+ package = make_package_node(
500
+ name=pkg_name,
501
+ import_path_best_effort=pkg_name,
502
+ dir=str(root_path),
503
+ files=py_files,
504
+ )
505
+
506
+ graph = make_graph(project_root=str(root_path))
507
+ graph.packages = [package]
508
+ graph.files = file_nodes
509
+ graph.symbols = all_symbols
510
+ graph.calls = all_calls
511
+ graph.imports = all_imports
512
+ graph.routes = all_routes
513
+ graph.blueprints = all_blueprints
514
+ graph.blueprint_registrations = all_blueprint_registrations
515
+ graph.template_refs = all_template_refs
516
+ graph.extensions = all_extensions
517
+ graph.env_reads = all_env_reads
518
+ graph.errors = all_errors
519
+ graph.test_edges = all_test_edges
520
+ graph.implements = all_implements
521
+
522
+ return graph
523
+
524
+
525
+ def resolve_git_ref(ref: str, root: str = "") -> str | None:
526
+ try:
527
+ cwd = root if root and Path(root).exists() else None
528
+ result = subprocess.run(
529
+ ["git", "rev-parse", ref],
530
+ capture_output=True, text=True, timeout=10,
531
+ cwd=cwd,
532
+ )
533
+ if result.returncode == 0:
534
+ return result.stdout.strip()
535
+ return None
536
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
537
+ return None
538
+
539
+
540
+ def build_and_write(root: str, incremental: bool = True) -> Path:
541
+ graph = build_graph(root, incremental)
542
+ out_path = Path(root) / ".pygraph" / "graph.json"
543
+ write_graph(graph, out_path)
544
+
545
+ cache_path = Path(root) / ".pygraph" / ".build_cache.json"
546
+ if not incremental or not cache_path.exists():
547
+ cache = BuildCache({})
548
+ try:
549
+ scan_result = scan_files(root)
550
+ for sf in scan_result.files:
551
+ if sf.kind == "py":
552
+ try:
553
+ mtime_ns, size = _get_file_mtime_size(sf.path)
554
+ cache.set(sf.relative_path, mtime_ns, size)
555
+ except OSError:
556
+ pass
557
+ except OSError:
558
+ pass
559
+ cache.save(cache_path)
560
+
561
+ return out_path