errortools 3.0.0__cp314-cp314-win_amd64.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/_cli.py +33 -0
- _errortools/_speedup.c +58 -0
- _errortools/_speedup.cp314-win_amd64.pyd +0 -0
- _errortools/classes/__init__.py +1 -0
- _errortools/classes/abc.py +211 -0
- _errortools/classes/errorcodes.py +273 -0
- _errortools/classes/group.py +121 -0
- _errortools/classes/warn.py +124 -0
- _errortools/cli.py +108 -0
- _errortools/const.py +12 -0
- _errortools/decorator/__init__.py +1 -0
- _errortools/decorator/cache.py +82 -0
- _errortools/decorator/deprecated.py +61 -0
- _errortools/descriptor/__init__.py +2 -0
- _errortools/descriptor/errormsg.py +37 -0
- _errortools/descriptor/nonblankmsg.py +52 -0
- _errortools/errno.py +86 -0
- _errortools/future.py +179 -0
- _errortools/ignore.py +288 -0
- _errortools/logging/__init__.py +43 -0
- _errortools/logging/base.py +467 -0
- _errortools/logging/level.py +85 -0
- _errortools/logging/logger.py +13 -0
- _errortools/logging/record.py +116 -0
- _errortools/logging/sink.py +243 -0
- _errortools/metadata.py +16 -0
- _errortools/partial.py +199 -0
- _errortools/py.typed +0 -0
- _errortools/raises.py +186 -0
- _errortools/typing.py +101 -0
- _errortools/version.py +7 -0
- _errortools/wrappers/__init__.py +2 -0
- _errortools/wrappers/cache.py +101 -0
- _errortools/wrappers/ignore.py +122 -0
- errortools/__init__.py +167 -0
- errortools/__main__.py +4 -0
- errortools/future.py +5 -0
- errortools/logging.py +46 -0
- errortools/partial.py +5 -0
- errortools-3.0.0.dist-info/METADATA +366 -0
- errortools-3.0.0.dist-info/RECORD +64 -0
- errortools-3.0.0.dist-info/WHEEL +5 -0
- errortools-3.0.0.dist-info/entry_points.txt +3 -0
- errortools-3.0.0.dist-info/licenses/AUTHORS.txt +3 -0
- errortools-3.0.0.dist-info/licenses/LICENSE.txt +20 -0
- errortools-3.0.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +12 -0
- tests/conftest.py +14 -0
- tests/run_tests.py +19 -0
- tests/test_abc.py +300 -0
- tests/test_const.py +28 -0
- tests/test_decorator.py +361 -0
- tests/test_descriptor.py +150 -0
- tests/test_errno.py +110 -0
- tests/test_errorcodes.py +395 -0
- tests/test_future.py +296 -0
- tests/test_groups.py +128 -0
- tests/test_ignore.py +412 -0
- tests/test_logging.py +674 -0
- tests/test_partials.py +228 -0
- tests/test_raises.py +175 -0
- tests/test_typing.py +152 -0
- tests/test_warnings.py +151 -0
_errortools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Nothing here!
|
_errortools/_cli.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from _errortools.metadata import (
|
|
4
|
+
__author__,
|
|
5
|
+
__author_email__,
|
|
6
|
+
__copyright__,
|
|
7
|
+
__description__,
|
|
8
|
+
__license__,
|
|
9
|
+
__url__,
|
|
10
|
+
)
|
|
11
|
+
from _errortools.version import __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cmd_log(message: str, level: str, output: str) -> None:
|
|
15
|
+
"""Emit a single log message via the errortools logger."""
|
|
16
|
+
from .logging import BaseLogger
|
|
17
|
+
from .logging.level import get_level
|
|
18
|
+
|
|
19
|
+
stream = sys.stdout if output == "stdout" else sys.stderr
|
|
20
|
+
log = BaseLogger(name="errortools-cli")
|
|
21
|
+
log.set_level("TRACE")
|
|
22
|
+
log.add(stream, level=level, colorize=None)
|
|
23
|
+
log.log(get_level(level), message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print_info() -> None:
|
|
27
|
+
"""Print a summary of all package metadata."""
|
|
28
|
+
print(f"errortools v{__version__}")
|
|
29
|
+
print(f" {__description__}")
|
|
30
|
+
print(f" Author: {__author__} <{__author_email__}>")
|
|
31
|
+
print(f" License: {__license__}")
|
|
32
|
+
print(f" URL: {__url__}")
|
|
33
|
+
print(f" Copyright: {__copyright__}")
|
_errortools/_speedup.c
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#define PY_SSIZE_T_CLEAN
|
|
2
|
+
#include <Python.h>
|
|
3
|
+
|
|
4
|
+
/* Fast exception type checking */
|
|
5
|
+
static PyObject* fast_issubclass_check(PyObject* self, PyObject* args) {
|
|
6
|
+
PyObject *typ, *excs;
|
|
7
|
+
if (!PyArg_ParseTuple(args, "OO", &typ, &excs)) {
|
|
8
|
+
return NULL;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typ == Py_None) {
|
|
12
|
+
Py_RETURN_FALSE;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
int result = PyObject_IsSubclass(typ, excs);
|
|
16
|
+
if (result == -1) {
|
|
17
|
+
return NULL;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return PyBool_FromLong(result);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Fast exception collector append */
|
|
24
|
+
static PyObject* fast_append_exception(PyObject* self, PyObject* args) {
|
|
25
|
+
PyObject *list, *exc;
|
|
26
|
+
if (!PyArg_ParseTuple(args, "OO", &list, &exc)) {
|
|
27
|
+
return NULL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (PyList_Append(list, exc) == -1) {
|
|
31
|
+
return NULL;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Py_RETURN_NONE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Method definitions */
|
|
38
|
+
static PyMethodDef SpeedupMethods[] = {
|
|
39
|
+
{"fast_issubclass_check", fast_issubclass_check, METH_VARARGS,
|
|
40
|
+
"Fast exception type checking"},
|
|
41
|
+
{"fast_append_exception", fast_append_exception, METH_VARARGS,
|
|
42
|
+
"Fast exception list append"},
|
|
43
|
+
{NULL, NULL, 0, NULL}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* Module definition */
|
|
47
|
+
static struct PyModuleDef speedupmodule = {
|
|
48
|
+
PyModuleDef_HEAD_INIT,
|
|
49
|
+
"_speedup",
|
|
50
|
+
"C speedup for errortools",
|
|
51
|
+
-1,
|
|
52
|
+
SpeedupMethods
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/* Module initialization */
|
|
56
|
+
PyMODINIT_FUNC PyInit__speedup(void) {
|
|
57
|
+
return PyModule_Create(&speedupmodule);
|
|
58
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Base classes."""
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from typing import Any, Literal, Union
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
import copy
|
|
4
|
+
import shutil
|
|
5
|
+
import csv
|
|
6
|
+
import configparser
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 15):
|
|
10
|
+
from typing import disjoint_base
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import disjoint_base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _check_methods(C: type[Any], *methods: str) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
16
|
+
"""Check methods in `C`. If has, return `True`, else `NotImplemented`."""
|
|
17
|
+
# from `_collections_abc.py`.
|
|
18
|
+
# Copyright 2007 Google, Inc. All Rights Reserved.
|
|
19
|
+
# Licensed to PSF under a Contributor Agreement.
|
|
20
|
+
mro: tuple[type[Any], ...] = C.__mro__ # Added type hints for mro var
|
|
21
|
+
for method in methods:
|
|
22
|
+
for B in mro:
|
|
23
|
+
if method in B.__dict__:
|
|
24
|
+
if B.__dict__[method] is None:
|
|
25
|
+
return NotImplemented
|
|
26
|
+
break
|
|
27
|
+
else:
|
|
28
|
+
return NotImplemented
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------
|
|
33
|
+
# ErrorCodeable
|
|
34
|
+
# ----------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@disjoint_base
|
|
38
|
+
class ErrorCodeable(ABC):
|
|
39
|
+
"""Abstract Base Class for exceptions that carry a machine-readable error code.
|
|
40
|
+
|
|
41
|
+
Follows the ``collections.abc`` pattern: any class that exposes both a
|
|
42
|
+
``code`` class attribute (``int``) and a ``default_detail`` class attribute
|
|
43
|
+
(``str``) is recognised as a virtual subclass automatically, without
|
|
44
|
+
explicit inheritance.
|
|
45
|
+
|
|
46
|
+
Concrete subclasses **must** implement:
|
|
47
|
+
- ``code`` — integer error code (class variable)
|
|
48
|
+
- ``default_detail`` — fallback human-readable message (class variable)
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
|
|
52
|
+
>>> class PaymentError(ErrorCodeable, Exception):
|
|
53
|
+
... code = 6001
|
|
54
|
+
... default_detail = "Payment failed."
|
|
55
|
+
>>> issubclass(PaymentError, ErrorCodeable)
|
|
56
|
+
True
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
__slots__ = ()
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
63
|
+
"""Recognise any class that defines ``code`` and ``default_detail``."""
|
|
64
|
+
if cls is ErrorCodeable:
|
|
65
|
+
return _check_methods(C, "code", "default_detail")
|
|
66
|
+
return NotImplemented
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def code(self) -> int:
|
|
71
|
+
"""Integer error code identifying this exception type."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def default_detail(self) -> str:
|
|
77
|
+
"""Fallback human-readable message used when no detail is provided."""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ----------------------------------------------------------------------
|
|
82
|
+
# Warnable
|
|
83
|
+
# ----------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Warnable(ABC):
|
|
87
|
+
"""Abstract Base Class for warning classes that can emit themselves.
|
|
88
|
+
|
|
89
|
+
Any class that exposes an ``emit`` classmethod is recognised as a
|
|
90
|
+
virtual subclass automatically via ``__subclasshook__``.
|
|
91
|
+
|
|
92
|
+
Concrete subclasses **must** implement:
|
|
93
|
+
- ``emit(cls, detail, stacklevel)`` — issue the warning via ``warnings.warn``
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
|
|
97
|
+
>>> class SlowWarning(Warnable, Warning):
|
|
98
|
+
... default_detail = "This operation is slow."
|
|
99
|
+
... @classmethod
|
|
100
|
+
... def emit(cls, detail=None, stacklevel=2):
|
|
101
|
+
... import warnings
|
|
102
|
+
... warnings.warn(cls(detail), stacklevel=stacklevel)
|
|
103
|
+
>>> issubclass(SlowWarning, Warnable)
|
|
104
|
+
True
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
__slots__ = ()
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
111
|
+
"""Recognise any class that defines an ``emit`` classmethod."""
|
|
112
|
+
if cls is Warnable:
|
|
113
|
+
return _check_methods(C, "emit")
|
|
114
|
+
return NotImplemented
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def emit(cls, detail: str | None = None, stacklevel: int = 2) -> None:
|
|
119
|
+
"""Issue this warning via ``warnings.warn``.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
detail: Optional message override.
|
|
123
|
+
stacklevel: Passed to ``warnings.warn``; ``2`` points at the
|
|
124
|
+
caller of ``emit``.
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ----------------------------------------------------------------------
|
|
130
|
+
# Raiseable
|
|
131
|
+
# ----------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Raiseable(ABC):
|
|
135
|
+
"""Abstract Base Class for objects that know how to raise themselves.
|
|
136
|
+
|
|
137
|
+
Concrete subclasses **must** implement ``raise_it()``, which should
|
|
138
|
+
raise ``self`` (or a derived exception). Any class that exposes a
|
|
139
|
+
``raise_it`` method is recognised as a virtual subclass automatically.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
|
|
143
|
+
>>> class MyError(Raiseable, Exception):
|
|
144
|
+
... def raise_it(self):
|
|
145
|
+
... raise self
|
|
146
|
+
>>> e = MyError("oops")
|
|
147
|
+
>>> e.raise_it()
|
|
148
|
+
Traceback (most recent call last):
|
|
149
|
+
...
|
|
150
|
+
MyError: oops
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
__slots__ = ()
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
157
|
+
"""Recognise any class that defines a ``raise_it`` method."""
|
|
158
|
+
if cls is Raiseable:
|
|
159
|
+
return _check_methods(C, "raise_it")
|
|
160
|
+
return NotImplemented
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def raise_it(self) -> None:
|
|
164
|
+
"""Raise this object as an exception.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
self: Or a derived exception wrapping this object.
|
|
168
|
+
"""
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ----------------------------------------------------------------------
|
|
173
|
+
# Error
|
|
174
|
+
# ----------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Error(Exception, ABC):
|
|
178
|
+
"""Abstract Base Class for module-level Error exceptions.
|
|
179
|
+
|
|
180
|
+
Any class named **"Error"** (like copy.Error, shutil.Error, csv.Error)
|
|
181
|
+
is automatically recognised as a virtual subclass of this ABC.
|
|
182
|
+
|
|
183
|
+
Virtual subclasses do NOT need to explicitly inherit from this class.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
|
|
187
|
+
>>> import copy
|
|
188
|
+
>>> import shutil
|
|
189
|
+
>>> isinstance(copy.Error(), Error)
|
|
190
|
+
True
|
|
191
|
+
>>> isinstance(shutil.Error(), Error)
|
|
192
|
+
True
|
|
193
|
+
>>> class MyError:
|
|
194
|
+
... __name__ = "Error"
|
|
195
|
+
>>> isinstance(MyError(), Error)
|
|
196
|
+
True
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
__slots__ = ()
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def __subclasshook__(cls, subclass: type[Any]) -> bool:
|
|
203
|
+
if cls is Error:
|
|
204
|
+
return subclass.__name__ == "Error"
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
Error.register(copy.Error)
|
|
209
|
+
Error.register(shutil.Error)
|
|
210
|
+
Error.register(csv.Error)
|
|
211
|
+
Error.register(configparser.Error)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import traceback
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"PureBaseException",
|
|
7
|
+
"ContextException",
|
|
8
|
+
"BaseErrorCodes",
|
|
9
|
+
"InvalidInputError",
|
|
10
|
+
"AccessDeniedError",
|
|
11
|
+
"NotFoundError",
|
|
12
|
+
"RuntimeFailure",
|
|
13
|
+
"TimeoutFailure",
|
|
14
|
+
"ConfigurationError",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ==============================================
|
|
19
|
+
# 1. Pure Base Exception Class (core structure only, no additional capabilities)
|
|
20
|
+
# ==============================================
|
|
21
|
+
class PureBaseException(Exception):
|
|
22
|
+
"""
|
|
23
|
+
Pure Base Exception Class
|
|
24
|
+
Inherits from Exception, providing error code, default prompt, basic initialization and string formatting
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Class attributes: default error code and prompt, which can be overridden by subclasses
|
|
28
|
+
code: int = -1
|
|
29
|
+
default_detail: str = "An error occurred."
|
|
30
|
+
|
|
31
|
+
def __init__(self, detail: str | None = None) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Pure Base Exception Initialization
|
|
34
|
+
Args:
|
|
35
|
+
detail: Custom error prompt, use default prompt (default_detail) when None
|
|
36
|
+
"""
|
|
37
|
+
self.detail = detail if detail is not None else self.default_detail
|
|
38
|
+
super().__init__(self.detail)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Exception String Formatting
|
|
43
|
+
Returns:
|
|
44
|
+
Formatted string: [Error Code] Error Prompt
|
|
45
|
+
"""
|
|
46
|
+
return f"[{self.code}] {self.detail}"
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Exception Repr Formatting (core attributes)
|
|
51
|
+
Returns:
|
|
52
|
+
Repr string including class name, detail, and code
|
|
53
|
+
"""
|
|
54
|
+
return f"{type(self).__name__}(detail={self.detail!r}, code={self.code!r})"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ==============================================
|
|
58
|
+
# 2. Context/Exception Chain Common Capability Class (core extension functions)
|
|
59
|
+
# ==============================================
|
|
60
|
+
class ContextException(PureBaseException):
|
|
61
|
+
"""
|
|
62
|
+
Context/Exception Chain Capability Class (extension layer)
|
|
63
|
+
Inherits from pure base exception, providing trace ID, context management, exception chain, and simplified stack trace
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, detail: str | None = None) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Context Exception Initialization
|
|
69
|
+
Args:
|
|
70
|
+
detail: Custom error prompt, use default prompt (default_detail) when None
|
|
71
|
+
"""
|
|
72
|
+
super().__init__(detail)
|
|
73
|
+
# Extended attributes: trace ID (unique identifier for a single exception), context dictionary, root cause exception
|
|
74
|
+
self.trace_id: str = uuid.uuid4().hex
|
|
75
|
+
self.context: dict[str, Any] = {}
|
|
76
|
+
self.cause: Optional[Exception] = None
|
|
77
|
+
|
|
78
|
+
def __repr__(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Exception Repr Formatting (with trace_id)
|
|
81
|
+
Returns:
|
|
82
|
+
Repr string including class name, detail, code, and trace_id
|
|
83
|
+
"""
|
|
84
|
+
return f"{type(self).__name__}(detail={self.detail!r}, code={self.code!r}, trace_id={self.trace_id!r})"
|
|
85
|
+
|
|
86
|
+
def with_context(self, **kwargs: Any) -> "ContextException":
|
|
87
|
+
"""
|
|
88
|
+
Attach Context Data to Exception
|
|
89
|
+
Args:
|
|
90
|
+
**kwargs: Context key-value pairs (any type)
|
|
91
|
+
Returns:
|
|
92
|
+
Self instance (supports method chaining)
|
|
93
|
+
"""
|
|
94
|
+
self.context.update(kwargs)
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def with_cause(self, cause: Exception) -> "ContextException":
|
|
98
|
+
"""
|
|
99
|
+
Set Root Cause Exception
|
|
100
|
+
Args:
|
|
101
|
+
cause: Root cause exception (any subclass of Exception)
|
|
102
|
+
Returns:
|
|
103
|
+
Self instance (supports method chaining)
|
|
104
|
+
"""
|
|
105
|
+
self.cause = cause
|
|
106
|
+
self.__cause__ = cause # Retain native exception chain
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def chain(self) -> list[dict[str, Any]]:
|
|
111
|
+
"""
|
|
112
|
+
Exception Chain (current to root cause)
|
|
113
|
+
Returns:
|
|
114
|
+
List of dicts, each containing type, code, detail, trace_id, and context
|
|
115
|
+
for ContextException instances, or type and detail for plain exceptions.
|
|
116
|
+
"""
|
|
117
|
+
result = []
|
|
118
|
+
exc: Optional[Exception] = self
|
|
119
|
+
while exc is not None:
|
|
120
|
+
if isinstance(exc, ContextException):
|
|
121
|
+
result.append(
|
|
122
|
+
{
|
|
123
|
+
"type": exc.__class__.__name__,
|
|
124
|
+
"code": exc.code,
|
|
125
|
+
"detail": exc.detail,
|
|
126
|
+
"trace_id": exc.trace_id,
|
|
127
|
+
"context": exc.context,
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
exc = exc.cause
|
|
131
|
+
else:
|
|
132
|
+
result.append(
|
|
133
|
+
{
|
|
134
|
+
"type": exc.__class__.__name__,
|
|
135
|
+
"detail": str(exc),
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
exc = exc.__cause__ # type: ignore[assignment]
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def traceback(self) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Simplified Stack Trace (limit 4 frames)
|
|
145
|
+
Returns:
|
|
146
|
+
Stack trace string with frames joined by |, or "no traceback" if unavailable
|
|
147
|
+
"""
|
|
148
|
+
tb = self.__traceback__
|
|
149
|
+
if not tb:
|
|
150
|
+
return "no traceback"
|
|
151
|
+
lines = traceback.format_tb(tb, limit=4)
|
|
152
|
+
return " | ".join(line.strip() for line in lines if line.strip())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ==============================================
|
|
156
|
+
# 3. Common Error Class (business convenience factory methods)
|
|
157
|
+
# ==============================================
|
|
158
|
+
class BaseErrorCodes(ContextException):
|
|
159
|
+
"""
|
|
160
|
+
Common Error Class (business convenience layer)
|
|
161
|
+
Inherits from common capability class, providing factory methods for various predefined errors to simplify business exception throwing
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def invalid_input(cls, detail: str | None = None) -> "InvalidInputError":
|
|
166
|
+
"""
|
|
167
|
+
Input Validation Error (Error Code: 1001)
|
|
168
|
+
Args:
|
|
169
|
+
detail: Custom error prompt, use the default prompt of InvalidInputError if not provided
|
|
170
|
+
Returns:
|
|
171
|
+
InvalidInputError instance
|
|
172
|
+
"""
|
|
173
|
+
return InvalidInputError(detail)
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def not_found(cls, detail: str | None = None) -> "NotFoundError":
|
|
177
|
+
"""
|
|
178
|
+
Resource Not Found Error (Error Code: 3001)
|
|
179
|
+
Args:
|
|
180
|
+
detail: Custom error prompt, use the default prompt of NotFoundError if not provided
|
|
181
|
+
Returns:
|
|
182
|
+
NotFoundError instance
|
|
183
|
+
"""
|
|
184
|
+
return NotFoundError(detail)
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def access_denied(cls, detail: str | None = None) -> "AccessDeniedError":
|
|
188
|
+
"""
|
|
189
|
+
Access Denied Error (Error Code: 2001)
|
|
190
|
+
Args:
|
|
191
|
+
detail: Custom error prompt, use the default prompt of AccessDeniedError if not provided
|
|
192
|
+
Returns:
|
|
193
|
+
AccessDeniedError instance
|
|
194
|
+
"""
|
|
195
|
+
return AccessDeniedError(detail)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def configuration_error(cls, detail: str | None = None) -> "ConfigurationError":
|
|
199
|
+
"""
|
|
200
|
+
Configuration Error (Error Code: 5001)
|
|
201
|
+
Args:
|
|
202
|
+
detail: Custom error prompt, use the default prompt of ConfigurationError if not provided
|
|
203
|
+
Returns:
|
|
204
|
+
ConfigurationError instance
|
|
205
|
+
"""
|
|
206
|
+
return ConfigurationError(detail)
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def runtime_failure(cls, detail: str | None = None) -> "RuntimeFailure":
|
|
210
|
+
"""
|
|
211
|
+
Runtime Failure (Error Code: 4001)
|
|
212
|
+
Args:
|
|
213
|
+
detail: Custom error prompt, use the default prompt of RuntimeFailure if not provided
|
|
214
|
+
Returns:
|
|
215
|
+
RuntimeFailure instance
|
|
216
|
+
"""
|
|
217
|
+
return RuntimeFailure(detail)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def timeout_failure(cls, detail: str | None = None) -> "TimeoutFailure":
|
|
221
|
+
"""
|
|
222
|
+
Timeout Failure (Error Code: 4002)
|
|
223
|
+
Args:
|
|
224
|
+
detail: Custom error prompt, use the default prompt of TimeoutFailure if not provided
|
|
225
|
+
Returns:
|
|
226
|
+
TimeoutFailure instance
|
|
227
|
+
"""
|
|
228
|
+
return TimeoutFailure(detail)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ==============================================
|
|
232
|
+
# Specific Business Error Subclasses (unified naming and format, corresponding to common error codes)
|
|
233
|
+
# ==============================================
|
|
234
|
+
class InvalidInputError(BaseErrorCodes):
|
|
235
|
+
"""Input Validation Error (1001): Used for scenarios where parameter or input format validation fails"""
|
|
236
|
+
|
|
237
|
+
code = 1001
|
|
238
|
+
default_detail = "Invalid input."
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class NotFoundError(BaseErrorCodes):
|
|
242
|
+
"""Resource Not Found Error (3001): Used for scenarios where the queried resource does not exist"""
|
|
243
|
+
|
|
244
|
+
code = 3001
|
|
245
|
+
default_detail = "Resource not found."
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class AccessDeniedError(BaseErrorCodes):
|
|
249
|
+
"""Access Denied Error (2001): Used for scenarios where access to resources is not permitted"""
|
|
250
|
+
|
|
251
|
+
code = 2001
|
|
252
|
+
default_detail = "Access denied."
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ConfigurationError(BaseErrorCodes):
|
|
256
|
+
"""Configuration Error (5001): Used for scenarios where system or service configuration is abnormal"""
|
|
257
|
+
|
|
258
|
+
code = 5001
|
|
259
|
+
default_detail = "Configuration error."
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class RuntimeFailure(BaseErrorCodes):
|
|
263
|
+
"""Runtime Failure (4001): Used for unknown exception scenarios during system operation"""
|
|
264
|
+
|
|
265
|
+
code = 4001
|
|
266
|
+
default_detail = "Runtime failure."
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TimeoutFailure(BaseErrorCodes):
|
|
270
|
+
"""Timeout Failure (4002): Used for request or operation timeout scenarios"""
|
|
271
|
+
|
|
272
|
+
code = 4002
|
|
273
|
+
default_detail = "Operation timed out."
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Base and concrete group classes for collecting and raising exceptions together."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod, ABC
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BaseGroup",
|
|
8
|
+
"GroupErrors",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseGroup(Exception, ABC):
|
|
13
|
+
"""Abstract base for exception collector groups.
|
|
14
|
+
|
|
15
|
+
Defines the interface that all group implementations must satisfy:
|
|
16
|
+
`collect`, `raise_group`, `clear`, and the
|
|
17
|
+
`errors` property. Concrete subclasses decide the storage
|
|
18
|
+
strategy and the type of group they raise.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
group_msg: The message attached to the raised group.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, group_msg: str = "multiple errors") -> None:
|
|
25
|
+
"""Initialise the group with a message.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
group_msg: Message for the raised group.
|
|
29
|
+
Defaults to ``"multiple errors"``.
|
|
30
|
+
"""
|
|
31
|
+
self.group_msg = group_msg
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def errors(self) -> list[Exception]:
|
|
36
|
+
"""A copy of the collected exceptions."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def collect(self, exc: Exception) -> None:
|
|
40
|
+
"""Add *exc* to the group without raising it.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
exc: The exception instance to collect.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def clear(self) -> None:
|
|
48
|
+
"""Remove all collected exceptions."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def raise_group(self) -> None:
|
|
52
|
+
"""Raise all collected exceptions as a group.
|
|
53
|
+
|
|
54
|
+
Does nothing if no exceptions have been collected.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __len__(self) -> int:
|
|
58
|
+
"""Return the number of collected exceptions."""
|
|
59
|
+
return len(self.errors)
|
|
60
|
+
|
|
61
|
+
def __bool__(self) -> bool:
|
|
62
|
+
"""Return ``True`` if any exceptions have been collected."""
|
|
63
|
+
return bool(self.errors)
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return (
|
|
67
|
+
f"{type(self).__name__}(group_msg={self.group_msg!r}, errors={len(self)})"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if sys.version_info >= (3, 11):
|
|
72
|
+
|
|
73
|
+
class GroupErrors(BaseGroup):
|
|
74
|
+
"""A collector that accumulates exceptions and raises them as an ExceptionGroup.
|
|
75
|
+
|
|
76
|
+
Call `collect` to add exceptions one by one, then `raise_group`
|
|
77
|
+
to raise them all at once. Use `errors` to inspect what has been
|
|
78
|
+
collected without raising.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
|
|
82
|
+
>>> g = GroupErrors("validation failed")
|
|
83
|
+
>>> g.collect(TypeError("expected str"))
|
|
84
|
+
>>> g.collect(ValueError("value out of range"))
|
|
85
|
+
>>> g.raise_group()
|
|
86
|
+
Traceback (most recent call last):
|
|
87
|
+
...
|
|
88
|
+
ExceptionGroup: validation failed (2 sub-exceptions)
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, group_msg: str = "multiple errors") -> None:
|
|
92
|
+
super().__init__(group_msg)
|
|
93
|
+
self._errors: list[Exception] = []
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def errors(self) -> list[Exception]:
|
|
97
|
+
"""A copy of the collected exceptions."""
|
|
98
|
+
return list(self._errors)
|
|
99
|
+
|
|
100
|
+
def collect(self, exc: Exception) -> None:
|
|
101
|
+
"""Add *exc* to the group without raising it.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
exc: The exception instance to collect.
|
|
105
|
+
"""
|
|
106
|
+
self._errors.append(exc)
|
|
107
|
+
|
|
108
|
+
def clear(self) -> None:
|
|
109
|
+
"""Remove all collected exceptions."""
|
|
110
|
+
self._errors.clear()
|
|
111
|
+
|
|
112
|
+
def raise_group(self) -> None:
|
|
113
|
+
"""Raise all collected exceptions as an `ExceptionGroup`.
|
|
114
|
+
|
|
115
|
+
Does nothing if no exceptions have been collected.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ExceptionGroup: Containing every exception added via `collect`.
|
|
119
|
+
"""
|
|
120
|
+
if self._errors:
|
|
121
|
+
raise ExceptionGroup(self.group_msg, self._errors)
|