Coverage for functions \ flipdare \ error \ stack_util.py: 100%

0 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

3# 

4# This file is part of Flipdare's proprietary software and contains 

5# confidential and copyrighted material. Unauthorised copying, 

6# modification, distribution, or use of this file is strictly 

7# prohibited without prior written permission from Flipdare Pty Ltd. 

8# 

9# This software includes third-party components licensed under MIT, 

10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

11# 

12 

13 

14import inspect 

15import os 

16import sys 

17from dataclasses import dataclass 

18from traceback import FrameSummary 

19from typing import Final, override 

20import stackprinter 

21import re 

22from flipdare.constants import EMAIL_STACK_LINE_MAX_LENGTH 

23 

24__all__ = ["StackUtil"] 

25 

26 

27@dataclass 

28class CallerInfo: 

29 filename: str 

30 function: str 

31 lineno: int 

32 

33 @override 

34 def __str__(self) -> str: 

35 return f"{self.filename}:{self.function}::{self.lineno}" 

36 

37 

38class StackUtil: # pragma: no cover 

39 SUPPRESSED_PATHS: Final[list[str]] = [ 

40 r".*stack_util.*", 

41 r".*app_log_formatter.*", 

42 r".*app_log.*", 

43 r".*__init__.*", 

44 r".*__main__.*", 

45 r".*__call__.*", 

46 r".*pytest.*", 

47 r".*pluggy.*", 

48 r".*conftest.py.*", 

49 r".*<frozen.*", 

50 r".*runpy.*", # for testing 

51 ] 

52 

53 @staticmethod 

54 def get_flipdare_stack() -> str: 

55 # the custom code was replaced with stackprinter 

56 # because it formates the errors better, 

57 # unfortunately 

58 result: str = stackprinter.format( 

59 sys._getframe(2), # get the caller's caller frame 

60 reverse=True, 

61 suppressed_paths=StackUtil.SUPPRESSED_PATHS, 

62 source_lines=3, 

63 line_wrap=120, 

64 show_vals="line", 

65 truncate_vals=120, 

66 ) 

67 

68 lines = result.splitlines() 

69 # suppressed lines are of the format, 

70 # 

71 # File "C:\Users\dave\AppData\Local\Programs\Python\Python313\Lib\site-packages\_pytest\main.py", line 396, in pytest_runtestloop 

72 # item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) 

73 # 

74 # so we just iterate through each line 

75 # if we find a match exclude this line and the next line 

76 filtered_lines = [] 

77 fallback_lines = [] 

78 skip_next = False 

79 

80 for line in lines: 

81 truncated_line = line 

82 # truncate any lines > 500 chars for readabilty and to avoid 

83 # an email errors .. 

84 if len(truncated_line) > EMAIL_STACK_LINE_MAX_LENGTH: 

85 truncated_line = truncated_line[:EMAIL_STACK_LINE_MAX_LENGTH] + " ... [truncated]" 

86 

87 fallback_lines.append(truncated_line) 

88 

89 if skip_next: 

90 skip_next = False 

91 continue 

92 

93 if any(re.match(ignore_regex, line) for ignore_regex in StackUtil.SUPPRESSED_PATHS): 

94 skip_next = True # Skip the next line as well 

95 continue 

96 

97 filtered_lines.append(truncated_line) 

98 

99 if len(filtered_lines) == 0: 

100 return "\n".join(fallback_lines) # fallback to original if we end up with nothing 

101 

102 return "\n".join(filtered_lines) 

103 

104 @staticmethod 

105 def get_caller_str(start_depth: int = 2) -> str: 

106 """Get formatted string of the calling function (excludes this module and ignored files).""" 

107 caller = StackUtil.get_caller(start_depth) 

108 return f"{caller.filename}:{caller.function}::{caller.lineno}" 

109 

110 @staticmethod 

111 def get_caller(start_depth: int = 2) -> CallerInfo: 

112 stack = inspect.stack() 

113 

114 # Clamp start_depth to the maximum available index 

115 # (len - 1 is the oldest frame, usually the module/script level) 

116 actual_start = min(max(start_depth, 0), len(stack) - 1) 

117 

118 # Slice and iterate from your starting point 

119 for frame_info in stack[actual_start:]: 

120 return CallerInfo(frame_info.filename, frame_info.function, frame_info.lineno) 

121 

122 return CallerInfo("unknown", "unknown", 0) 

123 

124 @staticmethod 

125 def _parse_frame(frame: FrameSummary) -> CallerInfo: 

126 """Parse a frame into CallerInfo.""" 

127 filename = StackUtil._format_filename(frame.filename) 

128 lineno = frame.lineno or 0 

129 return CallerInfo(filename, frame.name, lineno) 

130 

131 @staticmethod 

132 def _should_ignore(filename: str) -> bool: 

133 """Check if a filename should be ignored.""" 

134 return any(re.match(ignore_regex, filename) for ignore_regex in StackUtil.SUPPRESSED_PATHS) 

135 

136 @staticmethod 

137 def _format_line(filename: str, function: str, lineno: int) -> str: 

138 """Format a line for display (backwards compatibility).""" 

139 filename = StackUtil._format_filename(filename) 

140 return f"{filename}:{function}:{lineno}" 

141 

142 @staticmethod 

143 def _format_filename(filename: str) -> str: 

144 """Format filename to be relative to firebase/flipdare/local-packages.""" 

145 sep = os.sep 

146 labels = [f"firebase{sep}", f"flipdare{sep}", f"local-packages{sep}"] 

147 for label in labels: 

148 if label in filename: 

149 filename = filename.rsplit(label, maxsplit=1)[-1] 

150 

151 # Replace with unix style separators 

152 return filename.replace("\\", "/")