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

1import sys 

2import textwrap 

3import traceback 

4 

5from typing_extensions import TypedDict 

6 

7from zapy.utils.singleton import SingletonMeta 

8 

9ANNOTATION_FIELD = "_parse_errors" 

10 

11 

12class TracebackInfo(TypedDict): 

13 exception_type: str 

14 exception_message: str 

15 line: int | None 

16 stacktrace: str 

17 

18 

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 

24 

25 

26def recover_traceback(exc_obj: BaseException) -> TracebackInfo | None: 

27 return getattr(exc_obj, ANNOTATION_FIELD, None) 

28 

29 

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 

36 

37 

38class TracebackHandler(metaclass=SingletonMeta): 

39 header = "Traceback (most recent call last):" 

40 

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) 

51 

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 ) 

68 

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 ) 

79 

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