ctxgraph-code 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.
File without changes
@@ -0,0 +1,3 @@
1
+ from ctxgraph_code.cli import app
2
+
3
+ app()
File without changes
File without changes
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from ctxgraph_code.graph.models import Edge, Graph, Node
8
+
9
+
10
+ def analyze_imports(file_path: Path, root_path: Path) -> Graph:
11
+ graph = Graph()
12
+ rel_path = _relative_path(file_path, root_path)
13
+ file_node_id = f"file:{rel_path}"
14
+
15
+ graph.add_node(
16
+ Node(
17
+ id=file_node_id,
18
+ type="file",
19
+ name=file_path.name,
20
+ path=rel_path,
21
+ summary=None,
22
+ importance=0.5,
23
+ size_bytes=file_path.stat().st_size,
24
+ )
25
+ )
26
+
27
+ try:
28
+ tree = ast.parse(file_path.read_text(encoding="utf-8", errors="replace"))
29
+ except SyntaxError:
30
+ return graph
31
+
32
+ for node in ast.walk(tree):
33
+ if isinstance(node, ast.Import):
34
+ for alias in node.names:
35
+ target = _resolve_import_target(alias.name, root_path)
36
+ if target:
37
+ _add_import_edge(graph, rel_path, file_node_id, target, alias.name)
38
+
39
+ elif isinstance(node, ast.ImportFrom):
40
+ if node.module is None:
41
+ continue
42
+ if node.level and node.level > 0:
43
+ target = _resolve_relative_import(
44
+ node.module, node.level, rel_path, root_path
45
+ )
46
+ else:
47
+ target = _resolve_import_target(node.module, root_path)
48
+
49
+ if target:
50
+ for alias in node.names:
51
+ symbol_name = alias.asname or alias.name
52
+ edge_label = f"{node.module}.{alias.name}"
53
+ _add_import_edge(
54
+ graph, rel_path, file_node_id, target, edge_label
55
+ )
56
+
57
+ return graph
58
+
59
+
60
+ def _add_import_edge(
61
+ graph: Graph,
62
+ source_rel: str,
63
+ source_id: str,
64
+ target_rel: str,
65
+ label: str,
66
+ ):
67
+ target_id = f"file:{target_rel}"
68
+ if target_id not in graph.nodes:
69
+ graph.add_node(
70
+ Node(
71
+ id=target_id,
72
+ type="file",
73
+ name=Path(target_rel).name,
74
+ path=target_rel,
75
+ summary=None,
76
+ importance=0.3,
77
+ size_bytes=0,
78
+ )
79
+ )
80
+ graph.add_edge(
81
+ Edge(
82
+ source_id=source_id,
83
+ target_id=target_id,
84
+ relation="imports",
85
+ weight=1.0,
86
+ )
87
+ )
88
+
89
+
90
+ def _resolve_import_target(module_name: str, root_path: Path) -> Optional[str]:
91
+ package_path = module_name.replace(".", "/")
92
+ root_name = root_path.name
93
+ extensions = [".py", ".pyw"]
94
+
95
+ candidates = [package_path]
96
+ parts = module_name.split(".")
97
+ if len(parts) > 1 and parts[0] == root_name:
98
+ candidates.append("/".join(parts[1:]))
99
+
100
+ for base in candidates:
101
+ for ext in extensions:
102
+ candidate = f"{base}{ext}"
103
+ if _exists_in_project(candidate, root_path):
104
+ return candidate
105
+ init_candidate = f"{base}/__init__{ext}"
106
+ if _exists_in_project(init_candidate, root_path):
107
+ return init_candidate
108
+ return None
109
+
110
+
111
+ def _resolve_relative_import(
112
+ module_name: str, level: int, source_rel: str, root_path: Path
113
+ ) -> Optional[str]:
114
+ parts = Path(source_rel).parts
115
+ if level > len(parts):
116
+ return None
117
+ base = "/".join(parts[: len(parts) - level])
118
+ if module_name:
119
+ base = f"{base}/{module_name.replace('.', '/')}"
120
+ extensions = [".py", ".pyw"]
121
+ for ext in extensions:
122
+ candidate = f"{base}{ext}"
123
+ if _exists_in_project(candidate, root_path):
124
+ return candidate
125
+ init_candidate = f"{base}/__init__{ext}"
126
+ if _exists_in_project(init_candidate, root_path):
127
+ return init_candidate
128
+ return None
129
+
130
+
131
+ def _exists_in_project(candidate_path: str, root_path: Path) -> bool:
132
+ full = root_path / candidate_path
133
+ return full.exists()
134
+
135
+
136
+ def _relative_path(file_path: Path, root_path: Path) -> str:
137
+ try:
138
+ return str(file_path.relative_to(root_path)).replace("\\", "/")
139
+ except ValueError:
140
+ return file_path.name
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+
6
+ from ctxgraph_code.graph.models import Node
7
+
8
+
9
+ def enrich_node_summary(node: Node, file_path: Path) -> str:
10
+ if node.summary:
11
+ return node.summary
12
+
13
+ try:
14
+ source = file_path.read_text(encoding="utf-8", errors="replace")
15
+ tree = ast.parse(source)
16
+ except (SyntaxError, FileNotFoundError):
17
+ return node.summary or ""
18
+
19
+ if node.type == "file":
20
+ return _summarize_file(tree, file_path)
21
+ elif node.type == "class":
22
+ return _summarize_class(tree, node.name)
23
+ elif node.type == "function":
24
+ return _summarize_function(tree, node.name)
25
+
26
+ return node.summary or ""
27
+
28
+
29
+ def _summarize_file(tree: ast.AST, file_path: Path) -> str:
30
+ doc = ast.get_docstring(tree)
31
+ if doc:
32
+ return doc.split("\n\n")[0][:200]
33
+
34
+ classes = []
35
+ funcs = []
36
+ for node in ast.iter_child_nodes(tree):
37
+ if isinstance(node, ast.ClassDef):
38
+ classes.append(node.name)
39
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
40
+ funcs.append(node.name)
41
+
42
+ parts = []
43
+ if classes:
44
+ parts.append(f"Defines classes: {', '.join(classes)}")
45
+ if funcs:
46
+ parts.append(f"Defines functions: {', '.join(funcs)}")
47
+ return "; ".join(parts) if parts else ""
48
+
49
+
50
+ def _summarize_class(tree: ast.AST, class_name: str) -> str:
51
+ for node in ast.walk(tree):
52
+ if isinstance(node, ast.ClassDef) and node.name == class_name:
53
+ doc = ast.get_docstring(node)
54
+ if doc:
55
+ return doc.split("\n\n")[0][:200]
56
+ methods = [
57
+ c.name
58
+ for c in ast.iter_child_nodes(node)
59
+ if isinstance(c, (ast.FunctionDef, ast.AsyncFunctionDef))
60
+ ]
61
+ if methods:
62
+ return f"Methods: {', '.join(methods)}"
63
+ return ""
64
+
65
+
66
+ def _summarize_function(tree: ast.AST, func_name: str) -> str:
67
+ for node in ast.walk(tree):
68
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == func_name:
69
+ doc = ast.get_docstring(node)
70
+ if doc:
71
+ return doc.split("\n\n")[0][:200]
72
+ args = [a.arg for a in node.args.args]
73
+ if args:
74
+ return f"Args: {', '.join(args)}"
75
+ return ""
@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from ctxgraph_code.graph.models import Edge, Graph, Node
8
+
9
+
10
+ def analyze_symbols(file_path: Path, root_path: Path) -> Graph:
11
+ graph = Graph()
12
+ rel_path = _relative_path(file_path, root_path)
13
+ file_node_id = f"file:{rel_path}"
14
+
15
+ try:
16
+ source = file_path.read_text(encoding="utf-8", errors="replace")
17
+ tree = ast.parse(source)
18
+ except SyntaxError:
19
+ return graph
20
+
21
+ lines = source.split("\n")
22
+
23
+ for node in ast.iter_child_nodes(tree):
24
+ if isinstance(node, ast.ClassDef):
25
+ _process_class(node, graph, file_node_id, rel_path, lines)
26
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
27
+ _process_function(node, graph, file_node_id, rel_path, lines)
28
+
29
+ _process_calls(tree, graph, rel_path)
30
+
31
+ return graph
32
+
33
+
34
+ def _process_class(
35
+ node: ast.ClassDef,
36
+ graph: Graph,
37
+ file_node_id: str,
38
+ rel_path: str,
39
+ lines: list[str],
40
+ ):
41
+ class_id = f"class:{rel_path}::{node.name}"
42
+ summary = _extract_docstring(node)
43
+ bases = _get_base_names(node)
44
+
45
+ graph.add_node(
46
+ Node(
47
+ id=class_id,
48
+ type="class",
49
+ name=node.name,
50
+ path=rel_path,
51
+ parent_id=file_node_id,
52
+ summary=summary,
53
+ importance=0.6,
54
+ lineno=node.lineno,
55
+ )
56
+ )
57
+ graph.add_edge(
58
+ Edge(
59
+ source_id=file_node_id,
60
+ target_id=class_id,
61
+ relation="defines",
62
+ weight=1.0,
63
+ )
64
+ )
65
+
66
+ for base in bases:
67
+ graph.add_edge(
68
+ Edge(
69
+ source_id=class_id,
70
+ target_id=base,
71
+ relation="extends",
72
+ weight=0.8,
73
+ )
74
+ )
75
+
76
+ for child in ast.iter_child_nodes(node):
77
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
78
+ _process_method(child, graph, class_id, rel_path, lines)
79
+
80
+
81
+ def _process_function(
82
+ node: (ast.FunctionDef | ast.AsyncFunctionDef),
83
+ graph: Graph,
84
+ file_node_id: str,
85
+ rel_path: str,
86
+ lines: list[str],
87
+ ):
88
+ func_id = f"func:{rel_path}::{node.name}"
89
+ summary = _extract_docstring(node)
90
+
91
+ graph.add_node(
92
+ Node(
93
+ id=func_id,
94
+ type="function",
95
+ name=node.name,
96
+ path=rel_path,
97
+ parent_id=file_node_id,
98
+ summary=summary,
99
+ importance=0.5,
100
+ lineno=node.lineno,
101
+ )
102
+ )
103
+ graph.add_edge(
104
+ Edge(
105
+ source_id=file_node_id,
106
+ target_id=func_id,
107
+ relation="defines",
108
+ weight=1.0,
109
+ )
110
+ )
111
+
112
+
113
+ def _process_method(
114
+ node: (ast.FunctionDef | ast.AsyncFunctionDef),
115
+ graph: Graph,
116
+ class_id: str,
117
+ rel_path: str,
118
+ lines: list[str],
119
+ ):
120
+ method_id = f"func:{rel_path}::{node.name}"
121
+ summary = _extract_docstring(node)
122
+
123
+ graph.add_node(
124
+ Node(
125
+ id=method_id,
126
+ type="function",
127
+ name=node.name,
128
+ path=rel_path,
129
+ parent_id=class_id,
130
+ summary=summary,
131
+ importance=0.5,
132
+ lineno=node.lineno,
133
+ )
134
+ )
135
+ graph.add_edge(
136
+ Edge(
137
+ source_id=class_id,
138
+ target_id=method_id,
139
+ relation="defines",
140
+ weight=1.0,
141
+ )
142
+ )
143
+
144
+
145
+ def _process_calls(tree: ast.AST, graph: Graph, rel_path: str):
146
+ for node in ast.walk(tree):
147
+ if isinstance(node, ast.Call):
148
+ func_name = _get_call_name(node.func)
149
+ if func_name:
150
+ caller_id = _find_enclosing_symbol(tree, node, rel_path)
151
+ if caller_id:
152
+ callee_node = _find_target_node(func_name, graph)
153
+ if callee_node:
154
+ graph.add_edge(
155
+ Edge(
156
+ source_id=caller_id,
157
+ target_id=callee_node.id,
158
+ relation="calls",
159
+ weight=0.7,
160
+ )
161
+ )
162
+
163
+
164
+ def _get_call_name(node: ast.AST) -> Optional[str]:
165
+ if isinstance(node, ast.Name):
166
+ return node.id
167
+ elif isinstance(node, ast.Attribute):
168
+ return node.attr
169
+ return None
170
+
171
+
172
+ def _find_enclosing_symbol(
173
+ tree: ast.AST, target_node: ast.AST, rel_path: str
174
+ ) -> Optional[str]:
175
+ for node in ast.walk(tree):
176
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
177
+ if _contains_node(node, target_node):
178
+ return f"func:{rel_path}::{node.name}"
179
+ elif isinstance(node, ast.ClassDef):
180
+ for child in ast.iter_child_nodes(node):
181
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
182
+ if _contains_node(child, target_node):
183
+ return f"func:{rel_path}::{child.name}"
184
+ return None
185
+
186
+
187
+ def _contains_node(container: ast.AST, target: ast.AST) -> bool:
188
+ for node in ast.walk(container):
189
+ if node is target:
190
+ return True
191
+ return False
192
+
193
+
194
+ def _find_target_node(name: str, graph: Graph) -> Optional[Node]:
195
+ for node in graph.nodes.values():
196
+ if node.type in ("function", "class") and node.name == name:
197
+ return node
198
+ return None
199
+
200
+
201
+ def _extract_docstring(node: ast.AST) -> Optional[str]:
202
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
203
+ doc = ast.get_docstring(node)
204
+ if doc:
205
+ return doc.split("\n\n")[0][:200]
206
+ return None
207
+
208
+
209
+ def _get_base_names(node: ast.ClassDef) -> list[str]:
210
+ bases = []
211
+ for base in node.bases:
212
+ if isinstance(base, ast.Name):
213
+ bases.append(f"class:{base.id}")
214
+ return bases
215
+
216
+
217
+ def _relative_path(file_path: Path, root_path: Path) -> str:
218
+ try:
219
+ return str(file_path.relative_to(root_path)).replace("\\", "/")
220
+ except ValueError:
221
+ return file_path.name