roma-debug 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.
@@ -0,0 +1,422 @@
1
+ """Smart context extraction for ROMA Debug.
2
+
3
+ Uses AST parsing to extract full function/class definitions around errors,
4
+ with graceful fallback strategies for non-parseable or missing files.
5
+
6
+ V2: Now supports multi-language parsing via parser registry.
7
+ """
8
+
9
+ import ast
10
+ import os
11
+ import re
12
+ from dataclasses import dataclass
13
+ from typing import Optional, List, Tuple
14
+
15
+ from roma_debug.core.models import Language, FileContext as FileContextV2, Import, Symbol
16
+ from roma_debug.parsers.registry import get_parser, detect_language
17
+
18
+
19
+ @dataclass
20
+ class FileContext:
21
+ """Extracted context from a source file.
22
+
23
+ This is the V1 interface maintained for backward compatibility.
24
+ Internally delegates to FileContextV2 when possible.
25
+ """
26
+ filepath: str
27
+ line_number: int
28
+ context_type: str # 'ast', 'lines', 'missing', 'treesitter'
29
+ content: str
30
+ function_name: Optional[str] = None
31
+ class_name: Optional[str] = None
32
+ # V2 additions (optional for backward compat)
33
+ language: Language = Language.UNKNOWN
34
+ imports: List[Import] = None
35
+ symbol: Optional[Symbol] = None
36
+
37
+ def __post_init__(self):
38
+ if self.imports is None:
39
+ self.imports = []
40
+
41
+ def to_v2(self) -> FileContextV2:
42
+ """Convert to V2 FileContext."""
43
+ return FileContextV2(
44
+ filepath=self.filepath,
45
+ line_number=self.line_number,
46
+ context_type=self.context_type,
47
+ content=self.content,
48
+ function_name=self.function_name,
49
+ class_name=self.class_name,
50
+ language=self.language,
51
+ imports=self.imports or [],
52
+ symbol=self.symbol,
53
+ )
54
+
55
+
56
+ def _resolve_file_path(file_path: str) -> Optional[str]:
57
+ """Resolve file path, checking both absolute and cwd-relative locations.
58
+
59
+ Args:
60
+ file_path: Path from traceback (may be absolute or relative)
61
+
62
+ Returns:
63
+ Resolved path if file exists, None otherwise
64
+ """
65
+ # 1. Try the path as-is (absolute or relative to cwd)
66
+ if os.path.isfile(file_path):
67
+ return file_path
68
+
69
+ # 2. Try relative to current working directory
70
+ cwd = os.getcwd()
71
+ cwd_relative = os.path.join(cwd, file_path)
72
+ if os.path.isfile(cwd_relative):
73
+ return cwd_relative
74
+
75
+ # 3. Try just the filename in cwd (for logs from different machines)
76
+ filename = os.path.basename(file_path)
77
+ cwd_filename = os.path.join(cwd, filename)
78
+ if os.path.isfile(cwd_filename):
79
+ return cwd_filename
80
+
81
+ # 4. Try extracting relative path after common prefixes
82
+ # e.g., "/app/src/main.py" -> "src/main.py"
83
+ common_prefixes = ["/app/", "/home/", "/usr/", "/var/"]
84
+ for prefix in common_prefixes:
85
+ if file_path.startswith(prefix):
86
+ relative = file_path[len(prefix):]
87
+ cwd_relative = os.path.join(cwd, relative)
88
+ if os.path.isfile(cwd_relative):
89
+ return cwd_relative
90
+
91
+ # 5. Search for the file in common project subdirectories
92
+ search_dirs = [".", "src", "lib", "app", "tests", "test"]
93
+ for search_dir in search_dirs:
94
+ search_path = os.path.join(cwd, search_dir, filename)
95
+ if os.path.isfile(search_path):
96
+ return search_path
97
+
98
+ return None
99
+
100
+
101
+ def get_file_context(error_log: str) -> Tuple[str, List[FileContext]]:
102
+ """Extract file context from a Python traceback.
103
+
104
+ Uses AST parsing to extract full function/class definitions.
105
+ Falls back to line-based extraction if AST fails.
106
+ Searches for files relative to os.getcwd() for project awareness.
107
+
108
+ Args:
109
+ error_log: The error log or traceback string
110
+
111
+ Returns:
112
+ Tuple of (formatted context string, list of FileContext objects)
113
+ """
114
+ # Pattern to match Python traceback file references
115
+ pattern = re.compile(r'File ["\'](.+?)["\'], line (\d+)')
116
+ matches = pattern.findall(error_log)
117
+
118
+ if not matches:
119
+ return "", []
120
+
121
+ contexts: List[FileContext] = []
122
+ context_parts: List[str] = []
123
+
124
+ for file_path, line_num_str in matches:
125
+ line_num = int(line_num_str)
126
+ file_context = _extract_context(file_path, line_num)
127
+ contexts.append(file_context)
128
+
129
+ # Build formatted output
130
+ filename = os.path.basename(file_path)
131
+ if file_context.context_type == "missing":
132
+ context_parts.append(file_context.content)
133
+ else:
134
+ header = f"Context from {filename}"
135
+ if file_context.function_name:
136
+ header += f" (function: {file_context.function_name})"
137
+ if file_context.class_name:
138
+ header += f" (class: {file_context.class_name})"
139
+ context_parts.append(f"{header}:\n{file_context.content}")
140
+
141
+ return "\n\n".join(context_parts), contexts
142
+
143
+
144
+ def _extract_context(file_path: str, error_line: int) -> FileContext:
145
+ """Extract context from a file using parser or fallback.
146
+
147
+ Strategy:
148
+ 1. Resolve file path (check cwd-relative paths)
149
+ 2. Detect language from file extension
150
+ 3. Try language-specific parser to get full function/class
151
+ 4. Fallback to +/- 50 lines if parser fails
152
+ 5. Return friendly message if file missing
153
+
154
+ Args:
155
+ file_path: Path to the source file (from traceback)
156
+ error_line: Line number where error occurred
157
+
158
+ Returns:
159
+ FileContext with extracted content
160
+ """
161
+ # Resolve the file path (try cwd-relative if absolute doesn't exist)
162
+ resolved_path = _resolve_file_path(file_path)
163
+
164
+ if resolved_path is None:
165
+ return FileContext(
166
+ filepath=file_path,
167
+ line_number=error_line,
168
+ context_type="missing",
169
+ content=f"[System] Local file not found at {file_path}. Debugging based on logs only.",
170
+ language=detect_language(file_path),
171
+ )
172
+
173
+ # Read file content
174
+ try:
175
+ with open(resolved_path, 'r', encoding='utf-8', errors='replace') as f:
176
+ source = f.read()
177
+ lines = source.splitlines()
178
+ except (IOError, OSError) as e:
179
+ return FileContext(
180
+ filepath=file_path,
181
+ line_number=error_line,
182
+ context_type="missing",
183
+ content=f"[System] Cannot read file {file_path}: {e}. Debugging based on logs only.",
184
+ language=detect_language(file_path),
185
+ )
186
+
187
+ # Detect language
188
+ language = detect_language(resolved_path)
189
+
190
+ # Try parser-based extraction
191
+ parser_context = _try_parser_extraction(source, lines, error_line, resolved_path, language)
192
+ if parser_context:
193
+ return parser_context
194
+
195
+ # Fallback: +/- 50 lines
196
+ return _line_based_extraction(resolved_path, lines, error_line, language, context_lines=50)
197
+
198
+
199
+ def _try_parser_extraction(
200
+ source: str,
201
+ lines: List[str],
202
+ error_line: int,
203
+ file_path: str,
204
+ language: Language,
205
+ ) -> Optional[FileContext]:
206
+ """Try to extract full function/class definition using a parser.
207
+
208
+ Args:
209
+ source: Full source code
210
+ lines: Source split into lines
211
+ error_line: Target line number
212
+ file_path: Path to file
213
+ language: Detected language
214
+
215
+ Returns:
216
+ FileContext if successful, None if parsing fails
217
+ """
218
+ # Get appropriate parser
219
+ parser = get_parser(language, create_new=True)
220
+
221
+ if parser is None:
222
+ # No parser available for this language, try Python AST as fallback
223
+ if language == Language.PYTHON or file_path.endswith('.py'):
224
+ return _try_ast_extraction(source, lines, error_line, file_path)
225
+ return None
226
+
227
+ # Try parsing
228
+ if not parser.parse(source, file_path):
229
+ # Parser failed, try Python AST as last resort for .py files
230
+ if language == Language.PYTHON:
231
+ return _try_ast_extraction(source, lines, error_line, file_path)
232
+ return None
233
+
234
+ # Find enclosing symbol
235
+ symbol = parser.find_enclosing_symbol(error_line)
236
+
237
+ if symbol is None:
238
+ return None
239
+
240
+ # Extract the full function/class with some buffer
241
+ start_line = max(1, symbol.start_line - 2) # 2 lines before for decorators
242
+ end_line = min(len(lines), symbol.end_line + 2)
243
+
244
+ # Build snippet with line numbers
245
+ snippet = parser.format_snippet(start_line, end_line, highlight_line=error_line)
246
+
247
+ # Determine names
248
+ function_name = None
249
+ class_name = None
250
+ if symbol.kind in ("function", "method", "async_function"):
251
+ function_name = symbol.name
252
+ if symbol.parent and symbol.parent.kind == "class":
253
+ class_name = symbol.parent.name
254
+ elif symbol.kind == "class":
255
+ class_name = symbol.name
256
+
257
+ # Extract imports
258
+ imports = parser.extract_imports()
259
+
260
+ context_type = "treesitter" if "treesitter" in type(parser).__module__ else "ast"
261
+
262
+ return FileContext(
263
+ filepath=file_path,
264
+ line_number=error_line,
265
+ context_type=context_type,
266
+ content=snippet,
267
+ function_name=function_name,
268
+ class_name=class_name,
269
+ language=language,
270
+ imports=imports,
271
+ symbol=symbol,
272
+ )
273
+
274
+
275
+ def _try_ast_extraction(
276
+ source: str,
277
+ lines: List[str],
278
+ error_line: int,
279
+ file_path: str
280
+ ) -> Optional[FileContext]:
281
+ """Try to extract full function/class definition using AST.
282
+
283
+ Legacy method for backward compatibility. Kept as fallback.
284
+
285
+ Args:
286
+ source: Full source code
287
+ lines: Source split into lines
288
+ error_line: Target line number
289
+ file_path: Path to file
290
+
291
+ Returns:
292
+ FileContext if successful, None if AST parsing fails
293
+ """
294
+ try:
295
+ tree = ast.parse(source)
296
+ except SyntaxError:
297
+ return None
298
+
299
+ # Find the innermost function or class containing the error line
300
+ best_match = None
301
+ best_match_size = float('inf')
302
+
303
+ for node in ast.walk(tree):
304
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
305
+ # Check if error line is within this node
306
+ if hasattr(node, 'lineno') and hasattr(node, 'end_lineno'):
307
+ if node.lineno <= error_line <= (node.end_lineno or node.lineno):
308
+ # Prefer smaller (more specific) matches
309
+ size = (node.end_lineno or node.lineno) - node.lineno
310
+ if size < best_match_size:
311
+ best_match = node
312
+ best_match_size = size
313
+
314
+ if not best_match:
315
+ return None
316
+
317
+ # Extract the full function/class with some buffer
318
+ start_line = max(1, best_match.lineno - 2) # 2 lines before for decorators
319
+ end_line = min(len(lines), (best_match.end_lineno or best_match.lineno) + 2)
320
+
321
+ # Build snippet with line numbers
322
+ snippet_lines = []
323
+ for i in range(start_line - 1, end_line):
324
+ line_num = i + 1
325
+ line_content = lines[i]
326
+ marker = " >> " if line_num == error_line else " "
327
+ snippet_lines.append(f"{marker}{line_num:4d} | {line_content}")
328
+
329
+ # Determine names
330
+ function_name = None
331
+ class_name = None
332
+ if isinstance(best_match, (ast.FunctionDef, ast.AsyncFunctionDef)):
333
+ function_name = best_match.name
334
+ elif isinstance(best_match, ast.ClassDef):
335
+ class_name = best_match.name
336
+
337
+ return FileContext(
338
+ filepath=file_path,
339
+ line_number=error_line,
340
+ context_type="ast",
341
+ content="\n".join(snippet_lines),
342
+ function_name=function_name,
343
+ class_name=class_name,
344
+ language=Language.PYTHON,
345
+ )
346
+
347
+
348
+ def _line_based_extraction(
349
+ file_path: str,
350
+ lines: List[str],
351
+ error_line: int,
352
+ language: Language = Language.UNKNOWN,
353
+ context_lines: int = 50
354
+ ) -> FileContext:
355
+ """Fallback: extract +/- N lines around error.
356
+
357
+ Args:
358
+ file_path: Path to file
359
+ lines: Source lines
360
+ error_line: Target line number
361
+ language: Detected language
362
+ context_lines: Lines before/after to include
363
+
364
+ Returns:
365
+ FileContext with line-based extraction
366
+ """
367
+ total_lines = len(lines)
368
+ start_line = max(1, error_line - context_lines)
369
+ end_line = min(total_lines, error_line + context_lines)
370
+
371
+ snippet_lines = []
372
+ for i in range(start_line - 1, end_line):
373
+ line_num = i + 1
374
+ line_content = lines[i]
375
+ marker = " >> " if line_num == error_line else " "
376
+ snippet_lines.append(f"{marker}{line_num:4d} | {line_content}")
377
+
378
+ return FileContext(
379
+ filepath=file_path,
380
+ line_number=error_line,
381
+ context_type="lines",
382
+ content="\n".join(snippet_lines),
383
+ language=language,
384
+ )
385
+
386
+
387
+ def get_primary_file(contexts: List[FileContext]) -> Optional[FileContext]:
388
+ """Get the primary file from contexts (last non-missing entry).
389
+
390
+ Usually the last file in the traceback is the most relevant.
391
+
392
+ Args:
393
+ contexts: List of FileContext objects
394
+
395
+ Returns:
396
+ The primary FileContext or None
397
+ """
398
+ for ctx in reversed(contexts):
399
+ if ctx.context_type != "missing":
400
+ return ctx
401
+ return None
402
+
403
+
404
+ def extract_context_v2(
405
+ error_log: str,
406
+ project_root: Optional[str] = None,
407
+ ) -> Tuple[str, List[FileContextV2]]:
408
+ """V2 context extraction with full language support.
409
+
410
+ Args:
411
+ error_log: The error log or traceback string
412
+ project_root: Optional project root for import resolution
413
+
414
+ Returns:
415
+ Tuple of (formatted context string, list of FileContextV2 objects)
416
+ """
417
+ # Use the same extraction logic but return V2 objects
418
+ context_str, contexts = get_file_context(error_log)
419
+
420
+ v2_contexts = [ctx.to_v2() for ctx in contexts]
421
+
422
+ return context_str, v2_contexts
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: roma-debug
3
+ Version: 0.1.0
4
+ Summary: Standalone CLI debugging tool powered by Gemini
5
+ Author: ROMA Team
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.10
14
+ License-File: LICENSE
15
+ Requires-Dist: click>=8.0.0
16
+ Requires-Dist: pydantic>=2.5.0
17
+ Requires-Dist: google-genai>=1.0.0
18
+ Requires-Dist: rich>=13.0.0
19
+ Requires-Dist: fastapi>=0.109.0
20
+ Requires-Dist: uvicorn>=0.27.0
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Requires-Dist: tree-sitter>=0.23.0
23
+ Requires-Dist: tree-sitter-python>=0.23.0
24
+ Requires-Dist: tree-sitter-javascript>=0.23.0
25
+ Requires-Dist: tree-sitter-typescript>=0.23.0
26
+ Requires-Dist: tree-sitter-go>=0.23.0
27
+ Requires-Dist: tree-sitter-rust>=0.23.0
28
+ Requires-Dist: tree-sitter-java>=0.23.0
29
+ Dynamic: author
30
+ Dynamic: classifier
31
+ Dynamic: license-file
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
@@ -0,0 +1,36 @@
1
+ roma_debug/__init__.py,sha256=nduW0_uiplK4TqgBEihMu9gbcZiYmQH95rwhQlt1O5U,73
2
+ roma_debug/config.py,sha256=ynIlTUQb95x_WtEsM7R4G0kgioBm2UsAhhZzSAAZkI0,2052
3
+ roma_debug/main.py,sha256=h_JoaXD1Tt236UtMGY7jftgqZoG7WU8W1EnjQf1ZHuY,25697
4
+ roma_debug/prompts.py,sha256=gICok5kamF6QOeo14eNKdvp8TeuDBAjB5KQapMlLrgU,7141
5
+ roma_debug/server.py,sha256=8v6qIy25T9d9Y1dB_zBeIMEsGe1vcImEXQ3LV38Eo1U,7446
6
+ roma_debug/core/__init__.py,sha256=m7Nus3vI9QeZhwZWHSs8-p4F338JRjE8XSz-OOvt_Co,114
7
+ roma_debug/core/engine.py,sha256=L0fFrNKZZ5zET_m5kbIcQxqGW3KikGNiO-giCwQXAeU,13282
8
+ roma_debug/core/models.py,sha256=8wqlsx_N8lYjZ4rLq7S3-AcbLiaTy7u7gv6QLW77tkI,10545
9
+ roma_debug/parsers/__init__.py,sha256=B1Xk6zYvghqRwcIEcZy3eX3JtOc0IbIQqpTVu3Ivdc4,491
10
+ roma_debug/parsers/base.py,sha256=eXS888T8K-PVXzGPIFp-asTUOwCEjSRr38DohiwPV_U,5465
11
+ roma_debug/parsers/python_ast_parser.py,sha256=-92VDCkIPvz2eqMXDsBdfFrtCKcVIrrqpgh8AdC9BaM,9079
12
+ roma_debug/parsers/registry.py,sha256=JEVLASy6Bq__Yb3xokH8uOlo9OEXB47aVlsjtuBIBZg,5578
13
+ roma_debug/parsers/traceback_patterns.py,sha256=0Vf3UeW0-iJVLS4_IJchb556tEzhDIIGg9VRgTEVK5A,10880
14
+ roma_debug/parsers/treesitter_parser.py,sha256=6U1UU1tLcbdBUec_Q6niwUUF15J5MenZCfGwiyRhaTU,21297
15
+ roma_debug/tracing/__init__.py,sha256=Za80mYx08DK4cICq_kJiOp6QHL_zVmZMcSJi1rdRgbw,952
16
+ roma_debug/tracing/call_chain.py,sha256=coFOexGZOiiAmry6gc_o-AK9SoAdhoJuNwUBEBCNwKM,8853
17
+ roma_debug/tracing/context_builder.py,sha256=EevjP5NjPcH8_TA8r-hlFQFMpQBM5K6IqHP2TN-_5d8,25392
18
+ roma_debug/tracing/dependency_graph.py,sha256=kC9bP9g_HR-5dPIjTBQ6GGvlkrhdX_sLF1b-cdjtDkA,9399
19
+ roma_debug/tracing/error_analyzer.py,sha256=nTIFXDGceRX1K7QkBSFTG7tse01XRATZDJm32yoRE7c,12595
20
+ roma_debug/tracing/import_resolver.py,sha256=HBiL17ojN1MvjI_-dA5HWJ7E0Ie-2cIhRcVEuJM4V3c,10357
21
+ roma_debug/tracing/project_scanner.py,sha256=q1jmO5Wm5Dpaab58kfQ_EIX4NVRS87FETGITXHueIbw,18067
22
+ roma_debug/utils/__init__.py,sha256=XxTYoo8gCFrcEx6_LvKDTxKhD0CnSmeiub8bPW0mZOg,125
23
+ roma_debug/utils/context.py,sha256=VtnCwGV_d4n05KZ6McH_0HTvbROMeSWvfKV5ZcI4WRU,13545
24
+ roma_debug-0.1.0.dist-info/licenses/LICENSE,sha256=83lDVbRoeUNHeZi_xRYBuCVePZtbGCRcKt_leGlRFs8,11348
25
+ tests/__init__.py,sha256=a9m7ixR4qxbxJZjdpc3GJFp4rErm1BZN6cGOPlAfjm0,28
26
+ tests/test_context.py,sha256=WVrAwuqvmkhoJMWrF0I18-VV5bmXDwbegsck2XOVasw,7132
27
+ tests/test_engine.py,sha256=bhu8Lc6IwkCbCZy3nzcyHUq7ecSbN9gKbI-r2qXPSqA,9977
28
+ tests/test_parsers.py,sha256=7tsOlcBuv_8RJcUvA0TqPb92FehtXPLaC8nNBbuZp1k,16056
29
+ tests/test_project_scanner.py,sha256=cN5ZSoxqyT9ZUfxC4LHeYtNB5j68j8bUVzPyySrYKfA,9268
30
+ tests/test_traceback_patterns.py,sha256=dL8gzmCEyBx5WchvceB98YwMIf6Z5_FmWzBxrC7nSfU,6779
31
+ tests/test_tracing.py,sha256=N53hX3Mh1C8Q9HDzAusW6-8BRSDwEUkT7ipswiT2rvM,9522
32
+ roma_debug-0.1.0.dist-info/METADATA,sha256=O1RyReLjg7Xr7-xEnbHqFZEljoKVGsQsNgz8wdZBJeA,1146
33
+ roma_debug-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
34
+ roma_debug-0.1.0.dist-info/entry_points.txt,sha256=z7vb9CzOFmKWg_JwpzOIxpstebIzqeiOmaHiEqM6HbI,45
35
+ roma_debug-0.1.0.dist-info/top_level.txt,sha256=aL4bhVn3nECTAeTcxWQZkogEKOnI57eWiLNoQWARIRk,17
36
+ roma_debug-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ roma = roma_debug.main:cli