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.
Files changed (64) hide show
  1. _errortools/__init__.py +1 -0
  2. _errortools/_cli.py +33 -0
  3. _errortools/_speedup.c +58 -0
  4. _errortools/_speedup.cp314-win_amd64.pyd +0 -0
  5. _errortools/classes/__init__.py +1 -0
  6. _errortools/classes/abc.py +211 -0
  7. _errortools/classes/errorcodes.py +273 -0
  8. _errortools/classes/group.py +121 -0
  9. _errortools/classes/warn.py +124 -0
  10. _errortools/cli.py +108 -0
  11. _errortools/const.py +12 -0
  12. _errortools/decorator/__init__.py +1 -0
  13. _errortools/decorator/cache.py +82 -0
  14. _errortools/decorator/deprecated.py +61 -0
  15. _errortools/descriptor/__init__.py +2 -0
  16. _errortools/descriptor/errormsg.py +37 -0
  17. _errortools/descriptor/nonblankmsg.py +52 -0
  18. _errortools/errno.py +86 -0
  19. _errortools/future.py +179 -0
  20. _errortools/ignore.py +288 -0
  21. _errortools/logging/__init__.py +43 -0
  22. _errortools/logging/base.py +467 -0
  23. _errortools/logging/level.py +85 -0
  24. _errortools/logging/logger.py +13 -0
  25. _errortools/logging/record.py +116 -0
  26. _errortools/logging/sink.py +243 -0
  27. _errortools/metadata.py +16 -0
  28. _errortools/partial.py +199 -0
  29. _errortools/py.typed +0 -0
  30. _errortools/raises.py +186 -0
  31. _errortools/typing.py +101 -0
  32. _errortools/version.py +7 -0
  33. _errortools/wrappers/__init__.py +2 -0
  34. _errortools/wrappers/cache.py +101 -0
  35. _errortools/wrappers/ignore.py +122 -0
  36. errortools/__init__.py +167 -0
  37. errortools/__main__.py +4 -0
  38. errortools/future.py +5 -0
  39. errortools/logging.py +46 -0
  40. errortools/partial.py +5 -0
  41. errortools-3.0.0.dist-info/METADATA +366 -0
  42. errortools-3.0.0.dist-info/RECORD +64 -0
  43. errortools-3.0.0.dist-info/WHEEL +5 -0
  44. errortools-3.0.0.dist-info/entry_points.txt +3 -0
  45. errortools-3.0.0.dist-info/licenses/AUTHORS.txt +3 -0
  46. errortools-3.0.0.dist-info/licenses/LICENSE.txt +20 -0
  47. errortools-3.0.0.dist-info/top_level.txt +3 -0
  48. tests/__init__.py +12 -0
  49. tests/conftest.py +14 -0
  50. tests/run_tests.py +19 -0
  51. tests/test_abc.py +300 -0
  52. tests/test_const.py +28 -0
  53. tests/test_decorator.py +361 -0
  54. tests/test_descriptor.py +150 -0
  55. tests/test_errno.py +110 -0
  56. tests/test_errorcodes.py +395 -0
  57. tests/test_future.py +296 -0
  58. tests/test_groups.py +128 -0
  59. tests/test_ignore.py +412 -0
  60. tests/test_logging.py +674 -0
  61. tests/test_partials.py +228 -0
  62. tests/test_raises.py +175 -0
  63. tests/test_typing.py +152 -0
  64. tests/test_warnings.py +151 -0
@@ -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)