pydefine 1.0.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.
pydefine/utils.py ADDED
@@ -0,0 +1,442 @@
1
+ """
2
+ pydefine.utils
3
+ ~~~~~~~~~~~~~~
4
+
5
+ Utility functions for parsing tracebacks, extracting error information,
6
+ formatting output, and tokenizing for rich display.
7
+
8
+ These are helper functions used by core.py and other modules.
9
+ """
10
+
11
+ import re
12
+ import sys
13
+ from typing import Dict, Any, List, Optional, Tuple
14
+
15
+
16
+ def extract_error_info(traceback_text: str) -> Dict[str, Any]:
17
+ """
18
+ Extract structured information from a raw traceback string.
19
+
20
+ Parses the traceback to extract:
21
+ - Error type (exception class name)
22
+ - Original error message
23
+ - Line number where error occurred
24
+ - File name where error occurred
25
+ - Full traceback frames
26
+
27
+ Args:
28
+ traceback_text: Raw traceback string (multi-line)
29
+
30
+ Returns:
31
+ Dictionary with extracted information
32
+
33
+ Example:
34
+ >>> tb = '''Traceback (most recent call last):
35
+ ... File "test.py", line 10, in <module>
36
+ ... x = 1/0
37
+ ... ZeroDivisionError: division by zero'''
38
+ >>> info = extract_error_info(tb)
39
+ >>> info['error_type']
40
+ 'ZeroDivisionError'
41
+ >>> info['line_number']
42
+ 10
43
+ """
44
+ result = {
45
+ "error_type": "UnknownError",
46
+ "original_message": "",
47
+ "line_number": None,
48
+ "file_name": None,
49
+ "frames": []
50
+ }
51
+
52
+ if not traceback_text:
53
+ return result
54
+
55
+ lines = traceback_text.strip().split('\n')
56
+
57
+ # Last line usually contains the error type and message
58
+ # Format: "ExceptionName: error message" or just "ExceptionName"
59
+ last_line = lines[-1].strip() if lines else ""
60
+
61
+ if ':' in last_line:
62
+ # Split on first colon only
63
+ error_parts = last_line.split(':', 1)
64
+ result["error_type"] = error_parts[0].strip()
65
+ result["original_message"] = error_parts[1].strip() if len(error_parts) > 1 else ""
66
+ else:
67
+ # Just error type, no message
68
+ result["error_type"] = last_line.strip()
69
+ result["original_message"] = ""
70
+
71
+ # Extract file and line information
72
+ # Pattern: File "filename.py", line 123, in function_name
73
+ file_line_pattern = r'File "([^"]+)", line (\d+)'
74
+
75
+ for line in lines:
76
+ match = re.search(file_line_pattern, line)
77
+ if match:
78
+ file_name = match.group(1)
79
+ line_number = int(match.group(2))
80
+
81
+ # The last occurrence is where the error happened
82
+ result["file_name"] = file_name
83
+ result["line_number"] = line_number
84
+
85
+ result["frames"].append({
86
+ "file": file_name,
87
+ "line": line_number,
88
+ "text": line.strip()
89
+ })
90
+
91
+ # Handle SyntaxError special case (has additional info)
92
+ if result["error_type"] == "SyntaxError":
93
+ # SyntaxError often has the file/line in a different format
94
+ for i, line in enumerate(lines):
95
+ if line.strip().startswith('File "'):
96
+ match = re.search(r'File "([^"]+)", line (\d+)', line)
97
+ if match:
98
+ result["file_name"] = match.group(1)
99
+ result["line_number"] = int(match.group(2))
100
+
101
+ return result
102
+
103
+
104
+ def format_output(decoded_info: Dict[str, Any]) -> str:
105
+ """
106
+ Format decoded error information into a human-readable string.
107
+
108
+ Creates a nicely formatted output with emoji, explanations, and fixes.
109
+
110
+ Args:
111
+ decoded_info: Dictionary from decode_traceback() or decode_exception()
112
+
113
+ Returns:
114
+ Formatted string ready to print
115
+
116
+ Example:
117
+ >>> info = decode_exception(some_error)
118
+ >>> print(format_output(info))
119
+ """
120
+ lines = []
121
+
122
+ # Header
123
+ lines.append("=" * 70)
124
+ emoji = decoded_info.get("emoji", "❓")
125
+ error_type = decoded_info.get("error_type", "UnknownError")
126
+ original_msg = decoded_info.get("original_message", "")
127
+
128
+ if original_msg:
129
+ lines.append(f"{emoji} {error_type}: {original_msg}")
130
+ else:
131
+ lines.append(f"{emoji} {error_type}")
132
+
133
+ lines.append("=" * 70)
134
+ lines.append("")
135
+
136
+ # Simple explanation
137
+ explanation = decoded_info.get("simple_explanation", "No explanation available.")
138
+ lines.append("📖 What happened:")
139
+ lines.append(f" {explanation}")
140
+ lines.append("")
141
+
142
+ # Fix suggestion
143
+ fix_suggestion = decoded_info.get("fix_suggestion", "No suggestion available.")
144
+ lines.append("💡 How to fix:")
145
+ lines.append(f" {fix_suggestion}")
146
+ lines.append("")
147
+
148
+ # Location information
149
+ line_num = decoded_info.get("line_number")
150
+ file_name = decoded_info.get("file_name")
151
+
152
+ if line_num or file_name:
153
+ lines.append("📍 Where:")
154
+ if file_name:
155
+ lines.append(f" File: {file_name}")
156
+ if line_num:
157
+ lines.append(f" Line: {line_num}")
158
+ lines.append("")
159
+
160
+ # Tags
161
+ tags = decoded_info.get("tags", [])
162
+ if tags:
163
+ lines.append(f"🏷️ Tags: {', '.join(tags)}")
164
+ lines.append("")
165
+
166
+ # Branding
167
+ branding = decoded_info.get("branding", "Powered by pyDefine ● Created by Yahya")
168
+ lines.append("─" * 70)
169
+ lines.append(f"✨ {branding} ✨")
170
+ lines.append("=" * 70)
171
+
172
+ return "\n".join(lines)
173
+
174
+
175
+ def tokenize_output(decoded_info: Dict[str, Any]) -> List[Dict[str, str]]:
176
+ """
177
+ Convert decoded error info into tokens for rich display.
178
+
179
+ Tokens can include special markers like [IMG:url] and [AUDIO:url]
180
+ for future rich rendering in web interfaces.
181
+
182
+ Args:
183
+ decoded_info: Dictionary from decode_traceback() or decode_exception()
184
+
185
+ Returns:
186
+ List of token dictionaries with 'type' and 'content' keys
187
+
188
+ Example:
189
+ >>> info = decode_exception(some_error)
190
+ >>> tokens = tokenize_output(info)
191
+ >>> for token in tokens:
192
+ ... print(f"{token['type']}: {token['content']}")
193
+ """
194
+ tokens = []
195
+
196
+ # Emoji token
197
+ emoji = decoded_info.get("emoji", "❓")
198
+ tokens.append({"type": "emoji", "content": emoji})
199
+
200
+ # Error type token
201
+ error_type = decoded_info.get("error_type", "UnknownError")
202
+ tokens.append({"type": "error_type", "content": error_type})
203
+
204
+ # Original message token
205
+ original_msg = decoded_info.get("original_message", "")
206
+ if original_msg:
207
+ tokens.append({"type": "original_message", "content": original_msg})
208
+
209
+ # Explanation token
210
+ explanation = decoded_info.get("simple_explanation", "")
211
+ if explanation:
212
+ tokens.append({"type": "explanation", "content": explanation})
213
+
214
+ # Check for special rich content markers
215
+ # [IMG:url] for images
216
+ img_matches = re.findall(r'\[IMG:([^\]]+)\]', explanation)
217
+ for img_url in img_matches:
218
+ tokens.append({"type": "image", "content": img_url})
219
+
220
+ # [AUDIO:url] for audio
221
+ audio_matches = re.findall(r'\[AUDIO:([^\]]+)\]', explanation)
222
+ for audio_url in audio_matches:
223
+ tokens.append({"type": "audio", "content": audio_url})
224
+
225
+ # Fix suggestion token
226
+ fix_suggestion = decoded_info.get("fix_suggestion", "")
227
+ if fix_suggestion:
228
+ tokens.append({"type": "fix_suggestion", "content": fix_suggestion})
229
+
230
+ # Location tokens
231
+ line_num = decoded_info.get("line_number")
232
+ if line_num:
233
+ tokens.append({"type": "line_number", "content": str(line_num)})
234
+
235
+ file_name = decoded_info.get("file_name")
236
+ if file_name:
237
+ tokens.append({"type": "file_name", "content": file_name})
238
+
239
+ # Tags token
240
+ tags = decoded_info.get("tags", [])
241
+ if tags:
242
+ tokens.append({"type": "tags", "content": tags})
243
+
244
+ # Branding token
245
+ branding = decoded_info.get("branding", "Powered by pyDefine ● Created by Yahya")
246
+ tokens.append({"type": "branding", "content": branding})
247
+
248
+ return tokens
249
+
250
+
251
+ def strip_ansi_codes(text: str) -> str:
252
+ """
253
+ Remove ANSI color codes from text.
254
+
255
+ Useful when parsing traceback from colored terminal output.
256
+
257
+ Args:
258
+ text: String potentially containing ANSI codes
259
+
260
+ Returns:
261
+ Clean text without ANSI codes
262
+ """
263
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
264
+ return ansi_escape.sub('', text)
265
+
266
+
267
+ def truncate_message(message: str, max_length: int = 200) -> str:
268
+ """
269
+ Truncate long error messages for display.
270
+
271
+ Args:
272
+ message: Error message to truncate
273
+ max_length: Maximum length (default: 200)
274
+
275
+ Returns:
276
+ Truncated message with ellipsis if needed
277
+ """
278
+ if len(message) <= max_length:
279
+ return message
280
+ return message[:max_length - 3] + "..."
281
+
282
+
283
+ def extract_exception_from_string(text: str) -> Optional[str]:
284
+ """
285
+ Try to find exception name in arbitrary text.
286
+
287
+ Useful for parsing log files or mixed content.
288
+
289
+ Args:
290
+ text: Text that might contain an exception
291
+
292
+ Returns:
293
+ Exception name if found, None otherwise
294
+ """
295
+ # Common exception name pattern: CapitalizedWord ending in Error or Exception
296
+ pattern = r'\b([A-Z][a-zA-Z]*(?:Error|Exception|Warning))\b'
297
+ matches = re.findall(pattern, text)
298
+
299
+ # Return the last match (most specific)
300
+ return matches[-1] if matches else None
301
+
302
+
303
+ def get_python_version() -> str:
304
+ """
305
+ Get current Python version as a string.
306
+
307
+ Returns:
308
+ Python version (e.g., "3.10.5")
309
+ """
310
+ return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
311
+
312
+
313
+ def is_syntax_error(error_type: str) -> bool:
314
+ """
315
+ Check if error type is a syntax-related error.
316
+
317
+ Args:
318
+ error_type: Name of exception class
319
+
320
+ Returns:
321
+ True if syntax-related, False otherwise
322
+ """
323
+ syntax_errors = [
324
+ "SyntaxError",
325
+ "IndentationError",
326
+ "TabError"
327
+ ]
328
+ return error_type in syntax_errors
329
+
330
+
331
+ def get_error_category(error_type: str) -> str:
332
+ """
333
+ Categorize an error into a broad category.
334
+
335
+ Args:
336
+ error_type: Name of exception class
337
+
338
+ Returns:
339
+ Category name (e.g., "Syntax", "Type", "File", etc.)
340
+ """
341
+ syntax_errors = ["SyntaxError", "IndentationError", "TabError"]
342
+ type_errors = ["TypeError", "ValueError", "AttributeError"]
343
+ name_errors = ["NameError", "UnboundLocalError"]
344
+ lookup_errors = ["KeyError", "IndexError", "LookupError"]
345
+ file_errors = ["FileNotFoundError", "FileExistsError", "PermissionError",
346
+ "IsADirectoryError", "NotADirectoryError", "IOError", "OSError"]
347
+ import_errors = ["ImportError", "ModuleNotFoundError"]
348
+ arithmetic_errors = ["ZeroDivisionError", "OverflowError", "FloatingPointError", "ArithmeticError"]
349
+ runtime_errors = ["RuntimeError", "RecursionError", "NotImplementedError"]
350
+ connection_errors = ["ConnectionError", "BrokenPipeError", "ConnectionAbortedError",
351
+ "ConnectionRefusedError", "ConnectionResetError", "TimeoutError"]
352
+
353
+ if error_type in syntax_errors:
354
+ return "Syntax"
355
+ elif error_type in type_errors:
356
+ return "Type"
357
+ elif error_type in name_errors:
358
+ return "Name"
359
+ elif error_type in lookup_errors:
360
+ return "Lookup"
361
+ elif error_type in file_errors:
362
+ return "File/IO"
363
+ elif error_type in import_errors:
364
+ return "Import"
365
+ elif error_type in arithmetic_errors:
366
+ return "Arithmetic"
367
+ elif error_type in runtime_errors:
368
+ return "Runtime"
369
+ elif error_type in connection_errors:
370
+ return "Connection"
371
+ else:
372
+ return "Other"
373
+
374
+
375
+ def clean_traceback_text(traceback_text: str) -> str:
376
+ """
377
+ Clean and normalize traceback text.
378
+
379
+ Removes ANSI codes, extra whitespace, and normalizes line endings.
380
+
381
+ Args:
382
+ traceback_text: Raw traceback string
383
+
384
+ Returns:
385
+ Cleaned traceback text
386
+ """
387
+ # Remove ANSI codes
388
+ cleaned = strip_ansi_codes(traceback_text)
389
+
390
+ # Normalize line endings
391
+ cleaned = cleaned.replace('\r\n', '\n').replace('\r', '\n')
392
+
393
+ # Remove excessive blank lines
394
+ lines = cleaned.split('\n')
395
+ cleaned_lines = []
396
+ prev_blank = False
397
+
398
+ for line in lines:
399
+ is_blank = line.strip() == ''
400
+ if is_blank and prev_blank:
401
+ continue # Skip consecutive blank lines
402
+ cleaned_lines.append(line)
403
+ prev_blank = is_blank
404
+
405
+ return '\n'.join(cleaned_lines).strip()
406
+
407
+
408
+ def highlight_code_line(code_line: str, position: Optional[int] = None) -> str:
409
+ """
410
+ Add visual highlighting to a code line (for error location).
411
+
412
+ Args:
413
+ code_line: Line of code
414
+ position: Character position to highlight (optional)
415
+
416
+ Returns:
417
+ Highlighted code line with caret (^) indicator
418
+ """
419
+ result = [code_line]
420
+
421
+ if position is not None and 0 <= position < len(code_line):
422
+ # Add caret line
423
+ caret_line = ' ' * position + '^'
424
+ result.append(caret_line)
425
+
426
+ return '\n'.join(result)
427
+
428
+
429
+ # Export all utility functions
430
+ __all__ = [
431
+ 'extract_error_info',
432
+ 'format_output',
433
+ 'tokenize_output',
434
+ 'strip_ansi_codes',
435
+ 'truncate_message',
436
+ 'extract_exception_from_string',
437
+ 'get_python_version',
438
+ 'is_syntax_error',
439
+ 'get_error_category',
440
+ 'clean_traceback_text',
441
+ 'highlight_code_line',
442
+ ]
pydefine/version.py ADDED
@@ -0,0 +1,20 @@
1
+ """
2
+ pydefine.version
3
+ ~~~~~~~~~~~~~~~~
4
+
5
+ Version information for pyDefine library.
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "Yahya"
10
+ __email__ = "yahyabuilds@gmail.com"
11
+ __license__ = "MIT"
12
+ __url__ = "https://github.com/mdyahhya/pydefine"
13
+ __description__ = "Convert Python errors into beginner-friendly explanations"
14
+
15
+ # Version info tuple
16
+ VERSION = tuple(map(int, __version__.split('.')))
17
+
18
+ # Library metadata
19
+ LIBRARY_NAME = "pyDefine"
20
+ PACKAGE_NAME = "pydefine"