docs for muutils v0.8.1
View Source on GitHub

muutils.errormode

provides ErrorMode enum for handling errors consistently

pass an error_mode: ErrorMode to a function to specify how to handle a certain kind of exception. That function then instead of raiseing or warnings.warning, calls error_mode.process with the message and the exception.

you can also specify the exception class to raise, the warning class to use, and the source of the exception/warning.


  1"""provides `ErrorMode` enum for handling errors consistently
  2
  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.
  5
  6you can also specify the exception class to raise, the warning class to use, and the source of the exception/warning.
  7
  8"""
  9
 10from __future__ import annotations
 11
 12import sys
 13import typing
 14import types
 15import warnings
 16from enum import Enum
 17
 18
 19class WarningFunc(typing.Protocol):
 20    def __call__(
 21        self,
 22        msg: str,
 23        category: typing.Type[Warning],
 24        source: typing.Any = None,
 25    ) -> None: ...
 26
 27
 28LoggingFunc = typing.Callable[[str], None]
 29
 30GLOBAL_WARN_FUNC: WarningFunc = warnings.warn  # type: ignore[assignment]
 31GLOBAL_LOG_FUNC: LoggingFunc = print
 32
 33
 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)
 56
 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    # )
 76
 77
 78class ErrorMode(Enum):
 79    """Enum for handling errors consistently
 80
 81    pass one of the instances of this enum to a function to specify how to handle a certain kind of exception.
 82
 83    That function then instead of `raise`ing or `warnings.warn`ing, calls `error_mode.process` with the message and the exception.
 84    """
 85
 86    EXCEPT = "except"
 87    WARN = "warn"
 88    LOG = "log"
 89    IGNORE = "ignore"
 90
 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
101
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`)
120
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            )
132
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}")
161
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()
175
176            # remove prefix
177            if allow_prefix and mode.startswith("ErrorMode."):
178                mode = mode[len("ErrorMode.") :]
179
180            # lowercase and strip again
181            mode = mode.strip().lower()
182
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            )
196
197    def __str__(self) -> str:
198        return f"ErrorMode.{self.value.capitalize()}"
199
200    def __repr__(self) -> str:
201        return str(self)
202
203    def serialize(self) -> str:
204        return str(self)
205
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        )
213
214
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"

class WarningFunc(typing.Protocol):
20class WarningFunc(typing.Protocol):
21    def __call__(
22        self,
23        msg: str,
24        category: typing.Type[Warning],
25        source: typing.Any = None,
26    ) -> None: ...

Base class for protocol classes.

Protocol classes are defined as::

class Proto(Protocol):
    def meth(self) -> int:
        ...

Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).

For example::

class C:
    def meth(self) -> int:
        return 0

def func(x: Proto) -> int:
    return x.meth()

func(C())  # Passes static type check

See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::

class GenProto[T](Protocol):
    def meth(self) -> T:
        ...
WarningFunc(*args, **kwargs)
1710def _no_init_or_replace_init(self, *args, **kwargs):
1711    cls = type(self)
1712
1713    if cls._is_protocol:
1714        raise TypeError('Protocols cannot be instantiated')
1715
1716    # Already using a custom `__init__`. No need to calculate correct
1717    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1718    if cls.__init__ is not _no_init_or_replace_init:
1719        return
1720
1721    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1722    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1723    # searches for a proper new `__init__` in the MRO. The new `__init__`
1724    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1725    # instantiation of the protocol subclass will thus use the new
1726    # `__init__` and no longer call `_no_init_or_replace_init`.
1727    for base in cls.__mro__:
1728        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1729        if init is not _no_init_or_replace_init:
1730            cls.__init__ = init
1731            break
1732    else:
1733        # should not happen
1734        cls.__init__ = object.__init__
1735
1736    cls.__init__(self, *args, **kwargs)
LoggingFunc = typing.Callable[[str], NoneType]
def GLOBAL_WARN_FUNC(unknown):

Issue a warning, or maybe ignore it or raise an exception.

message Text of the warning message. category The Warning category subclass. Defaults to UserWarning. stacklevel How far up the call stack to make this warning appear. A value of 2 for example attributes the warning to the caller of the code calling warn(). source If supplied, the destroyed object which emitted a ResourceWarning skip_file_prefixes An optional tuple of module filename prefixes indicating frames to skip during stacklevel computations for stack frame attribution.

def GLOBAL_LOG_FUNC(*args, sep=' ', end='\n', file=None, flush=False):

Prints the values to a stream, or to sys.stdout by default.

sep string inserted between values, default a space. end string appended after the last value, default a newline. file a file-like object (stream); defaults to the current sys.stdout. flush whether to forcibly flush the stream.

def custom_showwarning( message: Warning | str, category: Optional[Type[Warning]] = None, filename: str | None = None, lineno: int | None = None, file: Optional[TextIO] = None, line: Optional[str] = None) -> None:
35def custom_showwarning(
36    message: Warning | str,
37    category: typing.Type[Warning] | None = None,
38    filename: str | None = None,
39    lineno: int | None = None,
40    file: typing.Optional[typing.TextIO] = None,
41    line: typing.Optional[str] = None,
42) -> None:
43    if category is None:
44        category = UserWarning
45    # Get the frame where process() was called
46    # Adjusted to account for the extra function call
47    frame: types.FrameType = sys._getframe(2)
48    # get globals and traceback
49    traceback: types.TracebackType = types.TracebackType(
50        None, frame, frame.f_lasti, frame.f_lineno
51    )
52    _globals: dict[str, typing.Any] = frame.f_globals
53    # init the new warning and add the traceback
54    if isinstance(message, str):
55        message = category(message)
56    message = message.with_traceback(traceback)
57
58    # Call the original showwarning function
59    warnings.warn_explicit(
60        message=message,
61        category=category,
62        # filename arg if it's passed, otherwise use the frame's filename
63        filename=frame.f_code.co_filename,
64        lineno=frame.f_lineno,
65        module=frame.f_globals.get("__name__", "__main__"),
66        registry=_globals.setdefault("__warningregistry__", {}),
67        module_globals=_globals,
68    )
69    # warnings._showwarning_orig(
70    #     message,
71    #     category,
72    #     frame.f_code.co_filename,
73    #     frame.f_lineno,
74    #     file,
75    #     line,
76    # )
class ErrorMode(enum.Enum):
 79class ErrorMode(Enum):
 80    """Enum for handling errors consistently
 81
 82    pass one of the instances of this enum to a function to specify how to handle a certain kind of exception.
 83
 84    That function then instead of `raise`ing or `warnings.warn`ing, calls `error_mode.process` with the message and the exception.
 85    """
 86
 87    EXCEPT = "except"
 88    WARN = "warn"
 89    LOG = "log"
 90    IGNORE = "ignore"
 91
 92    def process(
 93        self,
 94        msg: str,
 95        except_cls: typing.Type[Exception] = ValueError,
 96        warn_cls: typing.Type[Warning] = UserWarning,
 97        except_from: typing.Optional[Exception] = None,
 98        warn_func: WarningFunc | None = None,
 99        log_func: LoggingFunc | None = None,
100    ):
101        """process an exception or warning according to the error mode
102
103        # Parameters:
104         - `msg : str`
105           message to pass to `except_cls` or `warn_func`
106         - `except_cls : typing.Type[Exception]`
107            exception class to raise, must be a subclass of `Exception`
108           (defaults to `ValueError`)
109         - `warn_cls : typing.Type[Warning]`
110            warning class to use, must be a subclass of `Warning`
111           (defaults to `UserWarning`)
112         - `except_from : typing.Optional[Exception]`
113            will `raise except_cls(msg) from except_from` if not `None`
114           (defaults to `None`)
115         - `warn_func : WarningFunc | None`
116            function to use for warnings, must have the signature `warn_func(msg: str, category: typing.Type[Warning], source: typing.Any = None) -> None`
117           (defaults to `None`)
118         - `log_func : LoggingFunc | None`
119            function to use for logging, must have the signature `log_func(msg: str) -> None`
120           (defaults to `None`)
121
122        # Raises:
123         - `except_cls` : _description_
124         - `except_cls` : _description_
125         - `ValueError` : _description_
126        """
127        if self is ErrorMode.EXCEPT:
128            # except, possibly with a chained exception
129            frame: types.FrameType = sys._getframe(1)
130            traceback: types.TracebackType = types.TracebackType(
131                None, frame, frame.f_lasti, frame.f_lineno
132            )
133
134            # Attach the new traceback to the exception and raise it without the internal call stack
135            if except_from is not None:
136                raise except_cls(msg).with_traceback(traceback) from except_from
137            else:
138                raise except_cls(msg).with_traceback(traceback)
139        elif self is ErrorMode.WARN:
140            # get global warn function if not passed
141            if warn_func is None:
142                warn_func = GLOBAL_WARN_FUNC
143            # augment warning message with source
144            if except_from is not None:
145                msg = f"{msg}\n\tSource of warning: {except_from}"
146            if warn_func == warnings.warn:
147                custom_showwarning(msg, category=warn_cls)
148            else:
149                # Use the provided warn_func as-is
150                warn_func(msg, category=warn_cls)
151        elif self is ErrorMode.LOG:
152            # get global log function if not passed
153            if log_func is None:
154                log_func = GLOBAL_LOG_FUNC
155            # log
156            log_func(msg)
157        elif self is ErrorMode.IGNORE:
158            # do nothing
159            pass
160        else:
161            raise ValueError(f"Unknown error mode {self}")
162
163    @classmethod
164    def from_any(
165        cls,
166        mode: "str|ErrorMode",
167        allow_aliases: bool = True,
168        allow_prefix: bool = True,
169    ) -> ErrorMode:
170        """initialize an `ErrorMode` from a string or an `ErrorMode` instance"""
171        if isinstance(mode, ErrorMode):
172            return mode
173        elif isinstance(mode, str):
174            # strip
175            mode = mode.strip()
176
177            # remove prefix
178            if allow_prefix and mode.startswith("ErrorMode."):
179                mode = mode[len("ErrorMode.") :]
180
181            # lowercase and strip again
182            mode = mode.strip().lower()
183
184            if not allow_aliases:
185                # try without aliases
186                try:
187                    return ErrorMode(mode)
188                except ValueError as e:
189                    raise KeyError(f"Unknown error mode {mode = }") from e
190            else:
191                # look up in aliases map
192                return ERROR_MODE_ALIASES[mode]
193        else:
194            raise TypeError(
195                f"Expected {ErrorMode = } or str, got {type(mode) = } {mode = }"
196            )
197
198    def __str__(self) -> str:
199        return f"ErrorMode.{self.value.capitalize()}"
200
201    def __repr__(self) -> str:
202        return str(self)
203
204    def serialize(self) -> str:
205        return str(self)
206
207    @classmethod
208    def load(cls, data: str) -> ErrorMode:
209        return cls.from_any(
210            data,
211            allow_aliases=False,
212            allow_prefix=True,
213        )

Enum for handling errors consistently

pass one of the instances of this enum to a function to specify how to handle a certain kind of exception.

That function then instead of raiseing or warnings.warning, calls error_mode.process with the message and the exception.

EXCEPT = ErrorMode.Except
WARN = ErrorMode.Warn
LOG = ErrorMode.Log
IGNORE = ErrorMode.Ignore
def process( self, msg: str, except_cls: Type[Exception] = <class 'ValueError'>, warn_cls: Type[Warning] = <class 'UserWarning'>, except_from: Optional[Exception] = None, warn_func: WarningFunc | None = None, log_func: Optional[Callable[[str], NoneType]] = None):
 92    def process(
 93        self,
 94        msg: str,
 95        except_cls: typing.Type[Exception] = ValueError,
 96        warn_cls: typing.Type[Warning] = UserWarning,
 97        except_from: typing.Optional[Exception] = None,
 98        warn_func: WarningFunc | None = None,
 99        log_func: LoggingFunc | None = None,
100    ):
101        """process an exception or warning according to the error mode
102
103        # Parameters:
104         - `msg : str`
105           message to pass to `except_cls` or `warn_func`
106         - `except_cls : typing.Type[Exception]`
107            exception class to raise, must be a subclass of `Exception`
108           (defaults to `ValueError`)
109         - `warn_cls : typing.Type[Warning]`
110            warning class to use, must be a subclass of `Warning`
111           (defaults to `UserWarning`)
112         - `except_from : typing.Optional[Exception]`
113            will `raise except_cls(msg) from except_from` if not `None`
114           (defaults to `None`)
115         - `warn_func : WarningFunc | None`
116            function to use for warnings, must have the signature `warn_func(msg: str, category: typing.Type[Warning], source: typing.Any = None) -> None`
117           (defaults to `None`)
118         - `log_func : LoggingFunc | None`
119            function to use for logging, must have the signature `log_func(msg: str) -> None`
120           (defaults to `None`)
121
122        # Raises:
123         - `except_cls` : _description_
124         - `except_cls` : _description_
125         - `ValueError` : _description_
126        """
127        if self is ErrorMode.EXCEPT:
128            # except, possibly with a chained exception
129            frame: types.FrameType = sys._getframe(1)
130            traceback: types.TracebackType = types.TracebackType(
131                None, frame, frame.f_lasti, frame.f_lineno
132            )
133
134            # Attach the new traceback to the exception and raise it without the internal call stack
135            if except_from is not None:
136                raise except_cls(msg).with_traceback(traceback) from except_from
137            else:
138                raise except_cls(msg).with_traceback(traceback)
139        elif self is ErrorMode.WARN:
140            # get global warn function if not passed
141            if warn_func is None:
142                warn_func = GLOBAL_WARN_FUNC
143            # augment warning message with source
144            if except_from is not None:
145                msg = f"{msg}\n\tSource of warning: {except_from}"
146            if warn_func == warnings.warn:
147                custom_showwarning(msg, category=warn_cls)
148            else:
149                # Use the provided warn_func as-is
150                warn_func(msg, category=warn_cls)
151        elif self is ErrorMode.LOG:
152            # get global log function if not passed
153            if log_func is None:
154                log_func = GLOBAL_LOG_FUNC
155            # log
156            log_func(msg)
157        elif self is ErrorMode.IGNORE:
158            # do nothing
159            pass
160        else:
161            raise ValueError(f"Unknown error mode {self}")

process an exception or warning according to the error mode

Parameters:

  • msg : str message to pass to except_cls or warn_func
  • except_cls : typing.Type[Exception] exception class to raise, must be a subclass of Exception (defaults to ValueError)
  • warn_cls : typing.Type[Warning] warning class to use, must be a subclass of Warning (defaults to UserWarning)
  • except_from : typing.Optional[Exception] will raise except_cls(msg) from except_from if not None (defaults to None)
  • warn_func : WarningFunc | None function to use for warnings, must have the signature warn_func(msg: str, category: typing.Type[Warning], source: typing.Any = None) -> None (defaults to None)
  • log_func : LoggingFunc | None function to use for logging, must have the signature log_func(msg: str) -> None (defaults to None)

Raises:

  • except_cls : _description_
  • except_cls : _description_
  • ValueError : _description_
@classmethod
def from_any( cls, mode: str | ErrorMode, allow_aliases: bool = True, allow_prefix: bool = True) -> ErrorMode:
163    @classmethod
164    def from_any(
165        cls,
166        mode: "str|ErrorMode",
167        allow_aliases: bool = True,
168        allow_prefix: bool = True,
169    ) -> ErrorMode:
170        """initialize an `ErrorMode` from a string or an `ErrorMode` instance"""
171        if isinstance(mode, ErrorMode):
172            return mode
173        elif isinstance(mode, str):
174            # strip
175            mode = mode.strip()
176
177            # remove prefix
178            if allow_prefix and mode.startswith("ErrorMode."):
179                mode = mode[len("ErrorMode.") :]
180
181            # lowercase and strip again
182            mode = mode.strip().lower()
183
184            if not allow_aliases:
185                # try without aliases
186                try:
187                    return ErrorMode(mode)
188                except ValueError as e:
189                    raise KeyError(f"Unknown error mode {mode = }") from e
190            else:
191                # look up in aliases map
192                return ERROR_MODE_ALIASES[mode]
193        else:
194            raise TypeError(
195                f"Expected {ErrorMode = } or str, got {type(mode) = } {mode = }"
196            )

initialize an ErrorMode from a string or an ErrorMode instance

def serialize(self) -> str:
204    def serialize(self) -> str:
205        return str(self)
@classmethod
def load(cls, data: str) -> ErrorMode:
207    @classmethod
208    def load(cls, data: str) -> ErrorMode:
209        return cls.from_any(
210            data,
211            allow_aliases=False,
212            allow_prefix=True,
213        )
Inherited Members
enum.Enum
name
value
ERROR_MODE_ALIASES: dict[str, ErrorMode] = {'except': ErrorMode.Except, 'warn': ErrorMode.Warn, 'log': ErrorMode.Log, 'ignore': ErrorMode.Ignore, 'e': ErrorMode.Except, 'error': ErrorMode.Except, 'err': ErrorMode.Except, 'raise': ErrorMode.Except, 'w': ErrorMode.Warn, 'warning': ErrorMode.Warn, 'l': ErrorMode.Log, 'print': ErrorMode.Log, 'output': ErrorMode.Log, 'show': ErrorMode.Log, 'display': ErrorMode.Log, 'i': ErrorMode.Ignore, 'silent': ErrorMode.Ignore, 'quiet': ErrorMode.Ignore, 'nothing': ErrorMode.Ignore}

map of string aliases to ErrorMode instances