errortools 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _errortools/__init__.py +1 -0
- _errortools/_metadata.py +9 -0
- _errortools/_types.py +21 -0
- _errortools/_version.py +7 -0
- _errortools/abc.py +333 -0
- _errortools/cached/__init__.py +1 -0
- _errortools/cached/cache.py +82 -0
- _errortools/cached/wrapper.py +76 -0
- _errortools/classes/__init__.py +1 -0
- _errortools/classes/errorcodes.py +148 -0
- _errortools/classes/warn.py +124 -0
- _errortools/groups.py +8 -0
- _errortools/ignore.py +90 -0
- _errortools/methods/__init__.py +11 -0
- _errortools/methods/errorattr.py +37 -0
- _errortools/methods/errordelattr.py +35 -0
- _errortools/methods/errorhasattr.py +27 -0
- _errortools/methods/errorsetattr.py +40 -0
- _errortools/py.typed +0 -0
- _errortools/raises.py +166 -0
- _errortools/tools/__init__.py +1 -0
- _errortools/tools/_warps.py +26 -0
- _errortools/tools/error_msg.py +15 -0
- errortools/__init__.py +104 -0
- errortools-1.0.0.dist-info/METADATA +63 -0
- errortools-1.0.0.dist-info/RECORD +41 -0
- errortools-1.0.0.dist-info/WHEEL +5 -0
- errortools-1.0.0.dist-info/licenses/LICENSE.txt +20 -0
- errortools-1.0.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +3 -0
- tests/conftest.py +14 -0
- tests/run_tests.py +6 -0
- tests/test_abc.py +249 -0
- tests/test_cache.py +230 -0
- tests/test_descriptor.py +66 -0
- tests/test_errorcodes.py +150 -0
- tests/test_groups.py +127 -0
- tests/test_ignore.py +124 -0
- tests/test_mixins.py +253 -0
- tests/test_raises.py +192 -0
- tests/test_warnings.py +151 -0
_errortools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Nothing here!
|
_errortools/_metadata.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
__author__: str = "Evan Yang"
|
|
2
|
+
__author_email__: str = "quantbit@126.com"
|
|
3
|
+
__copyright__: str = "Copyright 2026 (C) aiwonderland Evan Yang"
|
|
4
|
+
__description__: str = (
|
|
5
|
+
"errortools - a toolset for working with Python exceptions and warnings"
|
|
6
|
+
)
|
|
7
|
+
__license__: str = "MIT"
|
|
8
|
+
__title__: str = "errortools"
|
|
9
|
+
__url__: str = "https://github.com/more-abc/errortools"
|
_errortools/_types.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import types
|
|
3
|
+
|
|
4
|
+
# Acquire the internal interpreter types for traceback objects and frame objects.
|
|
5
|
+
|
|
6
|
+
# This module uses a raised exception to safely obtain the runtime types
|
|
7
|
+
# of traceback and frame objects, for compatibility with older Python versions
|
|
8
|
+
# where these types are not easily importable from the types module.
|
|
9
|
+
if sys.version_info >= (3, 7):
|
|
10
|
+
# No docstrings are provided for standard library versions.
|
|
11
|
+
TracebackType = types.TracebackType
|
|
12
|
+
FrameType = types.FrameType
|
|
13
|
+
else:
|
|
14
|
+
try:
|
|
15
|
+
raise TypeError
|
|
16
|
+
except TypeError as exc:
|
|
17
|
+
TracebackType = type(exc.__traceback__)
|
|
18
|
+
"""The type of traceback objects returned by exception.__traceback__."""
|
|
19
|
+
|
|
20
|
+
FrameType = type(exc.__traceback__.tb_frame) # type: ignore
|
|
21
|
+
"""The type of frame objects representing an execution frame in the call stack."""
|
_errortools/_version.py
ADDED
_errortools/abc.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from typing import Any, Literal, Union
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from .tools.error_msg import (
|
|
5
|
+
ErrorAttrableRaiseNotImplementedErrorMessage as ErrorAttrNotImplementedMsg,
|
|
6
|
+
)
|
|
7
|
+
from .methods import (
|
|
8
|
+
ErrorAttrMixin,
|
|
9
|
+
ErrorAttrCheckMixin,
|
|
10
|
+
ErrorAttrDeletionMixin,
|
|
11
|
+
ErrorSetAttrMixin,
|
|
12
|
+
)
|
|
13
|
+
from .classes.errorcodes import (
|
|
14
|
+
BaseErrorCodes,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
AccessDeniedError,
|
|
17
|
+
InvalidInputError,
|
|
18
|
+
ConfigurationError,
|
|
19
|
+
RuntimeFailure,
|
|
20
|
+
TimeoutFailure,
|
|
21
|
+
)
|
|
22
|
+
from .classes.warn import (
|
|
23
|
+
BaseWarning,
|
|
24
|
+
DeprecatedWarning,
|
|
25
|
+
PerformanceWarning,
|
|
26
|
+
ConfigurationWarning,
|
|
27
|
+
ResourceUsageWarning,
|
|
28
|
+
RuntimeBehaviourWarning,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _check_methods(C: type[Any], *methods: str) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
33
|
+
"""Check methods in `C`. If has, return `True`, else `NotImplemented`.
|
|
34
|
+
from `_collections_abc.py`.
|
|
35
|
+
Copyright 2007 Google, Inc. All Rights Reserved.
|
|
36
|
+
Licensed to PSF under a Contributor Agreement.
|
|
37
|
+
"""
|
|
38
|
+
mro: tuple[type[Any], ...] = C.__mro__
|
|
39
|
+
for method in methods:
|
|
40
|
+
for B in mro:
|
|
41
|
+
if method in B.__dict__:
|
|
42
|
+
if B.__dict__[method] is None:
|
|
43
|
+
return NotImplemented
|
|
44
|
+
break
|
|
45
|
+
else:
|
|
46
|
+
return NotImplemented
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ErrorAttrable(ABC):
|
|
51
|
+
"""
|
|
52
|
+
Abstract Base Class (ABC) for classes supporting custom attribute error handling.
|
|
53
|
+
|
|
54
|
+
This class follows the design pattern of `collections.abc` (e.g., Iterable, Mapping):
|
|
55
|
+
- Uses `__subclasshook__` + `_check_methods` to validate subclass compliance
|
|
56
|
+
- Enforces implementation of attribute error handling methods via abstract methods
|
|
57
|
+
- Implements native attribute magic methods to forward errors to custom handlers
|
|
58
|
+
|
|
59
|
+
Core behavior:
|
|
60
|
+
When attribute operations (get/delete/check/set) fail, the corresponding native
|
|
61
|
+
magic methods automatically invoke custom error handling methods implemented by subclasses.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
__slots__ = ()
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
68
|
+
"""
|
|
69
|
+
Check if a class is a subclass of ErrorAttrable (per `collections.abc` style).
|
|
70
|
+
|
|
71
|
+
This method enables `issubclass()` to recognize classes that implement the core
|
|
72
|
+
__errorattr__ method (base requirement), matching the behavior of standard ABCs.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
C: The class to check for compliance with ErrorAttrable interface
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if C implements __errorattr__, NotImplemented otherwise
|
|
79
|
+
"""
|
|
80
|
+
if cls is ErrorAttrable:
|
|
81
|
+
return _check_methods(C, "__errorattr__")
|
|
82
|
+
return NotImplemented
|
|
83
|
+
|
|
84
|
+
def __getattr__(self, name: str) -> Any:
|
|
85
|
+
"""
|
|
86
|
+
Native magic method: Automatically invoked for missing attribute access.
|
|
87
|
+
|
|
88
|
+
Forwards the attribute lookup failure to the custom `__errorattr__` method.
|
|
89
|
+
"""
|
|
90
|
+
return self.__errorattr__(name)
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def __errorattr__(self, name: str) -> Any:
|
|
94
|
+
"""
|
|
95
|
+
Abstract method for custom missing attribute handling (MUST be implemented).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name: Name of the missing attribute being accessed
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
NotImplementedError: If not overridden
|
|
102
|
+
AttributeError: Recommended error type for missing attributes
|
|
103
|
+
"""
|
|
104
|
+
raise NotImplementedError(ErrorAttrNotImplementedMsg)
|
|
105
|
+
|
|
106
|
+
def __delattr__(self, name: str) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Native magic method: Automatically invoked for attribute deletion errors.
|
|
109
|
+
|
|
110
|
+
Forwards to __errordelattr__ if implemented, else raises standard error.
|
|
111
|
+
"""
|
|
112
|
+
if hasattr(self, "__errordelattr__"):
|
|
113
|
+
self.__errordelattr__(name)
|
|
114
|
+
else:
|
|
115
|
+
super().__delattr__(name)
|
|
116
|
+
|
|
117
|
+
def __errordelattr__(self, name: str) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Custom handler for attribute deletion errors (OPTIONAL to implement).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
name: Name of the attribute being deleted
|
|
123
|
+
"""
|
|
124
|
+
raise NotImplementedError(ErrorAttrNotImplementedMsg)
|
|
125
|
+
|
|
126
|
+
def __contains__(self, name: str) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Alternative to __hasattr__: Check if attribute exists (customizable).
|
|
129
|
+
|
|
130
|
+
Forwards to __errorhasattr__ if implemented, else uses standard check.
|
|
131
|
+
"""
|
|
132
|
+
if hasattr(self, "__errorhasattr__"):
|
|
133
|
+
return self.__errorhasattr__(name)
|
|
134
|
+
else:
|
|
135
|
+
return hasattr(super(), name)
|
|
136
|
+
|
|
137
|
+
def __errorhasattr__(self, name: str) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Custom handler for attribute existence checks (OPTIONAL to implement).
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
name: Name of the attribute to check
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
bool: True if attribute exists (custom logic), False otherwise
|
|
146
|
+
"""
|
|
147
|
+
raise NotImplementedError(ErrorAttrNotImplementedMsg)
|
|
148
|
+
|
|
149
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Native magic method: Automatically invoked for attribute setting errors.
|
|
152
|
+
|
|
153
|
+
Forwards to __errorsetattr__ if implemented, else uses standard setting.
|
|
154
|
+
"""
|
|
155
|
+
if hasattr(self, "__errorsetattr__"):
|
|
156
|
+
self.__errorsetattr__(name, value)
|
|
157
|
+
else:
|
|
158
|
+
super().__setattr__(name, value)
|
|
159
|
+
|
|
160
|
+
def __errorsetattr__(self, name: str, value: Any) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Custom handler for attribute setting errors (OPTIONAL to implement).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: Name of the attribute to set
|
|
166
|
+
value: Value to assign to the attribute
|
|
167
|
+
"""
|
|
168
|
+
raise NotImplementedError(ErrorAttrNotImplementedMsg)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# register four Mixin's
|
|
172
|
+
ErrorAttrable.register(ErrorAttrMixin)
|
|
173
|
+
ErrorAttrable.register(ErrorAttrDeletionMixin)
|
|
174
|
+
ErrorAttrable.register(ErrorAttrCheckMixin)
|
|
175
|
+
ErrorAttrable.register(ErrorSetAttrMixin)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ----------------------------------------------------------------------
|
|
179
|
+
# ErrorCodeable
|
|
180
|
+
# ----------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ErrorCodeable(ABC):
|
|
184
|
+
"""Abstract Base Class for exceptions that carry a machine-readable error code.
|
|
185
|
+
|
|
186
|
+
Follows the ``collections.abc`` pattern: any class that exposes both a
|
|
187
|
+
``code`` class attribute (``int``) and a ``default_detail`` class attribute
|
|
188
|
+
(``str``) is recognised as a virtual subclass automatically, without
|
|
189
|
+
explicit inheritance.
|
|
190
|
+
|
|
191
|
+
Concrete subclasses **must** implement:
|
|
192
|
+
- ``code`` — integer error code (class variable)
|
|
193
|
+
- ``default_detail`` — fallback human-readable message (class variable)
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
|
|
197
|
+
>>> class PaymentError(ErrorCodeable, Exception):
|
|
198
|
+
... code = 6001
|
|
199
|
+
... default_detail = "Payment failed."
|
|
200
|
+
>>> issubclass(PaymentError, ErrorCodeable)
|
|
201
|
+
True
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
__slots__ = ()
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
208
|
+
"""Recognise any class that defines ``code`` and ``default_detail``."""
|
|
209
|
+
if cls is ErrorCodeable:
|
|
210
|
+
return _check_methods(C, "code", "default_detail")
|
|
211
|
+
return NotImplemented
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def code(self) -> int:
|
|
216
|
+
"""Integer error code identifying this exception type."""
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
@abstractmethod
|
|
221
|
+
def default_detail(self) -> str:
|
|
222
|
+
"""Fallback human-readable message used when no detail is provided."""
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ----------------------------------------------------------------------
|
|
227
|
+
# Warnable
|
|
228
|
+
# ----------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Warnable(ABC):
|
|
232
|
+
"""Abstract Base Class for warning classes that can emit themselves.
|
|
233
|
+
|
|
234
|
+
Any class that exposes an ``emit`` classmethod is recognised as a
|
|
235
|
+
virtual subclass automatically via ``__subclasshook__``.
|
|
236
|
+
|
|
237
|
+
Concrete subclasses **must** implement:
|
|
238
|
+
- ``emit(cls, detail, stacklevel)`` — issue the warning via ``warnings.warn``
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
|
|
242
|
+
>>> class SlowWarning(Warnable, Warning):
|
|
243
|
+
... default_detail = "This operation is slow."
|
|
244
|
+
... @classmethod
|
|
245
|
+
... def emit(cls, detail=None, stacklevel=2):
|
|
246
|
+
... import warnings
|
|
247
|
+
... warnings.warn(cls(detail), stacklevel=stacklevel)
|
|
248
|
+
>>> issubclass(SlowWarning, Warnable)
|
|
249
|
+
True
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
__slots__ = ()
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
256
|
+
"""Recognise any class that defines an ``emit`` classmethod."""
|
|
257
|
+
if cls is Warnable:
|
|
258
|
+
return _check_methods(C, "emit")
|
|
259
|
+
return NotImplemented
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
@abstractmethod
|
|
263
|
+
def emit(cls, detail: str | None = None, stacklevel: int = 2) -> None:
|
|
264
|
+
"""Issue this warning via ``warnings.warn``.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
detail: Optional message override.
|
|
268
|
+
stacklevel: Passed to ``warnings.warn``; ``2`` points at the
|
|
269
|
+
caller of ``emit``.
|
|
270
|
+
"""
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ----------------------------------------------------------------------
|
|
275
|
+
# Raiseable
|
|
276
|
+
# ----------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Raiseable(ABC):
|
|
280
|
+
"""Abstract Base Class for objects that know how to raise themselves.
|
|
281
|
+
|
|
282
|
+
Concrete subclasses **must** implement ``raise_it()``, which should
|
|
283
|
+
raise ``self`` (or a derived exception). Any class that exposes a
|
|
284
|
+
``raise_it`` method is recognised as a virtual subclass automatically.
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
|
|
288
|
+
>>> class MyError(Raiseable, Exception):
|
|
289
|
+
... def raise_it(self):
|
|
290
|
+
... raise self
|
|
291
|
+
>>> e = MyError("oops")
|
|
292
|
+
>>> e.raise_it()
|
|
293
|
+
Traceback (most recent call last):
|
|
294
|
+
...
|
|
295
|
+
MyError: oops
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
__slots__ = ()
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
302
|
+
"""Recognise any class that defines a ``raise_it`` method."""
|
|
303
|
+
if cls is Raiseable:
|
|
304
|
+
return _check_methods(C, "raise_it")
|
|
305
|
+
return NotImplemented
|
|
306
|
+
|
|
307
|
+
@abstractmethod
|
|
308
|
+
def raise_it(self) -> None:
|
|
309
|
+
"""Raise this object as an exception.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
self: Or a derived exception wrapping this object.
|
|
313
|
+
"""
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ----------------------------------------------------------------------
|
|
318
|
+
# Register existing concrete classes as virtual subclasses
|
|
319
|
+
# ----------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
ErrorCodeable.register(BaseErrorCodes)
|
|
322
|
+
ErrorCodeable.register(NotFoundError)
|
|
323
|
+
ErrorCodeable.register(AccessDeniedError)
|
|
324
|
+
ErrorCodeable.register(InvalidInputError)
|
|
325
|
+
ErrorCodeable.register(ConfigurationError)
|
|
326
|
+
ErrorCodeable.register(RuntimeFailure)
|
|
327
|
+
ErrorCodeable.register(TimeoutFailure)
|
|
328
|
+
Warnable.register(BaseWarning)
|
|
329
|
+
Warnable.register(DeprecatedWarning)
|
|
330
|
+
Warnable.register(PerformanceWarning)
|
|
331
|
+
Warnable.register(ConfigurationWarning)
|
|
332
|
+
Warnable.register(ResourceUsageWarning)
|
|
333
|
+
Warnable.register(RuntimeBehaviourWarning)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cache tools in `errortools`."""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""A helper tool for caching exceptions raised by functions, like lru_cache."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import (
|
|
5
|
+
Callable,
|
|
6
|
+
Any,
|
|
7
|
+
Optional,
|
|
8
|
+
TypeVar,
|
|
9
|
+
overload,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from _errortools.cached.wrapper import ErrorCacheWrapper
|
|
13
|
+
|
|
14
|
+
_T = TypeVar("_T", bound=Callable[..., Any])
|
|
15
|
+
|
|
16
|
+
# fmt: off
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
def error_cache(func: _T) -> ErrorCacheWrapper[_T]:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@overload
|
|
25
|
+
def error_cache(maxsize: Optional[int] = 128) -> Callable[[_T], ErrorCacheWrapper[_T]]:
|
|
26
|
+
...
|
|
27
|
+
# fmt: on
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def error_cache( # type: ignore
|
|
31
|
+
func: Optional[_T] = None, maxsize: Optional[int] = 128
|
|
32
|
+
) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
Decorator to cache exceptions raised by a function (like functools.lru_cache).
|
|
35
|
+
|
|
36
|
+
This decorator automatically caches exceptions thrown by the wrapped function,
|
|
37
|
+
keyed by the function's arguments. If the function succeeds, the cached exception
|
|
38
|
+
(if any) for those arguments is removed.
|
|
39
|
+
|
|
40
|
+
Key features (aligned with lru_cache):
|
|
41
|
+
- maxsize: Maximum number of cached errors (None = unlimited, default=128)
|
|
42
|
+
- LRU eviction: Evicts least recently used entries when maxsize is reached
|
|
43
|
+
- cache_info(): Returns hits/misses/maxsize/currsize stats
|
|
44
|
+
- clear_cache(): Clears cache and resets statistics
|
|
45
|
+
|
|
46
|
+
Usage (same as lru_cache):
|
|
47
|
+
|
|
48
|
+
@error_cache # Default maxsize=128
|
|
49
|
+
def risky_func(x: int) -> int: ...
|
|
50
|
+
|
|
51
|
+
@error_cache(maxsize=32) # Custom maxsize
|
|
52
|
+
def risky_func(x: int) -> int: ...
|
|
53
|
+
|
|
54
|
+
@error_cache(maxsize=None) # Unlimited cache
|
|
55
|
+
def risky_func(x: int) -> int: ...
|
|
56
|
+
|
|
57
|
+
@error_cache() # Explicit empty args (maxsize=128)
|
|
58
|
+
def risky_func(x: int) -> int: ...
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
func: The function to wrap (auto-passed when using @error_cache without args).
|
|
62
|
+
maxsize: Maximum number of cached errors (None = unlimited, default=128).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Wrapped function with error caching functionality.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
TypeError: If non-hashable arguments are passed (same as lru_cache).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def decorator(f: _T) -> ErrorCacheWrapper[_T]:
|
|
72
|
+
if not callable(f):
|
|
73
|
+
raise TypeError(f"Expected a callable, got {type(f).__name__} instead")
|
|
74
|
+
|
|
75
|
+
wrapper = ErrorCacheWrapper(f, maxsize=maxsize)
|
|
76
|
+
functools.update_wrapper(wrapper, f)
|
|
77
|
+
return wrapper
|
|
78
|
+
|
|
79
|
+
# Handle both @error_cache and @error_cache(...) usage
|
|
80
|
+
if func is None:
|
|
81
|
+
return decorator
|
|
82
|
+
return decorator(func)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from collections.abc import Hashable, Callable
|
|
3
|
+
from typing import Any, Generic, TypeVar, Optional, TypeAlias
|
|
4
|
+
|
|
5
|
+
_T = TypeVar("_T", bound=Callable[..., Any])
|
|
6
|
+
_Key: TypeAlias = tuple[
|
|
7
|
+
str, tuple[Hashable, ...], tuple[tuple[Hashable, Hashable], ...]
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorCacheWrapper(Generic[_T]):
|
|
12
|
+
"""Wrapper class for error-cached functions (mimics lru_cache's wrapper style)."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, func: _T, maxsize: Optional[int] = 128) -> None:
|
|
15
|
+
self.__wrapped__ = func # Required for inspect module compatibility
|
|
16
|
+
self._func_name = func.__name__
|
|
17
|
+
|
|
18
|
+
# LRU cache with maxsize support (OrderedDict preserves access order)
|
|
19
|
+
self._maxsize = maxsize if (maxsize is None or maxsize > 0) else None
|
|
20
|
+
self._cache: OrderedDict[_Key, Exception] = OrderedDict()
|
|
21
|
+
|
|
22
|
+
# Cache statistics (1:1 with lru_cache)
|
|
23
|
+
self._hits = 0
|
|
24
|
+
self._misses = 0
|
|
25
|
+
|
|
26
|
+
def __call__(self, *args: Hashable, **kwargs: Hashable) -> Any:
|
|
27
|
+
"""Execute the wrapped function and cache exceptions if raised (with LRU)."""
|
|
28
|
+
cache_key = self._make_key(args, kwargs)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
result = self.__wrapped__(*args, **kwargs)
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
# Cache exception and enforce LRU eviction
|
|
34
|
+
self._cache[cache_key] = exc
|
|
35
|
+
self._misses += 1
|
|
36
|
+
|
|
37
|
+
# Evict least recently used if maxsize is exceeded
|
|
38
|
+
if self._maxsize is not None and len(self._cache) > self._maxsize:
|
|
39
|
+
self._cache.popitem(last=False) # FIFO = LRU for insert order
|
|
40
|
+
raise
|
|
41
|
+
else:
|
|
42
|
+
# Auto-clear cache for successful calls
|
|
43
|
+
self._cache.pop(cache_key, None)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
def _make_key(
|
|
47
|
+
self, args: tuple[Hashable, ...], kwargs: dict[str, Hashable]
|
|
48
|
+
) -> _Key:
|
|
49
|
+
"""Generate a unique hashable key (consistent with lru_cache's logic)."""
|
|
50
|
+
sorted_kwargs = tuple(sorted(kwargs.items()))
|
|
51
|
+
return (self._func_name, args, sorted_kwargs)
|
|
52
|
+
|
|
53
|
+
# ---------------------- Cache control methods (like lru_cache) ----------------------
|
|
54
|
+
def get_cached_error(
|
|
55
|
+
self, *args: Hashable, **kwargs: Hashable
|
|
56
|
+
) -> Optional[Exception]:
|
|
57
|
+
"""Get the cached exception for the given arguments (if exists)."""
|
|
58
|
+
cache_key = self._make_key(args, kwargs)
|
|
59
|
+
if cache_key in self._cache:
|
|
60
|
+
self._hits += 1
|
|
61
|
+
self._cache.move_to_end(cache_key) # Update LRU order (most recent)
|
|
62
|
+
return self._cache[cache_key]
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def clear_cache(self) -> None:
|
|
66
|
+
"""Clear all cached exceptions and reset statistics (mimics lru_cache.clear)."""
|
|
67
|
+
self._cache.clear()
|
|
68
|
+
self._hits = 0
|
|
69
|
+
self._misses = 0
|
|
70
|
+
|
|
71
|
+
def cache_info(self) -> str:
|
|
72
|
+
"""Return cache statistics (1:1 with lru_cache.cache_info)."""
|
|
73
|
+
return (
|
|
74
|
+
f"ErrorCacheInfo(hits={self._hits}, misses={self._misses}, "
|
|
75
|
+
f"maxsize={self._maxsize}, currsize={len(self._cache)})"
|
|
76
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Base classes."""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Base exception classes with general-purpose error code support."""
|
|
2
|
+
|
|
3
|
+
from .base.base import ErrorToolsBaseException
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"BaseErrorCodes",
|
|
7
|
+
"InvalidInputError",
|
|
8
|
+
"NotFoundError",
|
|
9
|
+
"AccessDeniedError",
|
|
10
|
+
"ConfigurationError",
|
|
11
|
+
"RuntimeFailure",
|
|
12
|
+
"TimeoutFailure",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseErrorCodes(ErrorToolsBaseException):
|
|
17
|
+
"""A base exception with class-level error code and default detail.
|
|
18
|
+
|
|
19
|
+
Subclass and override `code` and `default_detail` to define
|
|
20
|
+
domain-specific exceptions with stable, machine-readable codes. The
|
|
21
|
+
``detail`` passed at raise-time overrides the class default.
|
|
22
|
+
|
|
23
|
+
Error code ranges (by convention):
|
|
24
|
+
|
|
25
|
+
- ``1xxx`` — input / validation
|
|
26
|
+
- ``2xxx`` — access / permission
|
|
27
|
+
- ``3xxx`` — resource lookup
|
|
28
|
+
- ``4xxx`` — runtime / execution
|
|
29
|
+
- ``5xxx`` — configuration / setup
|
|
30
|
+
- ``-1`` — unspecified
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
code: Integer error code for this exception class. Defaults to ``-1``.
|
|
34
|
+
default_detail: Fallback message used when no *detail* is supplied.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> class PaymentError(BaseErrorCodes):
|
|
38
|
+
... code = 6001
|
|
39
|
+
... default_detail = "Payment processing failed."
|
|
40
|
+
>>> raise PaymentError()
|
|
41
|
+
Traceback (most recent call last):
|
|
42
|
+
...
|
|
43
|
+
PaymentError: [6001] Payment processing failed.
|
|
44
|
+
>>> raise PaymentError("card declined")
|
|
45
|
+
Traceback (most recent call last):
|
|
46
|
+
...
|
|
47
|
+
PaymentError: [6001] card declined
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
code: int = -1
|
|
51
|
+
default_detail: str = "An error occurred."
|
|
52
|
+
|
|
53
|
+
def __init__(self, detail: str | None = None) -> None:
|
|
54
|
+
"""Initialise the exception with an optional detail message.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
detail: Human-readable description of the error. Falls back to
|
|
58
|
+
`default_detail` when ``None``.
|
|
59
|
+
"""
|
|
60
|
+
self.detail = detail if detail is not None else self.default_detail
|
|
61
|
+
super().__init__(self.detail)
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
return f"[{self.code}] {self.detail}"
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return f"{type(self).__name__}(detail={self.detail!r}, code={self.code!r})"
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# Factory classmethods
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def invalid_input(cls, detail: str | None = None) -> InvalidInputError:
|
|
75
|
+
"""Return an `InvalidInputError` (1001) instance."""
|
|
76
|
+
return InvalidInputError(detail)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def not_found(cls, detail: str | None = None) -> NotFoundError:
|
|
80
|
+
"""Return a `NotFoundError` (3001) instance."""
|
|
81
|
+
return NotFoundError(detail)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def access_denied(cls, detail: str | None = None) -> AccessDeniedError:
|
|
85
|
+
"""Return an `AccessDeniedError` (2001) instance."""
|
|
86
|
+
return AccessDeniedError(detail)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def configuration_error(cls, detail: str | None = None) -> ConfigurationError:
|
|
90
|
+
"""Return a `ConfigurationError` (5001) instance."""
|
|
91
|
+
return ConfigurationError(detail)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def runtime_failure(cls, detail: str | None = None) -> RuntimeFailure:
|
|
95
|
+
"""Return a `RuntimeFailure` (4001) instance."""
|
|
96
|
+
return RuntimeFailure(detail)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def timeout_failure(cls, detail: str | None = None) -> TimeoutFailure:
|
|
100
|
+
"""Return a `TimeoutFailure` (4002) instance."""
|
|
101
|
+
return TimeoutFailure(detail)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ----------------------------------------------------------------------
|
|
105
|
+
# Predefined subclasses — general application categories
|
|
106
|
+
# ----------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class InvalidInputError(BaseErrorCodes):
|
|
110
|
+
"""1001 — Input failed validation or is of the wrong type/format."""
|
|
111
|
+
|
|
112
|
+
code = 1001
|
|
113
|
+
default_detail = "Invalid input."
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AccessDeniedError(BaseErrorCodes):
|
|
117
|
+
"""2001 — The caller lacks permission to perform the requested action."""
|
|
118
|
+
|
|
119
|
+
code = 2001
|
|
120
|
+
default_detail = "Access denied."
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NotFoundError(BaseErrorCodes):
|
|
124
|
+
"""3001 — The requested resource or item could not be located."""
|
|
125
|
+
|
|
126
|
+
code = 3001
|
|
127
|
+
default_detail = "Resource not found."
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RuntimeFailure(BaseErrorCodes):
|
|
131
|
+
"""4001 — An unexpected failure occurred during execution."""
|
|
132
|
+
|
|
133
|
+
code = 4001
|
|
134
|
+
default_detail = "Runtime failure."
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TimeoutFailure(BaseErrorCodes):
|
|
138
|
+
"""4002 — An operation exceeded its allowed time limit."""
|
|
139
|
+
|
|
140
|
+
code = 4002
|
|
141
|
+
default_detail = "Operation timed out."
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ConfigurationError(BaseErrorCodes):
|
|
145
|
+
"""5001 — The application is misconfigured or missing required settings."""
|
|
146
|
+
|
|
147
|
+
code = 5001
|
|
148
|
+
default_detail = "Configuration error."
|