workspace-graph 0.3.2__tar.gz → 0.3.4__tar.gz

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 (50) hide show
  1. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/PKG-INFO +1 -1
  2. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/pyproject.toml +1 -1
  3. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/query.py +41 -0
  4. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/server.py +111 -25
  5. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/uv.lock +1 -1
  6. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.github/workflows/publish.yml +0 -0
  7. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.gitignore +0 -0
  8. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.pygraph/.build_cache.json +0 -0
  9. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.pygraph/graph.json +0 -0
  10. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/AGENTS.md +0 -0
  11. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/README.md +0 -0
  12. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/TODOS.md +0 -0
  13. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/scripts/install.sh +0 -0
  14. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/scripts/uninstall.sh +0 -0
  15. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/__init__.py +0 -0
  16. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/__main__.py +0 -0
  17. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/builder.py +0 -0
  18. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/cli.py +0 -0
  19. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/__init__.py +0 -0
  20. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/build.py +0 -0
  21. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/callees.py +0 -0
  22. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/callers.py +0 -0
  23. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/clean.py +0 -0
  24. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/context.py +0 -0
  25. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/cross_service.py +0 -0
  26. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/impact.py +0 -0
  27. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/opencode_plugin.py +0 -0
  28. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/orphans.py +0 -0
  29. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/query_cmd.py +0 -0
  30. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/routes.py +0 -0
  31. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/status.py +0 -0
  32. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/trace.py +0 -0
  33. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/config.py +0 -0
  34. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/cross_service.py +0 -0
  35. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/discover.py +0 -0
  36. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/graph/__init__.py +0 -0
  37. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/graph/serialize.py +0 -0
  38. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/graph/types.py +0 -0
  39. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/plugin.py +0 -0
  40. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/__init__.py +0 -0
  41. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/conftest.py +0 -0
  42. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_builder.py +0 -0
  43. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_config.py +0 -0
  44. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_cross_service.py +0 -0
  45. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_discover.py +0 -0
  46. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_opencode_plugin.py +0 -0
  47. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_plugins.py +0 -0
  48. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_query.py +0 -0
  49. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_serialize.py +0 -0
  50. {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-graph
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Multi-language workspace code graph orchestrator
5
5
  Project-URL: Homepage, https://github.com/shvmgyl15/codegraph
6
6
  Project-URL: Repository, https://github.com/shvmgyl15/codegraph
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workspace-graph"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "Multi-language workspace code graph orchestrator"
5
5
  requires-python = ">=3.11"
6
6
  authors = [{name = "Shivam Goel", email = "sg15rokz@gmail.com"}]
@@ -511,6 +511,9 @@ class WorkspaceQuery:
511
511
  if callee_receiver and caller_class and callee_receiver == caller_class:
512
512
  self_ref_count += 1
513
513
  continue
514
+ if cs is None and ce.get("callee_raw", "").startswith(("self.", "cls.", "self[")):
515
+ self_ref_count += 1
516
+ continue
514
517
  if filter_builtins and callee_name in PYTHON_BUILTINS:
515
518
  builtin_count += 1
516
519
  continue
@@ -775,6 +778,44 @@ class WorkspaceQuery:
775
778
  except OSError:
776
779
  return None
777
780
 
781
+ def _extract_call_args(
782
+ self, file_path: str, line: int, callee_raw: str
783
+ ) -> dict[str, Any]:
784
+ source = self._load_source(file_path)
785
+ if source is None:
786
+ return {"args": [], "kwargs": {}}
787
+ try:
788
+ import ast
789
+ tree = ast.parse(source)
790
+ except SyntaxError:
791
+ return {"args": [], "kwargs": {}}
792
+ for node in ast.walk(tree):
793
+ if not isinstance(node, ast.Call) or getattr(node, "lineno", None) != line:
794
+ continue
795
+ parsed = ast.unparse(node.func)
796
+ name_match = (
797
+ callee_raw.startswith(parsed) or parsed.endswith(callee_raw)
798
+ )
799
+ if not name_match:
800
+ continue
801
+ args: list[str] = []
802
+ for arg in node.args:
803
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
804
+ args.append(arg.value)
805
+ elif isinstance(arg, ast.Name):
806
+ args.append(arg.id)
807
+
808
+ kwargs: dict[str, str] = {}
809
+ for kw in node.keywords:
810
+ if kw.arg is None:
811
+ continue
812
+ if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
813
+ kwargs[kw.arg] = kw.value.value
814
+ elif isinstance(kw.value, ast.Name):
815
+ kwargs[kw.arg] = kw.value.id
816
+ return {"args": args, "kwargs": kwargs}
817
+ return {"args": [], "kwargs": {}}
818
+
778
819
  def get_trace(self, error_message: str) -> list[dict[str, Any]]:
779
820
  matching: list[dict[str, Any]] = []
780
821
  for err in self.graph.errors:
@@ -300,31 +300,82 @@ def routes(
300
300
 
301
301
  if include_route_wrappers:
302
302
  wrapper_names = list(q.list_classifications(kind="route_wrapper").get("symbols", {}).keys())
303
- if wrapper_names:
304
- seen_calls: set[tuple[str, str, int]] = set()
305
- for call in q.graph.calls:
306
- craw = call.get("callee_raw", "")
307
- for wrapper in wrapper_names:
308
- if wrapper not in craw:
309
- continue
310
- cid = call.get("caller_symbol_id", "")
311
- lineno = call.get("line", 0)
312
- k = (cid, craw, lineno)
313
- if k in seen_calls:
314
- continue
315
- seen_calls.add(k)
316
- caller_sym = q._symbols_by_id.get(cid)
317
- caller_name = caller_sym.get("name", "") if caller_sym else "<module>"
318
- results.append({
319
- "method": "WRAPPER",
320
- "path": f"[{craw}]",
321
- "handler": caller_name,
322
- "file": call.get("file", ""),
323
- "line": lineno,
324
- "entry_name": call.get("entry_name", ""),
325
- "language": call.get("language", ""),
326
- "type": call.get("type", ""),
327
- })
303
+ route_patterns = q._classification.get("route_patterns", {})
304
+ seen_calls: set[tuple[str, str, int]] = set()
305
+
306
+ for call in q.graph.calls:
307
+ craw = call.get("callee_raw", "")
308
+ matching_wrappers = [w for w in wrapper_names if w in craw]
309
+ if not matching_wrappers:
310
+ continue
311
+
312
+ cid = call.get("caller_symbol_id", "")
313
+ lineno = call.get("line", 0)
314
+ k = (cid, craw, lineno)
315
+ if k in seen_calls:
316
+ continue
317
+ seen_calls.add(k)
318
+
319
+ caller_sym = q._symbols_by_id.get(cid)
320
+ caller_name = caller_sym.get("name", "") if caller_sym else "<module>"
321
+
322
+ wrapper_name = matching_wrappers[0]
323
+ config = route_patterns.get(wrapper_name, {})
324
+ path_idx = config.get("path_arg_index", 0)
325
+ class_idx = config.get("class_arg_index")
326
+
327
+ if config:
328
+ raw_args = q._extract_call_args(
329
+ call.get("file", ""), lineno, craw,
330
+ )
331
+ path_args = raw_args.get("args", [])
332
+ paths = [path_args[path_idx]] if path_idx < len(path_args) else [f"[{craw}]"]
333
+
334
+ http_methods = ["GET"]
335
+ cls_name = ""
336
+ if class_idx is not None and class_idx < len(path_args):
337
+ cls_name = path_args[class_idx]
338
+ cls_methods = q._methods_by_class.get(cls_name, [])
339
+ if cls_methods:
340
+ http_methods = []
341
+ for m in cls_methods:
342
+ mname = m.get("name", "").lower()
343
+ method_map = {
344
+ "get": "GET", "post": "POST", "put": "PUT",
345
+ "delete": "DELETE", "patch": "PATCH",
346
+ "head": "HEAD", "options": "OPTIONS",
347
+ }
348
+ mapped = method_map.get(mname)
349
+ if mapped:
350
+ http_methods.append(mapped)
351
+ if not http_methods:
352
+ http_methods = ["GET"]
353
+
354
+ for path in paths:
355
+ for method in http_methods:
356
+ results.append({
357
+ "method": method,
358
+ "path": path,
359
+ "handler": cls_name if cls_name else caller_name,
360
+ "file": call.get("file", ""),
361
+ "line": lineno,
362
+ "entry_name": call.get("entry_name", ""),
363
+ "language": call.get("language", ""),
364
+ "type": call.get("type", ""),
365
+ "detected_by": "route_pattern",
366
+ })
367
+ else:
368
+ results.append({
369
+ "method": "WRAPPER",
370
+ "path": f"[{craw}]",
371
+ "handler": caller_name,
372
+ "file": call.get("file", ""),
373
+ "line": lineno,
374
+ "entry_name": call.get("entry_name", ""),
375
+ "language": call.get("language", ""),
376
+ "type": call.get("type", ""),
377
+ "detected_by": "route_classification",
378
+ })
328
379
 
329
380
  items = [
330
381
  {
@@ -579,6 +630,41 @@ def classify_discover(
579
630
  return result
580
631
 
581
632
 
633
+ @server.tool()
634
+ def define_route_pattern(
635
+ name: str,
636
+ path_arg_index: int = 0,
637
+ class_arg_index: int | None = None,
638
+ root: str = ".",
639
+ ) -> dict[str, Any]:
640
+ """Define a custom route wrapper pattern for synthetic route detection.
641
+ name: function name (e.g. 'register_path', 'add_resource')
642
+ path_arg_index: which positional arg contains the route path (0-based)
643
+ class_arg_index: which positional arg contains the handler class (optional)
644
+
645
+ After defining a pattern, use classify_symbol(kind='route_wrapper') to
646
+ activate detection, then routes(include_route_wrappers=True) shows results.
647
+ Stored in .codegraph/classification.json — persists across sessions.
648
+ """
649
+ _s = time.monotonic()
650
+ q = create_query(root)
651
+ data = q._classification
652
+ patterns = data.setdefault("route_patterns", {})
653
+ patterns[name] = {
654
+ "path_arg_index": path_arg_index,
655
+ "class_arg_index": class_arg_index,
656
+ }
657
+ from codegraph.query import _save_classifications
658
+ _save_classifications(root, data)
659
+ q._classification["route_patterns"] = patterns
660
+ return {
661
+ "defined": name,
662
+ "path_arg_index": path_arg_index,
663
+ "class_arg_index": class_arg_index,
664
+ "duration_ms": _duration(_s),
665
+ }
666
+
667
+
582
668
  def run_server(root: str = ".") -> None:
583
669
  with suppress(FileNotFoundError):
584
670
  create_query(root)
@@ -1276,7 +1276,7 @@ wheels = [
1276
1276
 
1277
1277
  [[package]]
1278
1278
  name = "workspace-graph"
1279
- version = "0.3.2"
1279
+ version = "0.3.4"
1280
1280
  source = { editable = "." }
1281
1281
  dependencies = [
1282
1282
  { name = "mcp" },