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
« 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#
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
24__all__ = ["StackUtil"]
27@dataclass
28class CallerInfo:
29 filename: str
30 function: str
31 lineno: int
33 @override
34 def __str__(self) -> str:
35 return f"{self.filename}:{self.function}::{self.lineno}"
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 ]
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 )
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
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]"
87 fallback_lines.append(truncated_line)
89 if skip_next:
90 skip_next = False
91 continue
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
97 filtered_lines.append(truncated_line)
99 if len(filtered_lines) == 0:
100 return "\n".join(fallback_lines) # fallback to original if we end up with nothing
102 return "\n".join(filtered_lines)
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}"
110 @staticmethod
111 def get_caller(start_depth: int = 2) -> CallerInfo:
112 stack = inspect.stack()
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)
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)
122 return CallerInfo("unknown", "unknown", 0)
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)
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)
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}"
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]
151 # Replace with unix style separators
152 return filename.replace("\\", "/")