codegraphcontext 0.2.12__py3-none-any.whl → 0.3.1__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.
@@ -4,8 +4,18 @@ import uuid
4
4
  import urllib.parse
5
5
  from pathlib import Path
6
6
  import time
7
+ from typing import Optional
7
8
  from rich.console import Console
8
9
  from rich.table import Table
10
+ from rich.progress import (
11
+ Progress,
12
+ SpinnerColumn,
13
+ TextColumn,
14
+ BarColumn,
15
+ TaskProgressColumn,
16
+ TimeRemainingColumn,
17
+ MofNCompleteColumn,
18
+ )
9
19
 
10
20
  from ..core import get_database_manager
11
21
  from ..core.jobs import JobManager
@@ -67,6 +77,64 @@ def _initialize_services():
67
77
  return db_manager, graph_builder, code_finder
68
78
 
69
79
 
80
+ async def _run_index_with_progress(graph_builder: GraphBuilder, path_obj: Path, is_dependency: bool = False):
81
+ """Internal helper to run indexing with a Live progress bar."""
82
+ job_id = graph_builder.job_manager.create_job(str(path_obj), is_dependency=is_dependency)
83
+
84
+ # Create the progress bar
85
+ with Progress(
86
+ SpinnerColumn(),
87
+ TextColumn("[progress.description]{task.description}"),
88
+ BarColumn(),
89
+ TaskProgressColumn(),
90
+ MofNCompleteColumn(),
91
+ TimeRemainingColumn(),
92
+ TextColumn("[dim]{task.fields[filename]}"),
93
+ console=console,
94
+ transient=True,
95
+ ) as progress:
96
+
97
+ task_id = progress.add_task(
98
+ "Indexing...",
99
+ total=None, # Will be updated once file discovery is done
100
+ filename=""
101
+ )
102
+
103
+ indexing_task = asyncio.create_task(
104
+ graph_builder.build_graph_from_path_async(path_obj, is_dependency=is_dependency, job_id=job_id)
105
+ )
106
+
107
+ from ..core.jobs import JobStatus
108
+
109
+ # Poll for updates
110
+ while not indexing_task.done():
111
+ job = graph_builder.job_manager.get_job(job_id)
112
+ if job:
113
+ if job.total_files > 0:
114
+ progress.update(task_id, total=job.total_files, completed=job.processed_files)
115
+
116
+ # Update the current filename in the UI
117
+ current_file = job.current_file or ""
118
+ if len(current_file) > 40:
119
+ current_file = "..." + current_file[-37:]
120
+ progress.update(task_id, filename=current_file)
121
+
122
+ if job.status in [JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED]:
123
+ break
124
+
125
+ await asyncio.sleep(0.1)
126
+
127
+ # Wait for actual completion and handle final state
128
+ try:
129
+ await indexing_task
130
+ job = graph_builder.job_manager.get_job(job_id)
131
+ if job and job.status == JobStatus.FAILED:
132
+ error_msg = job.errors[0] if job.errors else "Unknown error"
133
+ raise RuntimeError(error_msg)
134
+ except Exception as e:
135
+ raise e
136
+
137
+
70
138
  def index_helper(path: str):
71
139
  """Synchronously indexes a repository."""
72
140
  time_start = time.time()
@@ -107,13 +175,9 @@ def index_helper(path: str):
107
175
  console.print(f"[yellow]Warning: Could not check file count: {e}. Proceeding with indexing...[/yellow]")
108
176
 
109
177
  console.print(f"Starting indexing for: {path_obj}")
110
- console.print("[yellow]This may take a few minutes for large repositories...[/yellow]")
111
-
112
- async def do_index():
113
- await graph_builder.build_graph_from_path_async(path_obj, is_dependency=False)
114
178
 
115
179
  try:
116
- asyncio.run(do_index())
180
+ asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False))
117
181
  time_end = time.time()
118
182
  elapsed = time_end - time_start
119
183
  console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
@@ -159,13 +223,9 @@ def add_package_helper(package_name: str, language: str):
159
223
  return
160
224
 
161
225
  console.print(f"Starting indexing for package '{package_name}' at: {package_path}")
162
- console.print("[yellow]This may take a few minutes...[/yellow]")
163
-
164
- async def do_index():
165
- await graph_builder.build_graph_from_path_async(package_path, is_dependency=True)
166
226
 
167
227
  try:
168
- asyncio.run(do_index())
228
+ asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True))
169
229
  console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
170
230
  except Exception as e:
171
231
  console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
@@ -280,44 +340,55 @@ def cypher_helper_visual(query: str):
280
340
  console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
281
341
  finally:
282
342
  db_manager.close_driver()
283
-
284
-
285
343
  import webbrowser
344
+ import urllib.parse
345
+ from ..viz.server import run_server, set_db_manager
286
346
 
287
- def visualize_helper(query: str):
288
- """"Generates a visualization."""
347
+ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000):
348
+ """"Generates an interactive visualization using the Playground UI."""
289
349
  services = _initialize_services()
290
350
  if not all(services):
291
351
  return
292
352
 
293
353
  db_manager, _, _ = services
294
354
 
295
- # Check Backend Type
296
- backend = getattr(db_manager, "name", "").lower()
297
- if not backend:
298
- # Fallback check
299
- if "FalkorDB" in db_manager.__class__.__name__:
300
- backend = "falkordb"
301
- elif "Kuzu" in db_manager.__class__.__name__:
302
- backend = "kuzudb"
303
- else:
304
- backend = "neo4j"
305
-
306
- if backend == "falkordb":
307
- _visualize_falkordb(db_manager)
308
- elif backend == "kuzudb":
309
- _visualize_kuzudb(db_manager)
310
- else:
311
- try:
312
- encoded_query = urllib.parse.quote(query)
313
- visualization_url = f"http://localhost:7474/browser/?cmd=edit&arg={encoded_query}"
314
- console.print("[green]Graph visualization URL:[/green]")
315
- console.print(visualization_url)
316
- console.print("Open the URL in your browser to see the graph.")
317
- except Exception as e:
318
- console.print(f"[bold red]An error occurred while generating URL:[/bold red] {e}")
319
- finally:
320
- db_manager.close_driver()
355
+ # Set the DB manager for the server
356
+ set_db_manager(db_manager)
357
+
358
+ # Determine the static directory (built React app)
359
+ static_dir = Path(__file__).parent.parent / "viz" / "dist"
360
+ if not static_dir.exists():
361
+ console.print("[yellow]Warning: Visualizer UI assets not found in package. Using fallback static dir.[/yellow]")
362
+ # Fallback for development
363
+ static_dir = Path.cwd() / "website" / "dist"
364
+
365
+ # Construct the URL
366
+ backend_url = f"http://localhost:{port}"
367
+ params = {"backend": backend_url}
368
+ if repo_path:
369
+ params["repo_path"] = str(Path(repo_path).resolve())
370
+
371
+ query_string = urllib.parse.urlencode(params)
372
+ visualization_url = f"{backend_url}/playground?{query_string}"
373
+
374
+ console.print(f"[green]Starting visualizer server on {backend_url}...[/green]")
375
+ console.print(f"[cyan]Opening Playground UI:[/cyan] {visualization_url}")
376
+
377
+ # Open browser in a separate thread/process if possible, or just before starting server
378
+ def open_browser():
379
+ import time
380
+ time.sleep(1.5) # Give the server a moment to start
381
+ webbrowser.open(visualization_url)
382
+
383
+ import threading
384
+ threading.Thread(target=open_browser, daemon=True).start()
385
+
386
+ try:
387
+ run_server(host="127.0.0.1", port=port, static_dir=str(static_dir))
388
+ except Exception as e:
389
+ console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
390
+ finally:
391
+ db_manager.close_driver()
321
392
 
322
393
  def _visualize_falkordb(db_manager):
323
394
  console.print("[dim]Generating FalkorDB visualization (showing up to 500 relationships)...[/dim]")
@@ -587,13 +658,9 @@ def reindex_helper(path: str):
587
658
  return
588
659
 
589
660
  console.print(f"[cyan]Re-indexing: {path_obj}[/cyan]")
590
- console.print("[yellow]This may take a few minutes for large repositories...[/yellow]")
591
-
592
- async def do_index():
593
- await graph_builder.build_graph_from_path_async(path_obj, is_dependency=False)
594
-
661
+
595
662
  try:
596
- asyncio.run(do_index())
663
+ asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False))
597
664
  time_end = time.time()
598
665
  elapsed = time_end - time_start
599
666
  console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
@@ -994,15 +994,15 @@ def delete(
994
994
  delete_helper(path)
995
995
 
996
996
  @app.command()
997
- def visualize(query: Optional[str] = typer.Argument(None, help="The Cypher query to visualize.")):
997
+ def visualize(
998
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Path to the repository to visualize."),
999
+ port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on.")
1000
+ ):
998
1001
  """
999
- Generates a URL to visualize a Cypher query in the Neo4j Browser.
1000
- If no query is provided, a default query will be used.
1002
+ Launches the interactive Playground UI to visualize the code graph.
1001
1003
  """
1002
- if query is None:
1003
- query = "MATCH p=()-->() RETURN p"
1004
1004
  _load_credentials()
1005
- visualize_helper(query)
1005
+ visualize_helper(repo, port)
1006
1006
 
1007
1007
  @app.command("list")
1008
1008
  def list_repositories():
@@ -2145,9 +2145,13 @@ def delete_abbrev(
2145
2145
  delete(path, all_repos)
2146
2146
 
2147
2147
  @app.command("v", rich_help_panel="Shortcuts")
2148
- def visualize_abbrev(query: Optional[str] = typer.Argument(None, help="Cypher query")):
2148
+ def visualize_abbrev(
2149
+ repo: Optional[str] = typer.Argument(None, help="Path to the repository to visualize."),
2150
+ port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on.")
2151
+ ):
2149
2152
  """Shortcut for 'cgc visualize'"""
2150
- visualize(query)
2153
+ _load_credentials()
2154
+ visualize_helper(repo, port)
2151
2155
 
2152
2156
  @app.command("w", rich_help_panel="Shortcuts")
2153
2157
  def watch_abbrev(path: str = typer.Argument(".", help="Path to watch")):
@@ -125,7 +125,14 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
125
125
  else:
126
126
  raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', or 'neo4j'.")
127
127
 
128
- # 4. Implicit Default -> FalkorDB Lite (Unix Zero Config)
128
+ # 4. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
129
+ # This takes priority over zero-config local backends because it's an explicit signal
130
+ if _is_falkordb_remote_configured():
131
+ from .database_falkordb_remote import FalkorDBRemoteManager
132
+ info_logger("Using remote FalkorDB (auto-detected via FALKORDB_HOST)")
133
+ return FalkorDBRemoteManager()
134
+
135
+ # 5. Implicit Default -> FalkorDB Lite (Unix Zero Config)
129
136
  if _is_falkordb_available():
130
137
  from .database_falkordb import FalkorDBManager, FalkorDBUnavailableError
131
138
  try:
@@ -139,18 +146,12 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
139
146
  )
140
147
  # fall through to KùzuDB below
141
148
 
142
- # 5. Implicit Default -> KùzuDB (Best Zero Config)
149
+ # 6. Implicit Default -> KùzuDB (Best Zero Config)
143
150
  if _is_kuzudb_available():
144
151
  from .database_kuzu import KuzuDBManager
145
152
  info_logger("Using KùzuDB (default)")
146
153
  return KuzuDBManager()
147
154
 
148
- # 6. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
149
- if _is_falkordb_remote_configured():
150
- from .database_falkordb_remote import FalkorDBRemoteManager
151
- info_logger("Using remote FalkorDB (auto-detected via FALKORDB_HOST)")
152
- return FalkorDBRemoteManager()
153
-
154
155
  # 7. Fallback if configured
155
156
  if _is_neo4j_configured():
156
157
  from .database import DatabaseManager
@@ -1,6 +1,6 @@
1
1
  # src/codegraphcontext/tools/code_finder.py
2
2
  import logging
3
- import re
3
+
4
4
  from typing import Any, Dict, List, Literal, Optional
5
5
  from pathlib import Path
6
6
 
@@ -348,7 +348,6 @@ class CodeFinder:
348
348
  def what_does_function_call(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
349
349
  """Find what functions a specific function calls using CALLS relationships"""
350
350
  with self.driver.session() as session:
351
- repo_filter = "AND called.path STARTS WITH $repo_path" if repo_path else ""
352
351
  if path:
353
352
  # Convert path to absolute path
354
353
  absolute_file_path = str(Path(path).resolve())
@@ -450,16 +449,16 @@ class CodeFinder:
450
449
  def find_class_hierarchy(self, class_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
451
450
  """Find class inheritance relationships using INHERITS relationships"""
452
451
  with self.driver.session() as session:
453
- repo_filter = "WHERE 1=1 AND parent.path STARTS WITH $repo_path" if repo_path else ""
452
+ repo_filter = "AND parent.path STARTS WITH $repo_path" if repo_path else ""
454
453
  if path:
455
454
  match_clause = "MATCH (child:Class {name: $class_name, path: $path})"
456
455
  else:
457
456
  match_clause = "MATCH (child:Class {name: $class_name})"
458
457
 
459
458
  parents_query = f"""
460
- {{match_clause}}
459
+ {match_clause}
461
460
  MATCH (child)-[:INHERITS]->(parent:Class)
462
- WHERE 1=1 {{repo_filter}}
461
+ WHERE 1=1 {repo_filter}
463
462
  OPTIONAL MATCH (parent_file:File)-[:CONTAINS]->(parent)
464
463
  RETURN DISTINCT
465
464
  parent.name as parent_class,
@@ -471,11 +470,11 @@ class CodeFinder:
471
470
  """
472
471
  parents_result = session.run(parents_query, class_name=class_name, path=path, repo_path=repo_path)
473
472
 
474
- repo_filter_child = "WHERE 1=1 AND grandchild.path STARTS WITH $repo_path" if repo_path else ""
473
+ repo_filter_child = "AND grandchild.path STARTS WITH $repo_path" if repo_path else ""
475
474
  children_query = f"""
476
- {{match_clause}}
475
+ {match_clause}
477
476
  MATCH (grandchild:Class)-[:INHERITS]->(child)
478
- WHERE 1=1 {{repo_filter_child}}
477
+ WHERE 1=1 {repo_filter_child}
479
478
  OPTIONAL MATCH (child_file:File)-[:CONTAINS]->(grandchild)
480
479
  RETURN DISTINCT
481
480
  grandchild.name as child_class,
@@ -513,10 +512,10 @@ class CodeFinder:
513
512
  def find_function_overrides(self, function_name: str, repo_path: Optional[str] = None) -> List[Dict]:
514
513
  """Find all implementations of a function across different classes"""
515
514
  with self.driver.session() as session:
516
- repo_filter = "WHERE 1=1 AND class.path STARTS WITH $repo_path" if repo_path else ""
515
+ repo_filter = "AND class.path STARTS WITH $repo_path" if repo_path else ""
517
516
  result = session.run(f"""
518
517
  MATCH (class:Class)-[:CONTAINS]->(func:Function {{name: $function_name}})
519
- WHERE 1=1 {{repo_filter}}
518
+ WHERE 1=1 {repo_filter}
520
519
  OPTIONAL MATCH (file:File)-[:CONTAINS]->(class)
521
520
  RETURN DISTINCT
522
521
  class.name as class_name,
@@ -821,69 +820,69 @@ class CodeFinder:
821
820
  "instances": variable_instances.data()
822
821
  }
823
822
 
824
- def analyze_code_relationships(self, query_type: str, target: str, context: Optional[str] = None) -> Dict[str, Any]:
823
+ def analyze_code_relationships(self, query_type: str, target: str, context: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
825
824
  """Main method to analyze different types of code relationships with fixed return types"""
826
825
  query_type = query_type.lower().strip()
827
826
 
828
827
  try:
829
828
  if query_type == "find_callers":
830
- results = self.who_calls_function(target, context)
829
+ results = self.who_calls_function(target, context, repo_path=repo_path)
831
830
  return {
832
831
  "query_type": "find_callers", "target": target, "context": context, "results": results,
833
832
  "summary": f"Found {len(results)} functions that call '{target}'"
834
833
  }
835
834
 
836
835
  elif query_type == "find_callees":
837
- results = self.what_does_function_call(target, context)
836
+ results = self.what_does_function_call(target, context, repo_path=repo_path)
838
837
  return {
839
838
  "query_type": "find_callees", "target": target, "context": context, "results": results,
840
839
  "summary": f"Function '{target}' calls {len(results)} other functions"
841
840
  }
842
841
 
843
842
  elif query_type == "find_importers":
844
- results = self.who_imports_module(target)
843
+ results = self.who_imports_module(target, repo_path=repo_path)
845
844
  return {
846
845
  "query_type": "find_importers", "target": target, "results": results,
847
846
  "summary": f"Found {len(results)} files that import '{target}'"
848
847
  }
849
848
 
850
849
  elif query_type == "find_functions_by_argument":
851
- results = self.find_functions_by_argument(target, context)
850
+ results = self.find_functions_by_argument(target, context, repo_path=repo_path)
852
851
  return {
853
852
  "query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
854
853
  "summary": f"Found {len(results)} functions that take '{target}' as an argument"
855
854
  }
856
855
 
857
856
  elif query_type == "find_functions_by_decorator":
858
- results = self.find_functions_by_decorator(target, context)
857
+ results = self.find_functions_by_decorator(target, context, repo_path=repo_path)
859
858
  return {
860
859
  "query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
861
860
  "summary": f"Found {len(results)} functions decorated with '{target}'"
862
861
  }
863
862
 
864
863
  elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
865
- results = self.who_modifies_variable(target)
864
+ results = self.who_modifies_variable(target, repo_path=repo_path)
866
865
  return {
867
866
  "query_type": "who_modifies", "target": target, "results": results,
868
867
  "summary": f"Found {len(results)} containers that hold variable '{target}'"
869
868
  }
870
869
 
871
870
  elif query_type in ["class_hierarchy", "inheritance", "extends"]:
872
- results = self.find_class_hierarchy(target, context)
871
+ results = self.find_class_hierarchy(target, context, repo_path=repo_path)
873
872
  return {
874
873
  "query_type": "class_hierarchy", "target": target, "results": results,
875
874
  "summary": f"Class '{target}' has {len(results['parent_classes'])} parents, {len(results['child_classes'])} children, and {len(results['methods'])} methods"
876
875
  }
877
876
 
878
877
  elif query_type in ["overrides", "implementations", "polymorphism"]:
879
- results = self.find_function_overrides(target)
878
+ results = self.find_function_overrides(target, repo_path=repo_path)
880
879
  return {
881
880
  "query_type": "overrides", "target": target, "results": results,
882
881
  "summary": f"Found {len(results)} implementations of function '{target}'"
883
882
  }
884
883
 
885
884
  elif query_type in ["dead_code", "unused", "unreachable"]:
886
- results = self.find_dead_code()
885
+ results = self.find_dead_code(repo_path=repo_path)
887
886
  return {
888
887
  "query_type": "dead_code", "results": results,
889
888
  "summary": f"Found {len(results['potentially_unused_functions'])} potentially unused functions"
@@ -891,21 +890,21 @@ class CodeFinder:
891
890
 
892
891
  elif query_type == "find_complexity":
893
892
  limit = int(context) if context and context.isdigit() else 10
894
- results = self.find_most_complex_functions(limit)
893
+ results = self.find_most_complex_functions(limit, repo_path=repo_path)
895
894
  return {
896
895
  "query_type": "find_complexity", "limit": limit, "results": results,
897
896
  "summary": f"Found the top {len(results)} most complex functions"
898
897
  }
899
898
 
900
899
  elif query_type == "find_all_callers":
901
- results = self.find_all_callers(target, context)
900
+ results = self.find_all_callers(target, context, repo_path=repo_path)
902
901
  return {
903
902
  "query_type": "find_all_callers", "target": target, "context": context, "results": results,
904
903
  "summary": f"Found {len(results)} direct and indirect callers of '{target}'"
905
904
  }
906
905
 
907
906
  elif query_type == "find_all_callees":
908
- results = self.find_all_callees(target, context)
907
+ results = self.find_all_callees(target, context, repo_path=repo_path)
909
908
  return {
910
909
  "query_type": "find_all_callees", "target": target, "context": context, "results": results,
911
910
  "summary": f"Found {len(results)} direct and indirect callees of '{target}'"
@@ -916,7 +915,7 @@ class CodeFinder:
916
915
  start_func, end_func = target.split('->', 1)
917
916
  # max_depth can be passed as context, default to 5 if not provided or invalid
918
917
  max_depth = int(context) if context and context.isdigit() else 5
919
- results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth)
918
+ results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth, repo_path=repo_path)
920
919
  return {
921
920
  "query_type": "call_chain", "target": target, "results": results,
922
921
  "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
@@ -928,14 +927,14 @@ class CodeFinder:
928
927
  }
929
928
 
930
929
  elif query_type in ["module_deps", "module_dependencies", "module_usage"]:
931
- results = self.find_module_dependencies(target)
930
+ results = self.find_module_dependencies(target, repo_path=repo_path)
932
931
  return {
933
932
  "query_type": "module_dependencies", "target": target, "results": results,
934
933
  "summary": f"Module '{target}' is imported by {len(results['imported_by_files'])} files"
935
934
  }
936
935
 
937
936
  elif query_type in ["variable_scope", "var_scope", "variable_usage_scope"]:
938
- results = self.find_variable_usage_scope(target)
937
+ results = self.find_variable_usage_scope(target, repo_path=repo_path)
939
938
  return {
940
939
  "query_type": "variable_scope", "target": target, "results": results,
941
940
  "summary": f"Variable '{target}' has {len(results['instances'])} instances across different scopes"