Coverage for muutils/errormode.py: 59%
75 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-04 03:33 -0600
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-04 03:33 -0600
1"""provides `ErrorMode` enum for handling errors consistently
3pass an `error_mode: ErrorMode` to a function to specify how to handle a certain kind of exception.
4That function then instead of `raise`ing or `warnings.warn`ing, calls `error_mode.process` with the message and the exception.
6you can also specify the exception class to raise, the warning class to use, and the source of the exception/warning.
8"""
10from __future__ import annotations
12import sys
13import typing
14import types
15import warnings
16from enum import Enum
19class WarningFunc(typing.Protocol):
20 def __call__(
21 self,
22 msg: str,
23 category: typing.Type[Warning],
24 source: typing.Any = None,
25 ) -> None: ...
28LoggingFunc = typing.Callable[[str], None]
30GLOBAL_WARN_FUNC: WarningFunc = warnings.warn # type: ignore[assignment]
31GLOBAL_LOG_FUNC: LoggingFunc = print
34def custom_showwarning(
35 message: Warning | str,
36 category: typing.Type[Warning] | None = None,
37 filename: str | None = None,
38 lineno: int | None = None,
39 file: typing.Optional[typing.TextIO] = None,
40 line: typing.Optional[str] = None,
41) -> None:
42 if category is None:
43 category = UserWarning
44 # Get the frame where process() was called
45 # Adjusted to account for the extra function call
46 frame: types.FrameType = sys._getframe(2)
47 # get globals and traceback
48 traceback: types.TracebackType = types.TracebackType(
49 None, frame, frame.f_lasti, frame.f_lineno
50 )
51 _globals: dict[str, typing.Any] = frame.f_globals
52 # init the new warning and add the traceback
53 if isinstance(message, str):
54 message = category(message)
55 message = message.with_traceback(traceback)
57 # Call the original showwarning function
58 warnings.warn_explicit(
59 message=message,
60 category=category,
61 # filename arg if it's passed, otherwise use the frame's filename
62 filename=frame.f_code.co_filename,
63 lineno=frame.f_lineno,
64 module=frame.f_globals.get("__name__", "__main__"),
65 registry=_globals.setdefault("__warningregistry__", {}),
66 module_globals=_globals,
67 )
68 # warnings._showwarning_orig(
69 # message,
70 # category,
71 # frame.f_code.co_filename,
72 # frame.f_lineno,
73 # file,
74 # line,
75 # )
78class ErrorMode(Enum):
79 """Enum for handling errors consistently
81 pass one of the instances of this enum to a function to specify how to handle a certain kind of exception.
83 That function then instead of `raise`ing or `warnings.warn`ing, calls `error_mode.process` with the message and the exception.
84 """
86 EXCEPT = "except"
87 WARN = "warn"
88 LOG = "log"
89 IGNORE = "ignore"
91 def process(
92 self,
93 msg: str,
94 except_cls: typing.Type[Exception] = ValueError,
95 warn_cls: typing.Type[Warning] = UserWarning,
96 except_from: typing.Optional[Exception] = None,
97 warn_func: WarningFunc | None = None,
98 log_func: LoggingFunc | None = None,
99 ):
100 """process an exception or warning according to the error mode
102 # Parameters:
103 - `msg : str`
104 message to pass to `except_cls` or `warn_func`
105 - `except_cls : typing.Type[Exception]`
106 exception class to raise, must be a subclass of `Exception`
107 (defaults to `ValueError`)
108 - `warn_cls : typing.Type[Warning]`
109 warning class to use, must be a subclass of `Warning`
110 (defaults to `UserWarning`)
111 - `except_from : typing.Optional[Exception]`
112 will `raise except_cls(msg) from except_from` if not `None`
113 (defaults to `None`)
114 - `warn_func : WarningFunc | None`
115 function to use for warnings, must have the signature `warn_func(msg: str, category: typing.Type[Warning], source: typing.Any = None) -> None`
116 (defaults to `None`)
117 - `log_func : LoggingFunc | None`
118 function to use for logging, must have the signature `log_func(msg: str) -> None`
119 (defaults to `None`)
121 # Raises:
122 - `except_cls` : _description_
123 - `except_cls` : _description_
124 - `ValueError` : _description_
125 """
126 if self is ErrorMode.EXCEPT:
127 # except, possibly with a chained exception
128 frame: types.FrameType = sys._getframe(1)
129 traceback: types.TracebackType = types.TracebackType(
130 None, frame, frame.f_lasti, frame.f_lineno
131 )
133 # Attach the new traceback to the exception and raise it without the internal call stack
134 if except_from is not None:
135 raise except_cls(msg).with_traceback(traceback) from except_from
136 else:
137 raise except_cls(msg).with_traceback(traceback)
138 elif self is ErrorMode.WARN:
139 # get global warn function if not passed
140 if warn_func is None:
141 warn_func = GLOBAL_WARN_FUNC
142 # augment warning message with source
143 if except_from is not None:
144 msg = f"{msg}\n\tSource of warning: {except_from}"
145 if warn_func == warnings.warn:
146 custom_showwarning(msg, category=warn_cls)
147 else:
148 # Use the provided warn_func as-is
149 warn_func(msg, category=warn_cls)
150 elif self is ErrorMode.LOG:
151 # get global log function if not passed
152 if log_func is None:
153 log_func = GLOBAL_LOG_FUNC
154 # log
155 log_func(msg)
156 elif self is ErrorMode.IGNORE:
157 # do nothing
158 pass
159 else:
160 raise ValueError(f"Unknown error mode {self}")
162 @classmethod
163 def from_any(
164 cls,
165 mode: "str|ErrorMode",
166 allow_aliases: bool = True,
167 allow_prefix: bool = True,
168 ) -> ErrorMode:
169 """initialize an `ErrorMode` from a string or an `ErrorMode` instance"""
170 if isinstance(mode, ErrorMode):
171 return mode
172 elif isinstance(mode, str):
173 # strip
174 mode = mode.strip()
176 # remove prefix
177 if allow_prefix and mode.startswith("ErrorMode."):
178 mode = mode[len("ErrorMode.") :]
180 # lowercase and strip again
181 mode = mode.strip().lower()
183 if not allow_aliases:
184 # try without aliases
185 try:
186 return ErrorMode(mode)
187 except ValueError as e:
188 raise KeyError(f"Unknown error mode {mode = }") from e
189 else:
190 # look up in aliases map
191 return ERROR_MODE_ALIASES[mode]
192 else:
193 raise TypeError(
194 f"Expected {ErrorMode = } or str, got {type(mode) = } {mode = }"
195 )
197 def __str__(self) -> str:
198 return f"ErrorMode.{self.value.capitalize()}"
200 def __repr__(self) -> str:
201 return str(self)
203 def serialize(self) -> str:
204 return str(self)
206 @classmethod
207 def load(cls, data: str) -> ErrorMode:
208 return cls.from_any(
209 data,
210 allow_aliases=False,
211 allow_prefix=True,
212 )
215ERROR_MODE_ALIASES: dict[str, ErrorMode] = {
216 # base
217 "except": ErrorMode.EXCEPT,
218 "warn": ErrorMode.WARN,
219 "log": ErrorMode.LOG,
220 "ignore": ErrorMode.IGNORE,
221 # except
222 "e": ErrorMode.EXCEPT,
223 "error": ErrorMode.EXCEPT,
224 "err": ErrorMode.EXCEPT,
225 "raise": ErrorMode.EXCEPT,
226 # warn
227 "w": ErrorMode.WARN,
228 "warning": ErrorMode.WARN,
229 # log
230 "l": ErrorMode.LOG,
231 "print": ErrorMode.LOG,
232 "output": ErrorMode.LOG,
233 "show": ErrorMode.LOG,
234 "display": ErrorMode.LOG,
235 # ignore
236 "i": ErrorMode.IGNORE,
237 "silent": ErrorMode.IGNORE,
238 "quiet": ErrorMode.IGNORE,
239 "nothing": ErrorMode.IGNORE,
240}
241"map of string aliases to `ErrorMode` instances"