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/__init__.py +84 -0
- pydefine/cli.py +284 -0
- pydefine/core.py +456 -0
- pydefine/i18n.py +344 -0
- pydefine/mapping.py +1037 -0
- pydefine/utils.py +442 -0
- pydefine/version.py +20 -0
- pydefine-1.0.0.dist-info/METADATA +395 -0
- pydefine-1.0.0.dist-info/RECORD +13 -0
- pydefine-1.0.0.dist-info/WHEEL +5 -0
- pydefine-1.0.0.dist-info/entry_points.txt +3 -0
- pydefine-1.0.0.dist-info/licenses/LICENSE +21 -0
- pydefine-1.0.0.dist-info/top_level.txt +1 -0
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"
|