Coverage for src/zapy/templating/traceback.py: 86%
54 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-10 19:35 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-10 19:35 +0000
1import sys
2import textwrap
3import traceback
5from typing_extensions import TypedDict
7from zapy.utils.singleton import SingletonMeta
9ANNOTATION_FIELD = "_parse_errors"
12class TracebackInfo(TypedDict):
13 exception_type: str
14 exception_message: str
15 line: int | None
16 stacktrace: str
19def annotate_traceback(exc_obj: BaseException, script: str = "", location: str = "") -> TracebackInfo:
20 traceback_info = TracebackHandler().extract(script, location)
21 setattr(exc_obj, ANNOTATION_FIELD, traceback_info)
22 exc_obj.add_note(traceback_info["stacktrace"])
23 return traceback_info
26def recover_traceback(exc_obj: BaseException) -> TracebackInfo | None:
27 return getattr(exc_obj, ANNOTATION_FIELD, None)
30def copy_traceback(exc_obj: BaseException, from_exc: BaseException) -> TracebackInfo | None:
31 info = recover_traceback(from_exc)
32 if info:
33 setattr(exc_obj, ANNOTATION_FIELD, info)
34 exc_obj.add_note(info["stacktrace"])
35 return info
38class TracebackHandler(metaclass=SingletonMeta):
39 header = "Traceback (most recent call last):"
41 def extract(self, script: str = "", location: str = "") -> TracebackInfo:
42 _, exc_obj, exc_tb = sys.exc_info()
43 frame_summary = traceback.extract_tb(exc_tb)[-1]
44 if exc_obj is None: 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true
45 err_msg = "Exception is None"
46 raise ValueError(err_msg)
47 if frame_summary.filename == "<string>":
48 return self.extract_from_frame(frame_summary, exc_obj, script=script, location=location)
49 else:
50 return self.extract_from_error(exc_obj, location=location)
52 def extract_from_frame(
53 self, frame_summary: traceback.FrameSummary, exc_obj: BaseException, script: str = "", location: str = ""
54 ) -> TracebackInfo:
55 stacktrace = textwrap.dedent(
56 f"""\
57 {self.header}
58 {location}, line {frame_summary.lineno}, in {frame_summary.name}
59 {frame_summary.line or self.__extract_line(script, frame_summary.lineno)}
60 {exc_obj.__class__.__name__}: {exc_obj}"""
61 )
62 return TracebackInfo(
63 exception_type=exc_obj.__class__.__name__,
64 exception_message=str(exc_obj),
65 line=frame_summary.lineno,
66 stacktrace=stacktrace,
67 )
69 def extract_from_error(self, exc_obj: BaseException, location: str = "") -> TracebackInfo:
70 tracelines = traceback.format_exception_only(exc_obj)
71 tracelines[0] = tracelines[0].replace('File "<string>"', location)
72 tracelines.insert(0, self.header + "\n")
73 return TracebackInfo(
74 exception_type=exc_obj.__class__.__name__,
75 exception_message=str(exc_obj),
76 line=exc_obj.lineno if isinstance(exc_obj, SyntaxError) else None,
77 stacktrace="".join(tracelines),
78 )
80 def __extract_line(self, script: str, line: int | None) -> str:
81 if not script: 81 ↛ 82line 81 didn't jump to line 82, because the condition on line 81 was never true
82 return ""
83 err_msg = "<<Zapy: line omitted due to an error on extraction, check your line separation>>"
84 if line is None: 84 ↛ 85line 84 didn't jump to line 85, because the condition on line 84 was never true
85 return err_msg
86 try:
87 script_lines = script.split("\n")
88 return script_lines[line - 1].strip()
89 except Exception:
90 return err_msg