traceback-id 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.
- traceback_id/__init__.py +19 -0
- traceback_id/__main__.py +8 -0
- traceback_id/adapters/__init__.py +1 -0
- traceback_id/adapters/base.py +36 -0
- traceback_id/adapters/python/__init__.py +1 -0
- traceback_id/adapters/python/adapter.py +140 -0
- traceback_id/adapters/python/rules.yaml +174 -0
- traceback_id/cli.py +78 -0
- traceback_id/core/__init__.py +1 -0
- traceback_id/core/explainer.py +108 -0
- traceback_id/core/formatter.py +54 -0
- traceback_id/core/models.py +52 -0
- traceback_id/core/registry.py +81 -0
- traceback_id/hook.py +53 -0
- traceback_id/rules/universal_categories.yaml +99 -0
- traceback_id-0.1.0.dist-info/METADATA +191 -0
- traceback_id-0.1.0.dist-info/RECORD +21 -0
- traceback_id-0.1.0.dist-info/WHEEL +5 -0
- traceback_id-0.1.0.dist-info/entry_points.txt +2 -0
- traceback_id-0.1.0.dist-info/licenses/LICENSE +21 -0
- traceback_id-0.1.0.dist-info/top_level.txt +1 -0
traceback_id/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""traceback_id — penjelasan error/traceback pemrograman dalam Bahasa
|
|
2
|
+
Indonesia, dimulai dari Python (lihat PRD.md).
|
|
3
|
+
|
|
4
|
+
Mode in-process (khusus Python, DX terbaik untuk kasus ini)::
|
|
5
|
+
|
|
6
|
+
import traceback_id
|
|
7
|
+
traceback_id.activate()
|
|
8
|
+
|
|
9
|
+
Mode CLI (universal, dipakai bahasa apa pun yang punya adapter)::
|
|
10
|
+
|
|
11
|
+
traceback_id run script.py
|
|
12
|
+
traceback_id run script.js
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from traceback_id.hook import activate, deactivate, is_active
|
|
17
|
+
|
|
18
|
+
__all__ = ["activate", "deactivate", "is_active", "__version__"]
|
|
19
|
+
__version__ = "0.1.0"
|
traceback_id/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Paket adapter bahasa. Lihat adapters/base.py untuk kontraknya."""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Kontrak adapter bahasa (ARCHITECTURE.md §4).
|
|
2
|
+
|
|
3
|
+
Setiap adapter — built-in maupun plugin komunitas yang didaftarkan lewat
|
|
4
|
+
entry_points `traceback_id.adapters` — wajib mengimplementasikan interface
|
|
5
|
+
ini. `core/` hanya bergantung pada `LanguageAdapter` + `StructuredError`,
|
|
6
|
+
tidak pernah mengimpor adapter bahasa tertentu secara langsung.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from traceback_id.core.models import StructuredError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LanguageAdapter(ABC):
|
|
17
|
+
"""Interface yang wajib dipenuhi setiap adapter bahasa."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
file_extensions: list[str]
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def capture(self, command: list[str]) -> str:
|
|
24
|
+
"""Jalankan command, kembalikan raw stderr/output sebagai teks."""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def parse(self, raw_output: str) -> StructuredError:
|
|
29
|
+
"""Ubah teks error mentah jadi StructuredError."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def rules_path(self) -> Path:
|
|
35
|
+
"""Lokasi file rule khusus bahasa ini."""
|
|
36
|
+
raise NotImplementedError
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapter Python. Lihat adapter.py untuk implementasi, rules.yaml untuk rule."""
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Adapter Python — implementasi pertama & satu-satunya yang matang di v0.1
|
|
2
|
+
(PRD.md §4). Semua kekhususan Python (capture via hook/subprocess, parsing
|
|
3
|
+
traceback) hidup di sini, terisolasi dari adapter bahasa lain.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from traceback_id.adapters.base import LanguageAdapter
|
|
15
|
+
from traceback_id.core.models import ErrorCategory, StructuredError
|
|
16
|
+
|
|
17
|
+
RULES_PATH = Path(__file__).resolve().parent / "rules.yaml"
|
|
18
|
+
|
|
19
|
+
# ` File "script.py", line 3, in <module>`
|
|
20
|
+
_FRAME_RE = re.compile(r'^\s*File "(?P<file>.+?)", line (?P<line>\d+)(?:, in (?P<func>.*))?$')
|
|
21
|
+
|
|
22
|
+
# Baris terakhir traceback Python, mis. `NameError: name 'x' is not defined`.
|
|
23
|
+
# StopIteration/SystemExit/KeyboardInterrupt/GeneratorExit tidak selalu
|
|
24
|
+
# punya prefix + pesan (mis. `StopIteration` bisa muncul sendirian tanpa
|
|
25
|
+
# titik dua sama sekali), jadi didaftarkan sebagai alternatif berdiri
|
|
26
|
+
# sendiri, terpisah dari pola "<prefix>Error/Exception/Warning".
|
|
27
|
+
_ERROR_LINE_RE = re.compile(
|
|
28
|
+
r"^(?P<error_type>[\w.]*(?:Error|Exception|Warning)"
|
|
29
|
+
r"|StopIteration|SystemExit|KeyboardInterrupt|GeneratorExit)"
|
|
30
|
+
r":?\s*(?P<message>.*)$"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _looks_like_traceback_line(text: str) -> bool:
|
|
35
|
+
"""True kalau `text` adalah baris "File ...", baris "ErrorType: ...",
|
|
36
|
+
atau header "Traceback (most recent call last):" — dipakai untuk
|
|
37
|
+
membedakan baris source-code asli dari baris struktur traceback."""
|
|
38
|
+
return (
|
|
39
|
+
bool(_FRAME_RE.match(text))
|
|
40
|
+
or bool(_ERROR_LINE_RE.match(text))
|
|
41
|
+
or text.startswith("Traceback (most recent call last)")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PythonAdapter(LanguageAdapter):
|
|
46
|
+
"""Adapter untuk error/traceback Python.
|
|
47
|
+
|
|
48
|
+
Selain kontrak dasar `capture()`/`parse()`, adapter ini juga dipakai
|
|
49
|
+
langsung oleh `traceback_id/hook.py` untuk mode in-process
|
|
50
|
+
(`sys.excepthook`) — satu-satunya adapter yang punya mode ini, karena
|
|
51
|
+
Python adalah bahasa implementasi library ini sendiri (ARCHITECTURE.md §4).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name = "python"
|
|
55
|
+
file_extensions = [".py", ".pyw"]
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._rules_cache: dict | None = None
|
|
59
|
+
self.last_returncode: int | None = None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def rules_path(self) -> Path:
|
|
63
|
+
return RULES_PATH
|
|
64
|
+
|
|
65
|
+
def capture(self, command: list) -> str:
|
|
66
|
+
"""Jalankan `command` sebagai subprocess Python, kembalikan stderr.
|
|
67
|
+
|
|
68
|
+
stdout program tetap diteruskan langsung ke terminal (real-time),
|
|
69
|
+
sedangkan stderr ditangkap untuk di-parse — kalau program tidak
|
|
70
|
+
error, stderr akan kosong dan tidak ada apa pun yang "hilang".
|
|
71
|
+
"""
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
[sys.executable, *command],
|
|
74
|
+
stdout=None,
|
|
75
|
+
stderr=subprocess.PIPE,
|
|
76
|
+
text=True,
|
|
77
|
+
)
|
|
78
|
+
self.last_returncode = result.returncode
|
|
79
|
+
return result.stderr
|
|
80
|
+
|
|
81
|
+
def parse(self, raw_output: str) -> StructuredError:
|
|
82
|
+
lines = raw_output.rstrip("\n").splitlines()
|
|
83
|
+
|
|
84
|
+
frames = []
|
|
85
|
+
for i, line in enumerate(lines):
|
|
86
|
+
match = _FRAME_RE.match(line)
|
|
87
|
+
if not match:
|
|
88
|
+
continue
|
|
89
|
+
code_context = None
|
|
90
|
+
if i + 1 < len(lines):
|
|
91
|
+
candidate = lines[i + 1].strip()
|
|
92
|
+
if candidate and not _looks_like_traceback_line(candidate):
|
|
93
|
+
code_context = candidate
|
|
94
|
+
frames.append(
|
|
95
|
+
{
|
|
96
|
+
"file": match.group("file"),
|
|
97
|
+
"line": int(match.group("line")),
|
|
98
|
+
"code_context": code_context,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
error_type = "UnknownError"
|
|
103
|
+
message = raw_output.strip() or "(tidak ada output error)"
|
|
104
|
+
for line in reversed(lines):
|
|
105
|
+
match = _ERROR_LINE_RE.match(line)
|
|
106
|
+
if match:
|
|
107
|
+
error_type = match.group("error_type")
|
|
108
|
+
message = match.group("message").strip()
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
last_frame = frames[-1] if frames else {}
|
|
112
|
+
|
|
113
|
+
return StructuredError(
|
|
114
|
+
language=self.name,
|
|
115
|
+
error_type=error_type,
|
|
116
|
+
category=self._category_for(error_type),
|
|
117
|
+
message=message,
|
|
118
|
+
file=last_frame.get("file"),
|
|
119
|
+
line=last_frame.get("line"),
|
|
120
|
+
code_context=last_frame.get("code_context"),
|
|
121
|
+
raw_output=raw_output,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _rules(self) -> dict:
|
|
125
|
+
if self._rules_cache is None:
|
|
126
|
+
if self.rules_path.exists():
|
|
127
|
+
with open(self.rules_path, encoding="utf-8") as fh:
|
|
128
|
+
self._rules_cache = yaml.safe_load(fh) or {}
|
|
129
|
+
else: # pragma: no cover - seharusnya tidak terjadi di instalasi normal
|
|
130
|
+
self._rules_cache = {}
|
|
131
|
+
return self._rules_cache
|
|
132
|
+
|
|
133
|
+
def _category_for(self, error_type: str) -> ErrorCategory:
|
|
134
|
+
rule = self._rules().get(error_type)
|
|
135
|
+
if rule and "category" in rule:
|
|
136
|
+
try:
|
|
137
|
+
return ErrorCategory(rule["category"])
|
|
138
|
+
except ValueError:
|
|
139
|
+
pass
|
|
140
|
+
return ErrorCategory.UNKNOWN
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Rule khusus Python (PRD.md §4 — 15 error type MVP).
|
|
2
|
+
#
|
|
3
|
+
# Key setiap entri = error_type persis seperti yang muncul di traceback
|
|
4
|
+
# (mis. "NameError"). Field:
|
|
5
|
+
# category -> salah satu nilai ErrorCategory di core/models.py
|
|
6
|
+
# pattern -> regex opsional dengan named group, dicocokkan ke
|
|
7
|
+
# StructuredError.message untuk mengisi variabel di
|
|
8
|
+
# template `penjelasan`/`saran` (format {nama_group})
|
|
9
|
+
# penjelasan -> kenapa error ini terjadi
|
|
10
|
+
# saran -> cara konkret memperbaikinya
|
|
11
|
+
#
|
|
12
|
+
# Kalau error_type tidak ada di sini, explainer fallback ke
|
|
13
|
+
# rules/universal_categories.yaml berdasarkan `category`.
|
|
14
|
+
|
|
15
|
+
NameError:
|
|
16
|
+
category: UNDEFINED_VARIABLE
|
|
17
|
+
pattern: "name '(?P<var>.+?)' is not defined"
|
|
18
|
+
penjelasan: >
|
|
19
|
+
Kode kamu mencoba memakai nama `{var}`, tapi Python belum pernah
|
|
20
|
+
melihat nama itu didefinisikan sebelumnya di titik ini.
|
|
21
|
+
saran: >
|
|
22
|
+
Periksa apakah `{var}` sudah kamu isi nilainya sebelum baris ini,
|
|
23
|
+
atau cek kemungkinan salah ketik nama variabel/fungsi.
|
|
24
|
+
|
|
25
|
+
UnboundLocalError:
|
|
26
|
+
category: UNDEFINED_VARIABLE
|
|
27
|
+
pattern: "local variable '(?P<var>.+?)'"
|
|
28
|
+
penjelasan: >
|
|
29
|
+
Di dalam fungsi ini, `{var}` dianggap sebagai variabel lokal (karena
|
|
30
|
+
ada baris yang meng-assign nilai ke situ di dalam fungsi), tapi kamu
|
|
31
|
+
memakainya sebelum baris assignment itu benar-benar dijalankan.
|
|
32
|
+
saran: >
|
|
33
|
+
Pastikan `{var}` sudah diberi nilai sebelum dipakai — lewat parameter
|
|
34
|
+
fungsi, inisialisasi di awal fungsi, atau `global {var}` kalau memang
|
|
35
|
+
ingin memakai variabel dari luar fungsi.
|
|
36
|
+
|
|
37
|
+
TypeError:
|
|
38
|
+
category: TYPE_MISMATCH
|
|
39
|
+
penjelasan: >
|
|
40
|
+
Kamu mencoba melakukan operasi pada nilai dengan tipe data yang tidak
|
|
41
|
+
cocok satu sama lain, atau memanggil sesuatu dengan cara yang tidak
|
|
42
|
+
sesuai tipe datanya.
|
|
43
|
+
saran: >
|
|
44
|
+
Cek tipe data tiap nilai yang terlibat (pakai `type(nilai)` kalau
|
|
45
|
+
perlu), lalu sesuaikan — misalnya ubah angka jadi teks dengan `str()`,
|
|
46
|
+
atau sebaliknya dengan `int()`/`float()`.
|
|
47
|
+
|
|
48
|
+
IndexError:
|
|
49
|
+
category: INDEX_OUT_OF_BOUNDS
|
|
50
|
+
penjelasan: >
|
|
51
|
+
Kamu mencoba mengakses posisi/index pada list atau tuple yang tidak
|
|
52
|
+
ada — index yang kamu pakai berada di luar jangkauan panjang data
|
|
53
|
+
tersebut.
|
|
54
|
+
saran: >
|
|
55
|
+
Cek dulu panjang list-nya dengan `len(...)` sebelum mengakses index
|
|
56
|
+
tertentu, dan ingat index di Python dimulai dari 0.
|
|
57
|
+
|
|
58
|
+
KeyError:
|
|
59
|
+
category: MISSING_KEY
|
|
60
|
+
pattern: "'(?P<key>.*)'"
|
|
61
|
+
penjelasan: >
|
|
62
|
+
Kamu mencoba mengambil nilai dari dictionary memakai key `{key}`,
|
|
63
|
+
tapi key itu tidak ada di dalam dictionary tersebut.
|
|
64
|
+
saran: >
|
|
65
|
+
Cek dulu apakah key `{key}` benar-benar ada, misalnya dengan
|
|
66
|
+
`if {key} in dict_kamu:`, atau pakai `dict_kamu.get({key})` yang
|
|
67
|
+
tidak error kalau key-nya tidak ketemu.
|
|
68
|
+
|
|
69
|
+
AttributeError:
|
|
70
|
+
category: ATTRIBUTE_ERROR
|
|
71
|
+
pattern: "'(?P<tipe>.+?)' object has no attribute '(?P<atribut>.+?)'"
|
|
72
|
+
penjelasan: >
|
|
73
|
+
Kamu mencoba memakai atribut atau method `{atribut}` pada objek
|
|
74
|
+
bertipe `{tipe}`, tapi tipe data itu tidak punya atribut/method
|
|
75
|
+
dengan nama tersebut.
|
|
76
|
+
saran: >
|
|
77
|
+
Cek lagi tipe data objeknya (`type(objek)`) dan pastikan nama
|
|
78
|
+
method/atributnya benar. Ini juga sering terjadi kalau nilainya
|
|
79
|
+
ternyata `None` — misalnya karena sebuah fungsi lupa `return`.
|
|
80
|
+
|
|
81
|
+
ZeroDivisionError:
|
|
82
|
+
category: DIVISION_BY_ZERO
|
|
83
|
+
penjelasan: >
|
|
84
|
+
Kode kamu mencoba membagi sebuah angka dengan 0, yang secara
|
|
85
|
+
matematika tidak terdefinisikan sehingga Python melempar error.
|
|
86
|
+
saran: >
|
|
87
|
+
Tambahkan pengecekan sebelum melakukan pembagian, misalnya
|
|
88
|
+
`if pembagi != 0:`, atau pastikan nilai pembagi memang tidak pernah
|
|
89
|
+
nol di kasus kamu.
|
|
90
|
+
|
|
91
|
+
ImportError:
|
|
92
|
+
category: MODULE_NOT_FOUND
|
|
93
|
+
pattern: "cannot import name '(?P<nama>.+?)' from '(?P<modul>.+?)'"
|
|
94
|
+
penjelasan: >
|
|
95
|
+
Kamu mencoba meng-import `{nama}` dari modul `{modul}`, tapi Python
|
|
96
|
+
tidak menemukan nama tersebut di dalam modul itu.
|
|
97
|
+
saran: >
|
|
98
|
+
Cek lagi ejaan nama yang di-import, dan pastikan modul `{modul}`
|
|
99
|
+
memang punya `{nama}` — bisa jadi versi library-nya berbeda dari
|
|
100
|
+
yang kamu kira.
|
|
101
|
+
|
|
102
|
+
ModuleNotFoundError:
|
|
103
|
+
category: MODULE_NOT_FOUND
|
|
104
|
+
pattern: "No module named '(?P<modul>.+?)'"
|
|
105
|
+
penjelasan: >
|
|
106
|
+
Python tidak menemukan modul/package bernama `{modul}` — kemungkinan
|
|
107
|
+
besar package itu belum ter-install, atau nama modulnya salah ketik.
|
|
108
|
+
saran: >
|
|
109
|
+
Coba install dengan `pip install {modul}` (sesuaikan nama package-nya
|
|
110
|
+
kalau beda dari nama modulnya), dan pastikan virtual environment yang
|
|
111
|
+
aktif memang sudah menginstall package tersebut.
|
|
112
|
+
|
|
113
|
+
ValueError:
|
|
114
|
+
category: VALUE_ERROR
|
|
115
|
+
penjelasan: >
|
|
116
|
+
Tipe data yang kamu pakai sudah benar, tapi nilainya tidak masuk
|
|
117
|
+
akal atau tidak valid untuk operasi yang sedang dilakukan.
|
|
118
|
+
saran: >
|
|
119
|
+
Periksa nilai yang dikirim ke fungsi/operasi ini — misalnya kalau
|
|
120
|
+
ini hasil `int(teks)`, pastikan `teks` benar-benar berisi angka.
|
|
121
|
+
|
|
122
|
+
FileNotFoundError:
|
|
123
|
+
category: FILE_NOT_FOUND
|
|
124
|
+
pattern: "No such file or directory: '(?P<berkas>.+?)'"
|
|
125
|
+
penjelasan: >
|
|
126
|
+
Kode kamu mencoba membuka file `{berkas}`, tapi Python tidak
|
|
127
|
+
menemukan file tersebut di lokasi yang dicari.
|
|
128
|
+
saran: >
|
|
129
|
+
Cek lagi path/nama file `{berkas}` — termasuk apakah kamu menjalankan
|
|
130
|
+
program dari folder yang benar, karena path relatif dihitung dari
|
|
131
|
+
folder tempat kamu menjalankan perintah, bukan dari lokasi file .py.
|
|
132
|
+
|
|
133
|
+
IndentationError:
|
|
134
|
+
category: SYNTAX_ERROR
|
|
135
|
+
penjelasan: >
|
|
136
|
+
Python sangat bergantung pada indentasi (spasi/tab di awal baris)
|
|
137
|
+
untuk mengetahui blok kode mana yang satu level. ada baris yang
|
|
138
|
+
indentasinya tidak konsisten dengan baris di sekitarnya.
|
|
139
|
+
saran: >
|
|
140
|
+
Pastikan kode di blok yang sama (misalnya di dalam satu `if`, `for`,
|
|
141
|
+
atau fungsi) punya jumlah indentasi yang sama persis. Hindari
|
|
142
|
+
mencampur tab dan spasi.
|
|
143
|
+
|
|
144
|
+
SyntaxError:
|
|
145
|
+
category: SYNTAX_ERROR
|
|
146
|
+
penjelasan: >
|
|
147
|
+
Ada bagian kode yang tidak mengikuti aturan penulisan (grammar)
|
|
148
|
+
Python, sehingga Python tidak bisa mem-parsing/membaca kodenya sama
|
|
149
|
+
sekali sebelum sempat dijalankan.
|
|
150
|
+
saran: >
|
|
151
|
+
Lihat tanda panah (^) di traceback di atas — itu menunjukkan kira-kira
|
|
152
|
+
di mana Python berhenti memahami kodenya. Cek tanda kurung, titik dua,
|
|
153
|
+
atau kutip yang mungkin belum ditutup.
|
|
154
|
+
|
|
155
|
+
RecursionError:
|
|
156
|
+
category: RECURSION_ERROR
|
|
157
|
+
penjelasan: >
|
|
158
|
+
Fungsi kamu memanggil dirinya sendiri (rekursi) terlalu banyak kali
|
|
159
|
+
tanpa pernah berhenti, sampai melebihi batas kedalaman rekursi yang
|
|
160
|
+
diizinkan Python.
|
|
161
|
+
saran: >
|
|
162
|
+
Pastikan fungsi rekursifmu punya base case (kondisi berhenti) yang
|
|
163
|
+
pasti tercapai, dan cek apakah tiap pemanggilan rekursif benar-benar
|
|
164
|
+
membuat progres menuju base case itu.
|
|
165
|
+
|
|
166
|
+
StopIteration:
|
|
167
|
+
category: VALUE_ERROR
|
|
168
|
+
penjelasan: >
|
|
169
|
+
Kamu memanggil `next()` pada sebuah iterator yang sudah habis — tidak
|
|
170
|
+
ada lagi elemen yang bisa diambil.
|
|
171
|
+
saran: >
|
|
172
|
+
Kalau ini terjadi di dalam loop manual dengan `next()`, tangkap
|
|
173
|
+
dengan `try/except StopIteration`, atau pertimbangkan memakai `for`
|
|
174
|
+
biasa yang otomatis berhenti saat iterator habis.
|
traceback_id/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Mode CLI, universal lintas bahasa (ARCHITECTURE.md §2, §6; FR8).
|
|
2
|
+
|
|
3
|
+
`traceback_id run script.py` mendeteksi bahasa dari ekstensi file, mengambil
|
|
4
|
+
adapter dari `core/registry.py`, lalu `capture()` menjalankan file sebagai
|
|
5
|
+
subprocess dan `parse()` menghasilkan `StructuredError` yang diteruskan ke
|
|
6
|
+
`core/explainer.py` dan `core/formatter.py` seperti biasa.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from traceback_id import __version__
|
|
14
|
+
from traceback_id.core.explainer import Explainer
|
|
15
|
+
from traceback_id.core.formatter import print_output
|
|
16
|
+
from traceback_id.core.registry import AdapterNotFoundError, get_registry
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="traceback_id",
|
|
22
|
+
description="Jalankan sebuah file dan dapatkan penjelasan error dalam Bahasa Indonesia.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("--version", action="version", version=f"traceback_id {__version__}")
|
|
25
|
+
|
|
26
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
27
|
+
|
|
28
|
+
run_parser = subparsers.add_parser("run", help="Jalankan sebuah file dan tangkap errornya.")
|
|
29
|
+
run_parser.add_argument("file", help="Path ke file yang akan dijalankan, mis. script.py")
|
|
30
|
+
run_parser.add_argument(
|
|
31
|
+
"--lang",
|
|
32
|
+
default=None,
|
|
33
|
+
help="Paksa bahasa tertentu (default: deteksi otomatis dari ekstensi file).",
|
|
34
|
+
)
|
|
35
|
+
run_parser.add_argument(
|
|
36
|
+
"args",
|
|
37
|
+
nargs=argparse.REMAINDER,
|
|
38
|
+
help="Argumen tambahan yang diteruskan ke file/program.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return parser
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: list | None = None) -> int:
|
|
45
|
+
parser = build_parser()
|
|
46
|
+
args = parser.parse_args(argv)
|
|
47
|
+
|
|
48
|
+
if args.command == "run":
|
|
49
|
+
return _run(args)
|
|
50
|
+
|
|
51
|
+
parser.print_help() # pragma: no cover - argparse `required=True` sudah menutup jalur ini
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _run(args: argparse.Namespace) -> int:
|
|
56
|
+
registry = get_registry()
|
|
57
|
+
try:
|
|
58
|
+
adapter = registry.get_by_name(args.lang) if args.lang else registry.get_by_extension(args.file)
|
|
59
|
+
except AdapterNotFoundError as exc:
|
|
60
|
+
print(f"traceback_id: {exc}", file=sys.stderr)
|
|
61
|
+
return 2
|
|
62
|
+
|
|
63
|
+
raw_output = adapter.capture([args.file, *args.args])
|
|
64
|
+
returncode = getattr(adapter, "last_returncode", None)
|
|
65
|
+
if returncode is None:
|
|
66
|
+
returncode = 1 if raw_output.strip() else 0
|
|
67
|
+
|
|
68
|
+
if not raw_output.strip():
|
|
69
|
+
return returncode
|
|
70
|
+
|
|
71
|
+
structured = adapter.parse(raw_output)
|
|
72
|
+
explanation = Explainer().explain(structured, adapter)
|
|
73
|
+
print_output(structured, explanation)
|
|
74
|
+
return returncode
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Mesin inti traceback_id — language-agnostic (ARCHITECTURE.md §3)."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Mencocokkan StructuredError ke rule penjelasan (ARCHITECTURE.md §3, §5).
|
|
2
|
+
|
|
3
|
+
Prioritas pencarian rule:
|
|
4
|
+
1. Rule spesifik-bahasa (`adapters/<lang>/rules.yaml`), key = error_type
|
|
5
|
+
2. Fallback rule universal (`rules/universal_categories.yaml`), key = category
|
|
6
|
+
3. Fallback generik kalau keduanya tidak ada
|
|
7
|
+
|
|
8
|
+
Modul ini tidak tahu dan tidak perlu tahu bahasa apa yang sedang diproses —
|
|
9
|
+
hanya bekerja lewat field di `StructuredError` dan `LanguageAdapter.rules_path`.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from traceback_id.core.models import ErrorCategory, StructuredError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from traceback_id.adapters.base import LanguageAdapter
|
|
25
|
+
|
|
26
|
+
UNIVERSAL_RULES_PATH = Path(__file__).resolve().parent.parent / "rules" / "universal_categories.yaml"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Explanation:
|
|
31
|
+
"""Hasil akhir penjelasan sebuah StructuredError, siap ditampilkan."""
|
|
32
|
+
|
|
33
|
+
penjelasan: str
|
|
34
|
+
saran: str | None = None
|
|
35
|
+
source: str = "fallback" # "language-specific" | "universal" | "fallback"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _SafeDict(dict):
|
|
39
|
+
"""dict yang tidak melempar KeyError saat template.format_map dipakai —
|
|
40
|
+
supaya rule.yaml yang polanya sedikit meleset tidak sampai membuat
|
|
41
|
+
traceback_id sendiri crash."""
|
|
42
|
+
|
|
43
|
+
def __missing__(self, key: str) -> str:
|
|
44
|
+
return f"<{key}>"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@lru_cache(maxsize=None)
|
|
48
|
+
def _load_yaml(path: Path) -> dict:
|
|
49
|
+
if not path.exists():
|
|
50
|
+
return {}
|
|
51
|
+
with open(path, encoding="utf-8") as fh:
|
|
52
|
+
return yaml.safe_load(fh) or {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract_variables(pattern: str | None, message: str) -> dict:
|
|
56
|
+
if not pattern:
|
|
57
|
+
return {}
|
|
58
|
+
match = re.search(pattern, message)
|
|
59
|
+
return match.groupdict() if match else {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _render(template: str | None, variables: dict) -> str | None:
|
|
63
|
+
if not template:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
return template.format_map(_SafeDict(variables)).strip()
|
|
67
|
+
except Exception:
|
|
68
|
+
return template.strip()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Explainer:
|
|
72
|
+
"""Cocokkan StructuredError ke rule & render template penjelasannya."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, universal_rules_path: Path | None = None) -> None:
|
|
75
|
+
self._universal_rules_path = universal_rules_path or UNIVERSAL_RULES_PATH
|
|
76
|
+
|
|
77
|
+
def explain(self, error: StructuredError, adapter: "LanguageAdapter") -> Explanation:
|
|
78
|
+
# 1. Rule spesifik-bahasa, key = error_type native (mis. "NameError")
|
|
79
|
+
language_rules = _load_yaml(adapter.rules_path)
|
|
80
|
+
rule = language_rules.get(error.error_type)
|
|
81
|
+
|
|
82
|
+
if rule:
|
|
83
|
+
variables = _extract_variables(rule.get("pattern"), error.message)
|
|
84
|
+
penjelasan = _render(rule.get("penjelasan"), variables)
|
|
85
|
+
saran = _render(rule.get("saran"), variables)
|
|
86
|
+
if penjelasan:
|
|
87
|
+
return Explanation(penjelasan=penjelasan, saran=saran, source="language-specific")
|
|
88
|
+
|
|
89
|
+
# 2. Fallback rule universal, key = category
|
|
90
|
+
universal_rules = _load_yaml(self._universal_rules_path)
|
|
91
|
+
category_key = error.category.value if isinstance(error.category, ErrorCategory) else str(error.category)
|
|
92
|
+
universal_rule = universal_rules.get(category_key)
|
|
93
|
+
|
|
94
|
+
if universal_rule:
|
|
95
|
+
penjelasan = _render(universal_rule.get("penjelasan"), {})
|
|
96
|
+
saran = _render(universal_rule.get("saran"), {})
|
|
97
|
+
if penjelasan:
|
|
98
|
+
return Explanation(penjelasan=penjelasan, saran=saran, source="universal")
|
|
99
|
+
|
|
100
|
+
# 3. Tidak ada rule yang cocok sama sekali
|
|
101
|
+
return Explanation(
|
|
102
|
+
penjelasan=(
|
|
103
|
+
"traceback_id belum punya penjelasan khusus untuk jenis error "
|
|
104
|
+
f"'{error.error_type}' ini."
|
|
105
|
+
),
|
|
106
|
+
saran="Coba baca pesan error asli di atas, atau cari error_type ini di dokumentasi resmi bahasa terkait.",
|
|
107
|
+
source="fallback",
|
|
108
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Susun raw_output asli + penjelasan jadi satu output akhir (ARCHITECTURE.md §3).
|
|
2
|
+
|
|
3
|
+
Pewarnaan lewat `rich` bersifat opsional (lihat pyproject.toml) — kalau tidak
|
|
4
|
+
ter-install, fallback otomatis ke teks polos.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from traceback_id.core.explainer import Explanation
|
|
11
|
+
from traceback_id.core.models import StructuredError
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
|
|
17
|
+
_HAS_RICH = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
_HAS_RICH = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_DIVIDER = "-" * 60
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_output(error: StructuredError, explanation: Explanation) -> str:
|
|
26
|
+
"""Versi teks polos, deterministic — dipakai formatter tanpa `rich`
|
|
27
|
+
dan gampang di-assert langsung di test."""
|
|
28
|
+
parts = [
|
|
29
|
+
error.raw_output.rstrip("\n"),
|
|
30
|
+
_DIVIDER,
|
|
31
|
+
"penjelasan traceback_id (Bahasa Indonesia):",
|
|
32
|
+
explanation.penjelasan,
|
|
33
|
+
]
|
|
34
|
+
if explanation.saran:
|
|
35
|
+
parts.extend(["", f"Saran: {explanation.saran}"])
|
|
36
|
+
return "\n".join(parts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_output(error: StructuredError, explanation: Explanation, use_color: bool | None = None) -> None:
|
|
40
|
+
"""Cetak output akhir ke stderr — dengan warna lewat `rich` kalau
|
|
41
|
+
tersedia, fallback ke teks polos kalau tidak."""
|
|
42
|
+
if use_color is None:
|
|
43
|
+
use_color = _HAS_RICH
|
|
44
|
+
|
|
45
|
+
if use_color and _HAS_RICH:
|
|
46
|
+
console = Console(stderr=True)
|
|
47
|
+
console.print(error.raw_output.rstrip("\n"), style="dim")
|
|
48
|
+
body = f"[bold]{explanation.penjelasan}[/bold]"
|
|
49
|
+
if explanation.saran:
|
|
50
|
+
body += f"\n\n[green]Saran:[/green] {explanation.saran}"
|
|
51
|
+
console.print(Panel(body, title="penjelasan traceback_id", border_style="cyan"))
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
print(format_output(error, explanation), file=sys.stderr)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Struktur data seragam lintas bahasa (ARCHITECTURE.md §3).
|
|
2
|
+
|
|
3
|
+
`core/` (matcher, explainer, formatter) hanya boleh bergantung pada
|
|
4
|
+
struktur data di file ini — tidak boleh tahu apa pun tentang Python,
|
|
5
|
+
JavaScript, atau bahasa spesifik lainnya.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ErrorCategory(str, Enum):
|
|
14
|
+
"""Konsep error universal, dipetakan dari error_type native tiap bahasa.
|
|
15
|
+
|
|
16
|
+
Ini yang memungkinkan `rules/universal_categories.yaml` dipakai sebagai
|
|
17
|
+
fallback penjelasan lintas bahasa (lihat ARCHITECTURE.md §5).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
UNDEFINED_VARIABLE = "UNDEFINED_VARIABLE"
|
|
21
|
+
TYPE_MISMATCH = "TYPE_MISMATCH"
|
|
22
|
+
INDEX_OUT_OF_BOUNDS = "INDEX_OUT_OF_BOUNDS"
|
|
23
|
+
MISSING_KEY = "MISSING_KEY"
|
|
24
|
+
DIVISION_BY_ZERO = "DIVISION_BY_ZERO"
|
|
25
|
+
MODULE_NOT_FOUND = "MODULE_NOT_FOUND"
|
|
26
|
+
SYNTAX_ERROR = "SYNTAX_ERROR"
|
|
27
|
+
ATTRIBUTE_ERROR = "ATTRIBUTE_ERROR"
|
|
28
|
+
FILE_NOT_FOUND = "FILE_NOT_FOUND"
|
|
29
|
+
RECURSION_ERROR = "RECURSION_ERROR"
|
|
30
|
+
VALUE_ERROR = "VALUE_ERROR"
|
|
31
|
+
UNKNOWN = "UNKNOWN"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class StructuredError:
|
|
36
|
+
"""Hasil parsing error mentah, seragam untuk bahasa apa pun.
|
|
37
|
+
|
|
38
|
+
Field ini adalah kontrak antara adapter (yang mengisinya lewat
|
|
39
|
+
`parse()`) dan `core/explainer.py` + `core/formatter.py` (yang
|
|
40
|
+
membacanya tanpa perlu tahu bahasa asalnya).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
language: str # "python", "javascript", "java", ...
|
|
44
|
+
error_type: str # nama native, mis. "NameError", "ReferenceError"
|
|
45
|
+
category: ErrorCategory # konsep semantik universal, lihat ErrorCategory
|
|
46
|
+
message: str # pesan asli dari runtime/compiler
|
|
47
|
+
file: str | None
|
|
48
|
+
line: int | None
|
|
49
|
+
column: int | None = None
|
|
50
|
+
code_context: str | None = None
|
|
51
|
+
variables: dict = field(default_factory=dict)
|
|
52
|
+
raw_output: str = "" # traceback/stack trace asli, untuk ditampilkan utuh
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Daftar adapter yang tersedia — built-in + plugin komunitas (ARCHITECTURE.md §3).
|
|
2
|
+
|
|
3
|
+
Built-in didaftarkan manual di `_register_builtin_adapters()`. Plugin
|
|
4
|
+
komunitas ditemukan otomatis lewat `importlib.metadata.entry_points` dengan
|
|
5
|
+
grup `traceback_id.adapters`, tanpa perlu mengubah file ini (ARCHITECTURE.md
|
|
6
|
+
§1, "Dibuka untuk plugin komunitas").
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib import metadata
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from traceback_id.adapters.base import LanguageAdapter
|
|
14
|
+
|
|
15
|
+
ENTRY_POINT_GROUP = "traceback_id.adapters"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AdapterNotFoundError(Exception):
|
|
19
|
+
"""Tidak ada adapter yang cocok untuk bahasa/ekstensi file yang diminta."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Registry:
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._adapters: dict = {}
|
|
25
|
+
self._register_builtin_adapters()
|
|
26
|
+
self._discover_plugin_adapters()
|
|
27
|
+
|
|
28
|
+
def _register_builtin_adapters(self) -> None:
|
|
29
|
+
from traceback_id.adapters.python.adapter import PythonAdapter
|
|
30
|
+
|
|
31
|
+
self.register(PythonAdapter())
|
|
32
|
+
|
|
33
|
+
def _discover_plugin_adapters(self) -> None:
|
|
34
|
+
try:
|
|
35
|
+
entry_points = metadata.entry_points(group=ENTRY_POINT_GROUP)
|
|
36
|
+
except TypeError:
|
|
37
|
+
# Python 3.8/3.9: entry_points() tidak menerima kwarg `group`.
|
|
38
|
+
entry_points = metadata.entry_points().get(ENTRY_POINT_GROUP, [])
|
|
39
|
+
|
|
40
|
+
for ep in entry_points:
|
|
41
|
+
try:
|
|
42
|
+
adapter_cls = ep.load()
|
|
43
|
+
self.register(adapter_cls())
|
|
44
|
+
except Exception as exc: # pragma: no cover - defensif, jangan sampai plugin rusak matikan CLI
|
|
45
|
+
print(f"[traceback_id] gagal memuat plugin adapter '{ep.name}': {exc}")
|
|
46
|
+
|
|
47
|
+
def register(self, adapter: LanguageAdapter) -> None:
|
|
48
|
+
self._adapters[adapter.name] = adapter
|
|
49
|
+
|
|
50
|
+
def get_by_name(self, name: str) -> LanguageAdapter:
|
|
51
|
+
try:
|
|
52
|
+
return self._adapters[name]
|
|
53
|
+
except KeyError as exc:
|
|
54
|
+
raise AdapterNotFoundError(
|
|
55
|
+
f"Adapter untuk bahasa '{name}' tidak ditemukan. "
|
|
56
|
+
f"Bahasa yang tersedia: {', '.join(self.available_languages()) or '(tidak ada)'}"
|
|
57
|
+
) from exc
|
|
58
|
+
|
|
59
|
+
def get_by_extension(self, path) -> LanguageAdapter:
|
|
60
|
+
ext = Path(path).suffix.lower()
|
|
61
|
+
for adapter in self._adapters.values():
|
|
62
|
+
if ext in adapter.file_extensions:
|
|
63
|
+
return adapter
|
|
64
|
+
raise AdapterNotFoundError(
|
|
65
|
+
f"Tidak ada adapter yang mendukung ekstensi file '{ext}'. "
|
|
66
|
+
f"Bahasa yang tersedia: {', '.join(self.available_languages()) or '(tidak ada)'}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def available_languages(self) -> list:
|
|
70
|
+
return sorted(self._adapters)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_default_registry: Registry | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_registry() -> Registry:
|
|
77
|
+
"""Registry singleton default, dipakai oleh cli.py."""
|
|
78
|
+
global _default_registry
|
|
79
|
+
if _default_registry is None:
|
|
80
|
+
_default_registry = Registry()
|
|
81
|
+
return _default_registry
|
traceback_id/hook.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Mode in-process, khusus Python (ARCHITECTURE.md §2, §6).
|
|
2
|
+
|
|
3
|
+
Memasang `sys.excepthook` supaya exception yang tidak tertangani otomatis
|
|
4
|
+
lewat `adapters/python/adapter.py` -> `core/explainer.py` -> `core/formatter.py`
|
|
5
|
+
sebelum ditampilkan. Ini satu-satunya adapter yang punya mode in-process,
|
|
6
|
+
karena Python adalah bahasa implementasi library ini sendiri.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import traceback as _traceback_module
|
|
12
|
+
|
|
13
|
+
from traceback_id.adapters.python.adapter import PythonAdapter
|
|
14
|
+
from traceback_id.core.explainer import Explainer
|
|
15
|
+
from traceback_id.core.formatter import print_output
|
|
16
|
+
|
|
17
|
+
_adapter = PythonAdapter()
|
|
18
|
+
_explainer = Explainer()
|
|
19
|
+
_original_excepthook = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _excepthook(exc_type, exc_value, exc_tb) -> None:
|
|
23
|
+
raw_output = "".join(_traceback_module.format_exception(exc_type, exc_value, exc_tb))
|
|
24
|
+
try:
|
|
25
|
+
structured = _adapter.parse(raw_output)
|
|
26
|
+
explanation = _explainer.explain(structured, _adapter)
|
|
27
|
+
print_output(structured, explanation)
|
|
28
|
+
except Exception:
|
|
29
|
+
# Kalau traceback_id sendiri gagal, jangan sampai menelan traceback
|
|
30
|
+
# asli pengguna — selalu jatuh balik ke excepthook bawaan Python.
|
|
31
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def activate() -> None:
|
|
35
|
+
"""Aktifkan penjelasan error otomatis untuk exception yang tidak
|
|
36
|
+
tertangani (FR4). Tidak mengubah exit code program (FR1)."""
|
|
37
|
+
global _original_excepthook
|
|
38
|
+
if _original_excepthook is not None:
|
|
39
|
+
return # sudah aktif
|
|
40
|
+
_original_excepthook = sys.excepthook
|
|
41
|
+
sys.excepthook = _excepthook
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def deactivate() -> None:
|
|
45
|
+
"""Kembalikan sys.excepthook ke behavior sebelum activate() dipanggil (FR6)."""
|
|
46
|
+
global _original_excepthook
|
|
47
|
+
if _original_excepthook is not None:
|
|
48
|
+
sys.excepthook = _original_excepthook
|
|
49
|
+
_original_excepthook = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_active() -> bool:
|
|
53
|
+
return _original_excepthook is not None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Penjelasan generik per ErrorCategory (ARCHITECTURE.md §5), dipakai sebagai
|
|
2
|
+
# fallback kalau tidak ada rule spesifik-bahasa yang cocok di
|
|
3
|
+
# adapters/<lang>/rules.yaml. Karena dipakai lintas bahasa pemrograman,
|
|
4
|
+
# penjelasannya sengaja netral — tidak menyebut istilah spesifik satu
|
|
5
|
+
# bahasa saja.
|
|
6
|
+
|
|
7
|
+
UNDEFINED_VARIABLE:
|
|
8
|
+
penjelasan: >
|
|
9
|
+
Kamu mencoba memakai sesuatu (variabel, fungsi, atau nama lain) yang
|
|
10
|
+
belum didefinisikan, atau sudah keluar dari jangkauan (scope)-nya di
|
|
11
|
+
titik kode ini.
|
|
12
|
+
saran: >
|
|
13
|
+
Periksa penulisan namanya, dan pastikan sudah didefinisikan sebelum
|
|
14
|
+
dipakai di baris ini.
|
|
15
|
+
|
|
16
|
+
TYPE_MISMATCH:
|
|
17
|
+
penjelasan: >
|
|
18
|
+
Ada operasi yang dilakukan pada nilai dengan tipe data yang tidak
|
|
19
|
+
cocok atau tidak didukung untuk operasi tersebut.
|
|
20
|
+
saran: >
|
|
21
|
+
Cek tipe data nilai-nilai yang terlibat, lalu konversi ke tipe yang
|
|
22
|
+
sesuai sebelum melakukan operasinya.
|
|
23
|
+
|
|
24
|
+
INDEX_OUT_OF_BOUNDS:
|
|
25
|
+
penjelasan: >
|
|
26
|
+
Kamu mencoba mengakses posisi/index pada kumpulan data (list, array,
|
|
27
|
+
dsb.) yang berada di luar jangkauan panjang data tersebut.
|
|
28
|
+
saran: >
|
|
29
|
+
Cek dulu panjang/ukuran datanya sebelum mengakses index tertentu.
|
|
30
|
+
|
|
31
|
+
MISSING_KEY:
|
|
32
|
+
penjelasan: >
|
|
33
|
+
Kamu mencoba mengambil nilai dari struktur data (seperti dictionary
|
|
34
|
+
atau map) memakai kunci yang ternyata tidak ada di dalamnya.
|
|
35
|
+
saran: >
|
|
36
|
+
Pastikan kunci yang dipakai benar-benar ada sebelum mengambil
|
|
37
|
+
nilainya, atau sediakan nilai default kalau kunci tidak ditemukan.
|
|
38
|
+
|
|
39
|
+
DIVISION_BY_ZERO:
|
|
40
|
+
penjelasan: >
|
|
41
|
+
Ada pembagian dengan angka 0, yang secara matematika tidak
|
|
42
|
+
terdefinisikan.
|
|
43
|
+
saran: >
|
|
44
|
+
Tambahkan pengecekan supaya pembagi tidak pernah bernilai 0 sebelum
|
|
45
|
+
melakukan pembagian.
|
|
46
|
+
|
|
47
|
+
MODULE_NOT_FOUND:
|
|
48
|
+
penjelasan: >
|
|
49
|
+
Program mencoba memakai modul/package/library yang tidak ditemukan —
|
|
50
|
+
kemungkinan belum ter-install, salah nama, atau belum diimpor dengan
|
|
51
|
+
benar.
|
|
52
|
+
saran: >
|
|
53
|
+
Pastikan nama modulnya benar dan sudah ter-install di environment
|
|
54
|
+
yang kamu pakai untuk menjalankan program.
|
|
55
|
+
|
|
56
|
+
SYNTAX_ERROR:
|
|
57
|
+
penjelasan: >
|
|
58
|
+
Ada bagian kode yang tidak mengikuti aturan penulisan bahasa
|
|
59
|
+
pemrogramannya, sehingga kode tidak bisa diproses sama sekali sebelum
|
|
60
|
+
sempat dijalankan.
|
|
61
|
+
saran: >
|
|
62
|
+
Periksa tanda kurung, titik dua/titik koma, kutip, dan indentasi di
|
|
63
|
+
sekitar lokasi error yang ditunjukkan.
|
|
64
|
+
|
|
65
|
+
ATTRIBUTE_ERROR:
|
|
66
|
+
penjelasan: >
|
|
67
|
+
Kamu mencoba memakai atribut, properti, atau method yang tidak
|
|
68
|
+
dimiliki oleh tipe data/objek tersebut.
|
|
69
|
+
saran: >
|
|
70
|
+
Cek lagi tipe data objeknya dan pastikan nama atribut/method-nya
|
|
71
|
+
benar.
|
|
72
|
+
|
|
73
|
+
FILE_NOT_FOUND:
|
|
74
|
+
penjelasan: >
|
|
75
|
+
Program mencoba membuka atau mengakses file yang tidak ditemukan di
|
|
76
|
+
lokasi yang dicari.
|
|
77
|
+
saran: >
|
|
78
|
+
Cek lagi nama dan lokasi (path) file-nya, termasuk folder tempat
|
|
79
|
+
kamu menjalankan program.
|
|
80
|
+
|
|
81
|
+
RECURSION_ERROR:
|
|
82
|
+
penjelasan: >
|
|
83
|
+
Sebuah fungsi memanggil dirinya sendiri terlalu banyak kali tanpa
|
|
84
|
+
pernah berhenti, sampai melebihi batas kedalaman yang diizinkan.
|
|
85
|
+
saran: >
|
|
86
|
+
Pastikan ada kondisi berhenti (base case) yang pasti tercapai dalam
|
|
87
|
+
fungsi rekursifmu.
|
|
88
|
+
|
|
89
|
+
VALUE_ERROR:
|
|
90
|
+
penjelasan: >
|
|
91
|
+
Tipe datanya sudah sesuai, tapi nilainya tidak valid atau tidak masuk
|
|
92
|
+
akal untuk operasi yang sedang dilakukan.
|
|
93
|
+
saran: >
|
|
94
|
+
Periksa lagi nilai yang dipakai, terutama kalau nilainya berasal dari
|
|
95
|
+
input pengguna atau konversi tipe data.
|
|
96
|
+
|
|
97
|
+
# Sengaja tidak ada entri UNKNOWN di sini — kalau category-nya benar-benar
|
|
98
|
+
# UNKNOWN (tidak ada rule bahasa maupun mapping kategori yang cocok),
|
|
99
|
+
# core/explainer.py jatuh ke fallback generik bawaan kode (tier ke-3).
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: traceback-id
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Penjelasan error/traceback pemrograman dalam Bahasa Indonesia, untuk pemula.
|
|
5
|
+
Author: traceback_id contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/traceback-id/traceback_id
|
|
8
|
+
Project-URL: Issues, https://github.com/traceback-id/traceback_id/issues
|
|
9
|
+
Keywords: error,traceback,bahasa indonesia,belajar coding,education,debugging
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Intended Audience :: Education
|
|
18
|
+
Classifier: Natural Language :: Indonesian
|
|
19
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: PyYAML>=6.0
|
|
25
|
+
Provides-Extra: color
|
|
26
|
+
Requires-Dist: rich>=13.0; extra == "color"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: rich>=13.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# traceback_id
|
|
33
|
+
|
|
34
|
+
Penjelasan error/traceback pemrograman dalam **Bahasa Indonesia**, untuk
|
|
35
|
+
pemula — dimulai dari Python.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Kenapa traceback_id?
|
|
39
|
+
|
|
40
|
+
Traceback Python (atau stack trace/compiler error di bahasa lain) ditulis
|
|
41
|
+
dalam bahasa Inggris teknis yang sering bikin pemula mentok. traceback_id
|
|
42
|
+
tetap menampilkan error aslinya (developer berpengalaman tetap butuh itu),
|
|
43
|
+
lalu menambahkan penjelasan singkat dalam Bahasa Indonesia: **kenapa** error
|
|
44
|
+
ini terjadi, dan **apa** yang bisa dicoba untuk memperbaikinya.
|
|
45
|
+
|
|
46
|
+
## Instalasi
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e ".[color]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`rich` (untuk output berwarna) sifatnya opsional — tanpa `[color]`,
|
|
53
|
+
traceback_id tetap berfungsi penuh dengan output teks polos.
|
|
54
|
+
|
|
55
|
+
## Pemakaian
|
|
56
|
+
|
|
57
|
+
### Mode in-process (khusus Python — DX terbaik)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import traceback_id
|
|
61
|
+
traceback_id.activate()
|
|
62
|
+
|
|
63
|
+
print(nilai_yang_tidak_ada)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Traceback (most recent call last):
|
|
68
|
+
File "contoh.py", line 4, in <module>
|
|
69
|
+
print(nilai_yang_tidak_ada)
|
|
70
|
+
NameError: name 'nilai_yang_tidak_ada' is not defined
|
|
71
|
+
------------------------------------------------------------
|
|
72
|
+
penjelasan traceback_id (Bahasa Indonesia):
|
|
73
|
+
Kode kamu mencoba memakai nama `nilai_yang_tidak_ada`, tapi Python belum
|
|
74
|
+
pernah melihat nama itu didefinisikan sebelumnya di titik ini.
|
|
75
|
+
|
|
76
|
+
Saran: Periksa apakah `nilai_yang_tidak_ada` sudah kamu isi nilainya
|
|
77
|
+
sebelum baris ini, atau cek kemungkinan salah ketik nama variabel/fungsi.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Nonaktifkan kapan saja dengan `traceback_id.deactivate()` — traceback
|
|
81
|
+
kembali ke behavior default Python, dan exit code program tidak berubah.
|
|
82
|
+
|
|
83
|
+
### Mode CLI (universal — dipakai bahasa apa pun yang punya adapter)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
traceback_id run script.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Ekstensi file (`.py`, nanti `.js`, `.java`, dst) dipakai untuk memilih
|
|
90
|
+
adapter secara otomatis, atau paksa lewat `--lang`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
traceback_id run script.mjs --lang javascript
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Contoh untuk dicoba
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
traceback_id run examples/demo_errors.py name_error
|
|
100
|
+
traceback_id run examples/syntax_error_demo.py
|
|
101
|
+
traceback_id run examples/indentation_error_demo.py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Lihat `examples/demo_errors.py` untuk daftar lengkap 15 jenis error yang
|
|
105
|
+
di-cover di v0.1 (`name_error`, `type_error`, `index_error`, `key_error`,
|
|
106
|
+
`attribute_error`, `zero_division_error`, `import_error`,
|
|
107
|
+
`module_not_found_error`, `value_error`, `file_not_found_error`,
|
|
108
|
+
`recursion_error`, `stop_iteration`, `unbound_local_error`, plus
|
|
109
|
+
`syntax_error_demo.py` & `indentation_error_demo.py` yang terpisah).
|
|
110
|
+
|
|
111
|
+
## Struktur Proyek
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
traceback_id/
|
|
115
|
+
├── traceback_id/ # package yang diinstall
|
|
116
|
+
│ ├── __init__.py # activate() / deactivate() publik
|
|
117
|
+
│ ├── hook.py # sys.excepthook, mode in-process
|
|
118
|
+
│ ├── cli.py # `traceback_id run <file>`
|
|
119
|
+
│ ├── core/ # mesin inti — language-agnostic
|
|
120
|
+
│ │ ├── models.py # StructuredError, ErrorCategory
|
|
121
|
+
│ │ ├── explainer.py # cocokkan rule, render template
|
|
122
|
+
│ │ ├── formatter.py # gabung traceback asli + penjelasan
|
|
123
|
+
│ │ └── registry.py # daftar adapter (built-in + plugin)
|
|
124
|
+
│ ├── adapters/
|
|
125
|
+
│ │ ├── base.py # LanguageAdapter (kontrak)
|
|
126
|
+
│ │ └── python/
|
|
127
|
+
│ │ ├── adapter.py # capture (hook + subprocess), parser
|
|
128
|
+
│ │ └── rules.yaml # 15 error type Python v0.1
|
|
129
|
+
│ └── rules/
|
|
130
|
+
│ └── universal_categories.yaml # fallback lintas bahasa
|
|
131
|
+
├── tests/
|
|
132
|
+
│ ├── test_core/
|
|
133
|
+
│ ├── test_adapters/
|
|
134
|
+
│ └── test_contract.py # semua adapter wajib lolos test ini
|
|
135
|
+
├── examples/
|
|
136
|
+
│ └── demo_errors.py
|
|
137
|
+
├── pyproject.toml
|
|
138
|
+
└── README.md
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Menambah bahasa baru = buat folder `traceback_id/adapters/<bahasa>/` berisi
|
|
142
|
+
`adapter.py` (implementasi `LanguageAdapter`) + `rules.yaml` — `core/` tidak
|
|
143
|
+
perlu disentuh sama sekali (lihat `ARCHITECTURE.md` §7).
|
|
144
|
+
|
|
145
|
+
## Menambah Rule Baru (Python)
|
|
146
|
+
|
|
147
|
+
Rule ada di `traceback_id/adapters/python/rules.yaml`, key-nya = nama
|
|
148
|
+
error native (mis. `NameError`). Tidak perlu ubah kode inti:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
NamaErrorBaru:
|
|
152
|
+
category: KATEGORI_UNIVERSAL_YANG_SESUAI # lihat ErrorCategory di core/models.py
|
|
153
|
+
pattern: "regex opsional dengan (?P<nama_variabel>...)"
|
|
154
|
+
penjelasan: >
|
|
155
|
+
Penjelasan kenapa error ini terjadi. Bisa pakai {nama_variabel} yang
|
|
156
|
+
diambil dari `pattern` di atas.
|
|
157
|
+
saran: >
|
|
158
|
+
Saran konkret cara memperbaikinya.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Kontribusi Adapter Bahasa Baru (fase komunitas)
|
|
162
|
+
|
|
163
|
+
Setelah kontrak `LanguageAdapter` dibuka untuk plugin (roadmap Fase 3 —
|
|
164
|
+
lihat `PRD.md` §10), adapter bahasa baru bisa didistribusikan sebagai
|
|
165
|
+
package Python terpisah dan otomatis terdeteksi lewat `entry_points`:
|
|
166
|
+
|
|
167
|
+
```toml
|
|
168
|
+
# pyproject.toml package plugin kamu, mis. traceback-id-javascript
|
|
169
|
+
[project.entry-points."traceback_id.adapters"]
|
|
170
|
+
javascript = "traceback_id_javascript.adapter:JavaScriptAdapter"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Semua adapter — built-in maupun plugin — wajib lolos `tests/test_contract.py`.
|
|
174
|
+
|
|
175
|
+
## Menjalankan Test
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pip install -e ".[dev]"
|
|
179
|
+
pytest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Status & Roadmap
|
|
183
|
+
|
|
184
|
+
Draft v0.1 (MVP): arsitektur core + adapter diterapkan, adapter Python
|
|
185
|
+
matang (15 jenis error, mode in-process & CLI). Roadmap lengkap
|
|
186
|
+
(JavaScript di v0.3, Java + `entry_points` publik di v0.4, dst) ada di
|
|
187
|
+
`PRD.md` §10 dan `ARCHITECTURE.md` §10.
|
|
188
|
+
|
|
189
|
+
## Lisensi
|
|
190
|
+
|
|
191
|
+
MIT — lihat [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
traceback_id/__init__.py,sha256=22TRl9vvLO576GNgjsxdAl-5GahwcAdGV-5SzdqhQEY,555
|
|
2
|
+
traceback_id/__main__.py,sha256=qkzyvM0j9bKmEouFAkHPVuhvWr92D4mWPxGqGYl5r1A,131
|
|
3
|
+
traceback_id/cli.py,sha256=E_Vo0oT-NMi92dYcEINcy4h6SpOsAenNzC4aWR2X-xk,2594
|
|
4
|
+
traceback_id/hook.py,sha256=KB-uOM8DY5V5FKdy-a3FbEneiJfEW69qqL64dTN84-w,1909
|
|
5
|
+
traceback_id/adapters/__init__.py,sha256=w__gKur4jxqGACU_le9uDrmZO5IBZaKQeCtkaz4MyS4,69
|
|
6
|
+
traceback_id/adapters/base.py,sha256=DmKesegCNvEYK5D_JncU_5XfZ6ha4g32v2wDhOfNsL0,1138
|
|
7
|
+
traceback_id/adapters/python/__init__.py,sha256=yLFuuLlP94WCw8dw3H8p7TBLKaJpi-jBKxf4j91MePo,82
|
|
8
|
+
traceback_id/adapters/python/adapter.py,sha256=4fbm19ZEjxCea1ZkHSUcHqWOCll3FlYtqkQ8B0fw90A,5085
|
|
9
|
+
traceback_id/adapters/python/rules.yaml,sha256=162lhcpBsvOQdmeveNyKRATReuMa29AH0Qz_yvpzmL4,6990
|
|
10
|
+
traceback_id/core/__init__.py,sha256=sIYlxO174AEpdCu6OsWFUNl4tHtUqBatCXwFW5J4btw,75
|
|
11
|
+
traceback_id/core/explainer.py,sha256=fUBx7_ZPnlUgXiNosBj_MR0i6PmVrevDlG7BwUrA0lw,3896
|
|
12
|
+
traceback_id/core/formatter.py,sha256=0EZsZ584bDy1V0PfoDeOLlYv9CpUe8Ua3y--pVGMvSU,1755
|
|
13
|
+
traceback_id/core/models.py,sha256=5ZwaxPzbg-RCQWG4SlfhRBsXEYTV_LGteTWWLqX5uuc,1899
|
|
14
|
+
traceback_id/core/registry.py,sha256=iPnPQSO4y-Q2cds-iRgj86pPMTqc2zE4sp8IQTWEXOA,2907
|
|
15
|
+
traceback_id/rules/universal_categories.yaml,sha256=PLlSiD7lcP5RneE-l1cVl7OXXqFiJ9P6-HGi-dbuRUY,3580
|
|
16
|
+
traceback_id-0.1.0.dist-info/licenses/LICENSE,sha256=s-eFVE2OqauKQO6tSOc7MbhKc5E3LDvLb2GJ1A5ARVs,1082
|
|
17
|
+
traceback_id-0.1.0.dist-info/METADATA,sha256=SpxdMFgtT1FfJsxYLNL1FZFeZIiAza6gYrPE8mvqqqQ,6828
|
|
18
|
+
traceback_id-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
19
|
+
traceback_id-0.1.0.dist-info/entry_points.txt,sha256=SYVIIM8s6dQvQjwYnyWuSO8F5lvJrnfBZSf9hL1F6hc,55
|
|
20
|
+
traceback_id-0.1.0.dist-info/top_level.txt,sha256=VkG5K7QhrUMJaQO5iZOOOA6Uv1gE_mA7b2gZ9KSFS70,13
|
|
21
|
+
traceback_id-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 traceback_id contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
traceback_id
|