errortools 2.3.0__tar.gz → 2.4.0__tar.gz
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-2.3.0/errortools.egg-info → errortools-2.4.0}/PKG-INFO +26 -5
- {errortools-2.3.0 → errortools-2.4.0}/README.md +25 -4
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/const.py +6 -4
- errortools-2.4.0/_errortools/errno.py +86 -0
- errortools-2.4.0/_errortools/future.py +165 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/level.py +4 -4
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/version.py +2 -2
- {errortools-2.3.0 → errortools-2.4.0/errortools.egg-info}/PKG-INFO +26 -5
- {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/SOURCES.txt +1 -0
- {errortools-2.3.0 → errortools-2.4.0}/setup.py +1 -1
- {errortools-2.3.0 → errortools-2.4.0}/tests/__init__.py +1 -1
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_errno.py +5 -2
- errortools-2.4.0/tests/test_future.py +302 -0
- errortools-2.3.0/_errortools/errno.py +0 -78
- errortools-2.3.0/_errortools/future.py +0 -23
- {errortools-2.3.0 → errortools-2.4.0}/AUTHORS.txt +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/LICENSE.txt +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/_cli.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/abc.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/errorcodes.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/group.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/warn.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/cli.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/decorator/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/decorator/cache.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/decorator/deprecated.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/descriptor/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/descriptor/errormsg.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/descriptor/nonblankmsg.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/ignore.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/base.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/logger.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/record.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/sink.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/metadata.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errorattr.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errordelattr.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errorhasattr.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errorsetattr.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/partial.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/py.typed +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/raises.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/typing.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/wrappers/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/wrappers/cache.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/_errortools/wrappers/ignore.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/errortools/__init__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/errortools/__main__.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/dependency_links.txt +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/entry_points.txt +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/requires.txt +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/top_level.txt +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/setup.cfg +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/conftest.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/run_tests.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_abc.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_const.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_decorator.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_descriptor.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_errorcodes.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_groups.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_ignore.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_logging.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_mixins.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_partials.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_raises.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_typing.py +0 -0
- {errortools-2.3.0 → errortools-2.4.0}/tests/test_warnings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: errortools
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
|
|
5
5
|
Home-page: https://github.com/more-abc/errortools
|
|
6
6
|
Author: Evan Yang
|
|
@@ -39,6 +39,7 @@ A lightweight Python exception handling utility library.
|
|
|
39
39
|
## Features
|
|
40
40
|
- **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
|
|
41
41
|
- **Catch & Suppress**: `ignore()`, `ignore_subclass()`, `ignore_warns()`, `fast_ignore()`, `super_fast_ignore()`, `timeout()`, `retry()` — graceful suppression of exceptions and warnings, with automatic retry
|
|
42
|
+
- **Future Utilities**: `super_fast_catch()`, `super_fast_reraise()`, `ExceptionCollector` — lightweight exception handling for high-performance scenarios
|
|
42
43
|
- **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
|
|
43
44
|
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
|
|
44
45
|
- **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
|
|
@@ -62,7 +63,7 @@ from errortools import (
|
|
|
62
63
|
error_cache,
|
|
63
64
|
PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
|
|
64
65
|
)
|
|
65
|
-
from errortools.future import super_fast_ignore
|
|
66
|
+
from errortools.future import super_fast_ignore, super_fast_catch, super_fast_reraise, ExceptionCollector
|
|
66
67
|
|
|
67
68
|
# ── 1. ignore ── context manager with full metadata ──────────────────────────
|
|
68
69
|
with ignore(KeyError) as err:
|
|
@@ -154,7 +155,27 @@ raises_all(
|
|
|
154
155
|
exc = assert_raises(int, [ValueError], "not-a-number")
|
|
155
156
|
print(exc) # invalid literal for int() with base 10: 'not-a-number'
|
|
156
157
|
|
|
157
|
-
# ── 10.
|
|
158
|
+
# ── 10. super_fast_catch ── lightweight exception capture ──────────────────────
|
|
159
|
+
with super_fast_catch(ValueError) as ctx:
|
|
160
|
+
raise ValueError("oops")
|
|
161
|
+
|
|
162
|
+
assert ctx.exception is not None
|
|
163
|
+
print(ctx.exception) # ValueError('oops')
|
|
164
|
+
|
|
165
|
+
# ── 11. super_fast_reraise ── lightweight exception type conversion ────────────
|
|
166
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
167
|
+
raise KeyError("missing") # → ValueError: 'missing'
|
|
168
|
+
|
|
169
|
+
# ── 12. ExceptionCollector ── batch collect exceptions ───────────────────────────
|
|
170
|
+
collector = ExceptionCollector()
|
|
171
|
+
collector.catch(int, "bad1")
|
|
172
|
+
collector.catch(int, "bad2")
|
|
173
|
+
|
|
174
|
+
if collector.has_errors:
|
|
175
|
+
print(f"Collected {collector.count} errors")
|
|
176
|
+
collector.raise_all("batch operation failed") # → ExceptionGroup (2 sub-exceptions)
|
|
177
|
+
|
|
178
|
+
# ── 13. error_cache ── cache exceptions by call arguments ─────────────────────
|
|
158
179
|
@error_cache(maxsize=64)
|
|
159
180
|
def load(user_id: int) -> dict:
|
|
160
181
|
if user_id < 0:
|
|
@@ -167,7 +188,7 @@ with ignore(ValueError):
|
|
|
167
188
|
print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
|
|
168
189
|
load.clear_cache()
|
|
169
190
|
|
|
170
|
-
# ──
|
|
191
|
+
# ── 14. Custom exceptions — three layers ──────────────────────────────────────
|
|
171
192
|
|
|
172
193
|
# Layer 1: PureBaseException — code + detail only
|
|
173
194
|
class AppError(PureBaseException):
|
|
@@ -203,7 +224,7 @@ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [
|
|
|
203
224
|
raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
|
|
204
225
|
raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
|
|
205
226
|
|
|
206
|
-
# ──
|
|
227
|
+
# ── 15. BaseWarning ── structured warnings with factory methods ───────────────
|
|
207
228
|
class ExperimentalWarning(BaseWarning):
|
|
208
229
|
default_detail = "This feature is experimental."
|
|
209
230
|
|
|
@@ -4,6 +4,7 @@ A lightweight Python exception handling utility library.
|
|
|
4
4
|
## Features
|
|
5
5
|
- **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
|
|
6
6
|
- **Catch & Suppress**: `ignore()`, `ignore_subclass()`, `ignore_warns()`, `fast_ignore()`, `super_fast_ignore()`, `timeout()`, `retry()` — graceful suppression of exceptions and warnings, with automatic retry
|
|
7
|
+
- **Future Utilities**: `super_fast_catch()`, `super_fast_reraise()`, `ExceptionCollector` — lightweight exception handling for high-performance scenarios
|
|
7
8
|
- **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
|
|
8
9
|
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
|
|
9
10
|
- **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
|
|
@@ -27,7 +28,7 @@ from errortools import (
|
|
|
27
28
|
error_cache,
|
|
28
29
|
PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
|
|
29
30
|
)
|
|
30
|
-
from errortools.future import super_fast_ignore
|
|
31
|
+
from errortools.future import super_fast_ignore, super_fast_catch, super_fast_reraise, ExceptionCollector
|
|
31
32
|
|
|
32
33
|
# ── 1. ignore ── context manager with full metadata ──────────────────────────
|
|
33
34
|
with ignore(KeyError) as err:
|
|
@@ -119,7 +120,27 @@ raises_all(
|
|
|
119
120
|
exc = assert_raises(int, [ValueError], "not-a-number")
|
|
120
121
|
print(exc) # invalid literal for int() with base 10: 'not-a-number'
|
|
121
122
|
|
|
122
|
-
# ── 10.
|
|
123
|
+
# ── 10. super_fast_catch ── lightweight exception capture ──────────────────────
|
|
124
|
+
with super_fast_catch(ValueError) as ctx:
|
|
125
|
+
raise ValueError("oops")
|
|
126
|
+
|
|
127
|
+
assert ctx.exception is not None
|
|
128
|
+
print(ctx.exception) # ValueError('oops')
|
|
129
|
+
|
|
130
|
+
# ── 11. super_fast_reraise ── lightweight exception type conversion ────────────
|
|
131
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
132
|
+
raise KeyError("missing") # → ValueError: 'missing'
|
|
133
|
+
|
|
134
|
+
# ── 12. ExceptionCollector ── batch collect exceptions ───────────────────────────
|
|
135
|
+
collector = ExceptionCollector()
|
|
136
|
+
collector.catch(int, "bad1")
|
|
137
|
+
collector.catch(int, "bad2")
|
|
138
|
+
|
|
139
|
+
if collector.has_errors:
|
|
140
|
+
print(f"Collected {collector.count} errors")
|
|
141
|
+
collector.raise_all("batch operation failed") # → ExceptionGroup (2 sub-exceptions)
|
|
142
|
+
|
|
143
|
+
# ── 13. error_cache ── cache exceptions by call arguments ─────────────────────
|
|
123
144
|
@error_cache(maxsize=64)
|
|
124
145
|
def load(user_id: int) -> dict:
|
|
125
146
|
if user_id < 0:
|
|
@@ -132,7 +153,7 @@ with ignore(ValueError):
|
|
|
132
153
|
print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
|
|
133
154
|
load.clear_cache()
|
|
134
155
|
|
|
135
|
-
# ──
|
|
156
|
+
# ── 14. Custom exceptions — three layers ──────────────────────────────────────
|
|
136
157
|
|
|
137
158
|
# Layer 1: PureBaseException — code + detail only
|
|
138
159
|
class AppError(PureBaseException):
|
|
@@ -168,7 +189,7 @@ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [
|
|
|
168
189
|
raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
|
|
169
190
|
raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
|
|
170
191
|
|
|
171
|
-
# ──
|
|
192
|
+
# ── 15. BaseWarning ── structured warnings with factory methods ───────────────
|
|
172
193
|
class ExperimentalWarning(BaseWarning):
|
|
173
194
|
default_detail = "This feature is experimental."
|
|
174
195
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Constants for errortools module."""
|
|
2
2
|
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
3
5
|
# ------------------------------------------------------------------
|
|
4
6
|
# error_cache constants
|
|
5
7
|
# ------------------------------------------------------------------
|
|
6
8
|
|
|
7
|
-
DEFAULT_ERROR_CACHE_SIZE: int = 128
|
|
8
|
-
SMALL_ERROR_CACHE_SIZE: int = 64
|
|
9
|
-
LARGE_ERROR_CACHE_SIZE: int = 1024
|
|
10
|
-
UNLIMITED_ERROR_CACHE: None = None
|
|
9
|
+
DEFAULT_ERROR_CACHE_SIZE: Final[int] = 128
|
|
10
|
+
SMALL_ERROR_CACHE_SIZE: Final[int] = 64
|
|
11
|
+
LARGE_ERROR_CACHE_SIZE: Final[int] = 1024
|
|
12
|
+
UNLIMITED_ERROR_CACHE: Final[None] = None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Lightweight utilities for errno error code inspection and handling."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_errno_name(code: int) -> str | None:
|
|
7
|
+
"""Get the symbolic name for an errno code.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
code: The numeric errno code (e.g., 2 for ENOENT)
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
The symbolic errno name (e.g., "ENOENT") or None if not found
|
|
14
|
+
"""
|
|
15
|
+
for name in dir(errno):
|
|
16
|
+
if name.isupper() and getattr(errno, name) == code:
|
|
17
|
+
return name
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_errno_message(code: int) -> str:
|
|
22
|
+
"""Get the corresponding message description for an errno code.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
code: The numeric errno code
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The message string corresponding to the errno code
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If the given errno code is invalid
|
|
32
|
+
"""
|
|
33
|
+
if not is_valid_errno(code):
|
|
34
|
+
raise ValueError(f"Unknown error code: {code}")
|
|
35
|
+
|
|
36
|
+
return errno.errorcode.get(code, f"Unknown error {code}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_all_errno_codes() -> dict[str, int]:
|
|
40
|
+
"""Get a dictionary of all errno constant names and their numeric codes.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary mapping uppercase errno names to their integer values
|
|
44
|
+
"""
|
|
45
|
+
codes = {}
|
|
46
|
+
for name in dir(errno):
|
|
47
|
+
if name.isupper():
|
|
48
|
+
try:
|
|
49
|
+
value = getattr(errno, name)
|
|
50
|
+
if isinstance(value, int):
|
|
51
|
+
codes[name] = value
|
|
52
|
+
except (AttributeError, TypeError):
|
|
53
|
+
pass
|
|
54
|
+
return codes
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_valid_errno(code: int) -> bool:
|
|
58
|
+
"""Check whether a given integer is a valid system errno code.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
code: The numeric code to validate
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if the code corresponds to a known errno constant, False otherwise
|
|
65
|
+
"""
|
|
66
|
+
return get_errno_name(code) is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def strerror(code: int) -> str:
|
|
70
|
+
"""Get the human-readable system error message for an errno code.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
code: The numeric errno code
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Human-readable error message string
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If the error code is not recognized by the system
|
|
80
|
+
"""
|
|
81
|
+
import os
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
return os.strerror(code)
|
|
85
|
+
except (ValueError, OSError):
|
|
86
|
+
raise ValueError(f"Unknown error code: {code}")
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Future-focused lightweight exception handling utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeAlias, cast, Literal
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"super_fast_ignore",
|
|
9
|
+
"super_fast_catch",
|
|
10
|
+
"super_fast_reraise",
|
|
11
|
+
"ExceptionCollector",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
_ExcType: TypeAlias = type[BaseException]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class super_fast_ignore:
|
|
18
|
+
"""Ultra-lightweight context manager to suppress exceptions."""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("excs",)
|
|
21
|
+
|
|
22
|
+
def __init__(self, *excs: _ExcType) -> None:
|
|
23
|
+
self.excs = excs
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> None:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
def __exit__(self, typ: _ExcType | None, *_) -> bool:
|
|
29
|
+
if typ is None:
|
|
30
|
+
return False
|
|
31
|
+
excs = self.excs
|
|
32
|
+
return issubclass(typ, excs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class super_fast_catch:
|
|
36
|
+
"""Ultra-lightweight context manager to catch and store exceptions.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
*excs: Exception types to catch. Empty means catch all.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> with super_fast_catch(ValueError) as ctx:
|
|
43
|
+
... raise ValueError("oops")
|
|
44
|
+
>>> print(ctx.exception)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("excs", "exception")
|
|
48
|
+
|
|
49
|
+
def __init__(self, *excs: _ExcType) -> None:
|
|
50
|
+
self.excs = excs if excs else (BaseException,)
|
|
51
|
+
self.exception: BaseException | None = None
|
|
52
|
+
|
|
53
|
+
def __enter__(self) -> "super_fast_catch":
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __exit__(self, typ: _ExcType | None, val, *_) -> bool:
|
|
57
|
+
if typ is None or not issubclass(typ, self.excs):
|
|
58
|
+
return False
|
|
59
|
+
self.exception = val
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class super_fast_reraise:
|
|
64
|
+
"""Ultra-lightweight context manager to convert exception types.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
catch: Exception type(s) to intercept.
|
|
68
|
+
raise_as: Exception type to raise instead.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> with super_fast_reraise(KeyError, ValueError):
|
|
72
|
+
... raise KeyError("missing")
|
|
73
|
+
>>> # Raises ValueError: missing
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
__slots__ = ("catch", "raise_as")
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
catch: _ExcType | tuple[_ExcType, ...],
|
|
81
|
+
raise_as: _ExcType,
|
|
82
|
+
) -> None:
|
|
83
|
+
self.catch = catch if isinstance(catch, tuple) else (catch,)
|
|
84
|
+
self.raise_as = raise_as
|
|
85
|
+
|
|
86
|
+
def __enter__(self) -> None:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def __exit__(self, typ: _ExcType | None, val, *_) -> Literal[False]:
|
|
90
|
+
if typ is None or not issubclass(typ, self.catch):
|
|
91
|
+
return False
|
|
92
|
+
raise self.raise_as(str(val)) from val
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ExceptionCollector:
|
|
96
|
+
"""Collect multiple exceptions and raise together at the end.
|
|
97
|
+
|
|
98
|
+
Useful for batch operations where you want all errors, not just the first.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> collector = ExceptionCollector()
|
|
102
|
+
>>> with collector:
|
|
103
|
+
... collector.catch(int, "bad1")
|
|
104
|
+
... collector.catch(int, "bad2")
|
|
105
|
+
>>> if collector.has_errors:
|
|
106
|
+
... collector.raise_all()
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
__slots__ = ("_exceptions", "_stop_on_first")
|
|
110
|
+
|
|
111
|
+
def __init__(self, stop_on_first: bool = False) -> None:
|
|
112
|
+
self._exceptions: list[BaseException] = []
|
|
113
|
+
self._stop_on_first = stop_on_first
|
|
114
|
+
|
|
115
|
+
def __enter__(self) -> ExceptionCollector:
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __exit__(self, exc_typ, exc_val, *_) -> bool:
|
|
119
|
+
if exc_typ is not None:
|
|
120
|
+
self._exceptions.append(exc_val)
|
|
121
|
+
if self._stop_on_first:
|
|
122
|
+
return False
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
def catch(self, func, *args, **kwargs) -> bool:
|
|
127
|
+
"""Try to call func and catch any exception. Returns True if exception caught."""
|
|
128
|
+
try:
|
|
129
|
+
func(*args, **kwargs)
|
|
130
|
+
return False
|
|
131
|
+
except BaseException as exc:
|
|
132
|
+
self._exceptions.append(exc)
|
|
133
|
+
if self._stop_on_first:
|
|
134
|
+
raise
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def add(self, exc: BaseException) -> None:
|
|
138
|
+
"""Manually add an exception."""
|
|
139
|
+
self._exceptions.append(exc)
|
|
140
|
+
if self._stop_on_first:
|
|
141
|
+
raise exc
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def has_errors(self) -> bool:
|
|
145
|
+
"""Check if any exceptions were collected."""
|
|
146
|
+
return len(self._exceptions) > 0
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def count(self) -> int:
|
|
150
|
+
"""Get number of collected exceptions."""
|
|
151
|
+
return len(self._exceptions)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def exceptions(self) -> list[BaseException]:
|
|
155
|
+
"""Get the list of exceptions."""
|
|
156
|
+
return self._exceptions
|
|
157
|
+
|
|
158
|
+
def raise_all(self, message: str = "collected errors") -> None:
|
|
159
|
+
"""Raise all collected exceptions as ExceptionGroup."""
|
|
160
|
+
if self._exceptions:
|
|
161
|
+
raise ExceptionGroup(message, cast(list[Exception], self._exceptions))
|
|
162
|
+
|
|
163
|
+
def clear(self) -> None:
|
|
164
|
+
"""Clear all exceptions."""
|
|
165
|
+
self._exceptions.clear()
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import ClassVar
|
|
6
|
+
from typing import ClassVar, Final
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@dataclass(frozen=True)
|
|
@@ -55,7 +55,7 @@ Level.CRITICAL = Level("CRITICAL", 50, "\033[1;31m", "☠") # bold red
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
# Ordered list for iteration / lookup
|
|
58
|
-
LEVELS: tuple[Level, ...] = (
|
|
58
|
+
LEVELS: Final[tuple[Level, ...]] = (
|
|
59
59
|
Level.TRACE,
|
|
60
60
|
Level.DEBUG,
|
|
61
61
|
Level.INFO,
|
|
@@ -65,8 +65,8 @@ LEVELS: tuple[Level, ...] = (
|
|
|
65
65
|
Level.CRITICAL,
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
_NAME_MAP: dict[str, Level] = {lv.name: lv for lv in LEVELS}
|
|
69
|
-
_NO_MAP: dict[int, Level] = {lv.no: lv for lv in LEVELS}
|
|
68
|
+
_NAME_MAP: Final[dict[str, Level]] = {lv.name: lv for lv in LEVELS}
|
|
69
|
+
_NO_MAP: Final[dict[int, Level]] = {lv.no: lv for lv in LEVELS}
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def get_level(name_or_no: str | int) -> Level:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: errortools
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
|
|
5
5
|
Home-page: https://github.com/more-abc/errortools
|
|
6
6
|
Author: Evan Yang
|
|
@@ -39,6 +39,7 @@ A lightweight Python exception handling utility library.
|
|
|
39
39
|
## Features
|
|
40
40
|
- **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
|
|
41
41
|
- **Catch & Suppress**: `ignore()`, `ignore_subclass()`, `ignore_warns()`, `fast_ignore()`, `super_fast_ignore()`, `timeout()`, `retry()` — graceful suppression of exceptions and warnings, with automatic retry
|
|
42
|
+
- **Future Utilities**: `super_fast_catch()`, `super_fast_reraise()`, `ExceptionCollector` — lightweight exception handling for high-performance scenarios
|
|
42
43
|
- **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
|
|
43
44
|
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
|
|
44
45
|
- **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
|
|
@@ -62,7 +63,7 @@ from errortools import (
|
|
|
62
63
|
error_cache,
|
|
63
64
|
PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
|
|
64
65
|
)
|
|
65
|
-
from errortools.future import super_fast_ignore
|
|
66
|
+
from errortools.future import super_fast_ignore, super_fast_catch, super_fast_reraise, ExceptionCollector
|
|
66
67
|
|
|
67
68
|
# ── 1. ignore ── context manager with full metadata ──────────────────────────
|
|
68
69
|
with ignore(KeyError) as err:
|
|
@@ -154,7 +155,27 @@ raises_all(
|
|
|
154
155
|
exc = assert_raises(int, [ValueError], "not-a-number")
|
|
155
156
|
print(exc) # invalid literal for int() with base 10: 'not-a-number'
|
|
156
157
|
|
|
157
|
-
# ── 10.
|
|
158
|
+
# ── 10. super_fast_catch ── lightweight exception capture ──────────────────────
|
|
159
|
+
with super_fast_catch(ValueError) as ctx:
|
|
160
|
+
raise ValueError("oops")
|
|
161
|
+
|
|
162
|
+
assert ctx.exception is not None
|
|
163
|
+
print(ctx.exception) # ValueError('oops')
|
|
164
|
+
|
|
165
|
+
# ── 11. super_fast_reraise ── lightweight exception type conversion ────────────
|
|
166
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
167
|
+
raise KeyError("missing") # → ValueError: 'missing'
|
|
168
|
+
|
|
169
|
+
# ── 12. ExceptionCollector ── batch collect exceptions ───────────────────────────
|
|
170
|
+
collector = ExceptionCollector()
|
|
171
|
+
collector.catch(int, "bad1")
|
|
172
|
+
collector.catch(int, "bad2")
|
|
173
|
+
|
|
174
|
+
if collector.has_errors:
|
|
175
|
+
print(f"Collected {collector.count} errors")
|
|
176
|
+
collector.raise_all("batch operation failed") # → ExceptionGroup (2 sub-exceptions)
|
|
177
|
+
|
|
178
|
+
# ── 13. error_cache ── cache exceptions by call arguments ─────────────────────
|
|
158
179
|
@error_cache(maxsize=64)
|
|
159
180
|
def load(user_id: int) -> dict:
|
|
160
181
|
if user_id < 0:
|
|
@@ -167,7 +188,7 @@ with ignore(ValueError):
|
|
|
167
188
|
print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
|
|
168
189
|
load.clear_cache()
|
|
169
190
|
|
|
170
|
-
# ──
|
|
191
|
+
# ── 14. Custom exceptions — three layers ──────────────────────────────────────
|
|
171
192
|
|
|
172
193
|
# Layer 1: PureBaseException — code + detail only
|
|
173
194
|
class AppError(PureBaseException):
|
|
@@ -203,7 +224,7 @@ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [
|
|
|
203
224
|
raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
|
|
204
225
|
raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
|
|
205
226
|
|
|
206
|
-
# ──
|
|
227
|
+
# ── 15. BaseWarning ── structured warnings with factory methods ───────────────
|
|
207
228
|
class ExperimentalWarning(BaseWarning):
|
|
208
229
|
default_detail = "This feature is experimental."
|
|
209
230
|
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="errortools",
|
|
5
|
-
version="2.
|
|
5
|
+
version="2.4.0",
|
|
6
6
|
description="errortools - a toolset for working with Python exceptions and warnings and logging.",
|
|
7
7
|
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import errno
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
5
7
|
from _errortools.errno import (
|
|
6
8
|
get_errno_name,
|
|
7
9
|
get_errno_message,
|
|
@@ -37,8 +39,9 @@ class TestGetErrnoMessage:
|
|
|
37
39
|
assert len(message) > 0
|
|
38
40
|
|
|
39
41
|
def test_invalid_errno_message(self):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
with pytest.raises(ValueError):
|
|
43
|
+
message = get_errno_message(9999)
|
|
44
|
+
assert "Unknown error" in message or len(message) > 0
|
|
42
45
|
|
|
43
46
|
def test_errno_two_message(self):
|
|
44
47
|
message = get_errno_message(2)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Tests for _errortools/future.py — super_fast_ignore, super_fast_catch, super_fast_reraise, ExceptionCollector."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from _errortools.future import (
|
|
6
|
+
super_fast_ignore,
|
|
7
|
+
super_fast_catch,
|
|
8
|
+
super_fast_reraise,
|
|
9
|
+
ExceptionCollector,
|
|
10
|
+
)
|
|
11
|
+
from . import HAS_PYTEST
|
|
12
|
+
|
|
13
|
+
if not HAS_PYTEST:
|
|
14
|
+
print("pytest is required to run these tests, skip run test_future.py")
|
|
15
|
+
exit(0)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# super_fast_ignore
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestSuperFastIgnore:
|
|
24
|
+
"""Tests for super_fast_ignore context manager."""
|
|
25
|
+
|
|
26
|
+
def test_suppresses_single_exception(self):
|
|
27
|
+
"""Test that super_fast_ignore suppresses a single exception type."""
|
|
28
|
+
with super_fast_ignore(ValueError):
|
|
29
|
+
raise ValueError("ignored")
|
|
30
|
+
|
|
31
|
+
def test_suppresses_multiple_exception_types(self):
|
|
32
|
+
"""Test that super_fast_ignore suppresses multiple exception types."""
|
|
33
|
+
with super_fast_ignore(ValueError, KeyError):
|
|
34
|
+
raise KeyError("ignored")
|
|
35
|
+
|
|
36
|
+
def test_unrelated_exception_propagates(self):
|
|
37
|
+
"""Test that unrelated exceptions are not suppressed."""
|
|
38
|
+
with pytest.raises(RuntimeError):
|
|
39
|
+
with super_fast_ignore(ValueError):
|
|
40
|
+
raise RuntimeError("not suppressed")
|
|
41
|
+
|
|
42
|
+
def test_no_exception_in_block(self):
|
|
43
|
+
"""Test that block with no exception executes normally."""
|
|
44
|
+
result = []
|
|
45
|
+
with super_fast_ignore(ValueError):
|
|
46
|
+
result.append(1)
|
|
47
|
+
assert result == [1]
|
|
48
|
+
|
|
49
|
+
def test_execution_continues_after_suppression(self):
|
|
50
|
+
"""Test that execution continues after exception is suppressed."""
|
|
51
|
+
items = []
|
|
52
|
+
with super_fast_ignore(ValueError):
|
|
53
|
+
raise ValueError("ignored")
|
|
54
|
+
items.append("after")
|
|
55
|
+
assert items == ["after"]
|
|
56
|
+
|
|
57
|
+
def test_suppresses_exception_subclass(self):
|
|
58
|
+
"""Test that subclasses of specified exceptions are suppressed."""
|
|
59
|
+
with super_fast_ignore(LookupError):
|
|
60
|
+
raise KeyError("subclass of LookupError")
|
|
61
|
+
|
|
62
|
+
def test_nested_suppress(self):
|
|
63
|
+
"""Test nested super_fast_ignore context managers."""
|
|
64
|
+
with super_fast_ignore(ValueError):
|
|
65
|
+
with super_fast_ignore(KeyError):
|
|
66
|
+
raise KeyError("inner")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# super_fast_catch
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestSuperFastCatch:
|
|
75
|
+
"""Tests for super_fast_catch context manager."""
|
|
76
|
+
|
|
77
|
+
def test_catches_single_exception_type(self):
|
|
78
|
+
"""Test that super_fast_catch catches a specified exception type."""
|
|
79
|
+
with super_fast_catch(ValueError) as ctx:
|
|
80
|
+
raise ValueError("caught")
|
|
81
|
+
assert ctx.exception is not None
|
|
82
|
+
assert isinstance(ctx.exception, ValueError)
|
|
83
|
+
assert str(ctx.exception) == "caught"
|
|
84
|
+
|
|
85
|
+
def test_catches_multiple_exception_types(self):
|
|
86
|
+
"""Test that super_fast_catch can catch multiple exception types."""
|
|
87
|
+
with super_fast_catch(ValueError, KeyError) as ctx:
|
|
88
|
+
raise KeyError("caught")
|
|
89
|
+
assert isinstance(ctx.exception, KeyError)
|
|
90
|
+
|
|
91
|
+
def test_catches_exception_subclass(self):
|
|
92
|
+
"""Test that subclasses of specified exceptions are caught."""
|
|
93
|
+
with super_fast_catch(LookupError) as ctx:
|
|
94
|
+
raise KeyError("subclass")
|
|
95
|
+
assert isinstance(ctx.exception, KeyError)
|
|
96
|
+
|
|
97
|
+
def test_no_exception_stored_when_none_raised(self):
|
|
98
|
+
"""Test that exception is None when no exception is raised."""
|
|
99
|
+
with super_fast_catch(ValueError) as ctx:
|
|
100
|
+
pass
|
|
101
|
+
assert ctx.exception is None
|
|
102
|
+
|
|
103
|
+
def test_unrelated_exception_propagates(self):
|
|
104
|
+
"""Test that unrelated exceptions propagate."""
|
|
105
|
+
with pytest.raises(RuntimeError):
|
|
106
|
+
with super_fast_catch(ValueError) as ctx:
|
|
107
|
+
raise RuntimeError("not caught")
|
|
108
|
+
|
|
109
|
+
def test_catch_all_when_no_args(self):
|
|
110
|
+
"""Test that super_fast_catch catches all exceptions when given no args."""
|
|
111
|
+
with super_fast_catch() as ctx:
|
|
112
|
+
raise RuntimeError("caught all")
|
|
113
|
+
assert ctx.exception is not None
|
|
114
|
+
assert isinstance(ctx.exception, RuntimeError)
|
|
115
|
+
|
|
116
|
+
def test_returns_self(self):
|
|
117
|
+
"""Test that super_fast_catch returns itself."""
|
|
118
|
+
with super_fast_catch(ValueError) as ctx:
|
|
119
|
+
assert isinstance(ctx, super_fast_catch)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# =============================================================================
|
|
123
|
+
# super_fast_reraise
|
|
124
|
+
# =============================================================================
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestSuperFastReraise:
|
|
128
|
+
"""Tests for super_fast_reraise context manager."""
|
|
129
|
+
|
|
130
|
+
def test_converts_exception_type(self):
|
|
131
|
+
"""Test that super_fast_reraise converts exception types."""
|
|
132
|
+
with pytest.raises(ValueError) as exc_info:
|
|
133
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
134
|
+
raise KeyError("original")
|
|
135
|
+
assert "'original'" in str(exc_info.value)
|
|
136
|
+
assert exc_info.value.__cause__.__class__ == KeyError
|
|
137
|
+
|
|
138
|
+
def test_preserves_exception_message(self):
|
|
139
|
+
"""Test that reraise preserves the original exception message."""
|
|
140
|
+
msg = "test message"
|
|
141
|
+
with pytest.raises(ValueError) as exc_info:
|
|
142
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
143
|
+
raise KeyError(msg)
|
|
144
|
+
assert msg in str(exc_info.value)
|
|
145
|
+
|
|
146
|
+
def test_catches_multiple_types(self):
|
|
147
|
+
"""Test that super_fast_reraise can catch multiple exception types."""
|
|
148
|
+
with pytest.raises(RuntimeError):
|
|
149
|
+
with super_fast_reraise((KeyError, ValueError), RuntimeError):
|
|
150
|
+
raise ValueError("caught")
|
|
151
|
+
|
|
152
|
+
def test_unrelated_exception_propagates(self):
|
|
153
|
+
"""Test that unrelated exceptions propagate unchanged."""
|
|
154
|
+
with pytest.raises(TypeError):
|
|
155
|
+
with super_fast_reraise(ValueError, KeyError):
|
|
156
|
+
raise TypeError("not caught")
|
|
157
|
+
|
|
158
|
+
def test_chains_exceptions(self):
|
|
159
|
+
"""Test that original exception is chained with __cause__."""
|
|
160
|
+
with pytest.raises(ValueError) as exc_info:
|
|
161
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
162
|
+
raise KeyError("original")
|
|
163
|
+
assert exc_info.value.__cause__ is not None
|
|
164
|
+
assert isinstance(exc_info.value.__cause__, KeyError)
|
|
165
|
+
|
|
166
|
+
def test_no_exception_in_block(self):
|
|
167
|
+
"""Test that block with no exception executes normally."""
|
|
168
|
+
with super_fast_reraise(ValueError, KeyError):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# =============================================================================
|
|
173
|
+
# ExceptionCollector
|
|
174
|
+
# =============================================================================
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestExceptionCollector:
|
|
178
|
+
"""Tests for ExceptionCollector context manager."""
|
|
179
|
+
|
|
180
|
+
def test_collects_single_exception(self):
|
|
181
|
+
"""Test that ExceptionCollector collects a single exception."""
|
|
182
|
+
collector = ExceptionCollector()
|
|
183
|
+
collector.catch(int, "not-a-number")
|
|
184
|
+
assert collector.has_errors
|
|
185
|
+
assert collector.count == 1
|
|
186
|
+
assert len(collector.exceptions) == 1
|
|
187
|
+
|
|
188
|
+
def test_collects_multiple_exceptions(self):
|
|
189
|
+
"""Test that ExceptionCollector collects multiple exceptions."""
|
|
190
|
+
collector = ExceptionCollector()
|
|
191
|
+
collector.catch(int, "bad1")
|
|
192
|
+
collector.catch(int, "bad2")
|
|
193
|
+
collector.catch(int, "bad3")
|
|
194
|
+
assert collector.count == 3
|
|
195
|
+
|
|
196
|
+
def test_catch_returns_false_on_success(self):
|
|
197
|
+
"""Test that catch returns False when no exception is raised."""
|
|
198
|
+
collector = ExceptionCollector()
|
|
199
|
+
result = collector.catch(int, "123")
|
|
200
|
+
assert result is False
|
|
201
|
+
|
|
202
|
+
def test_catch_returns_true_on_exception(self):
|
|
203
|
+
"""Test that catch returns True when exception is raised."""
|
|
204
|
+
collector = ExceptionCollector()
|
|
205
|
+
result = collector.catch(int, "not-a-number")
|
|
206
|
+
assert result is True
|
|
207
|
+
|
|
208
|
+
def test_add_method(self):
|
|
209
|
+
"""Test that add method adds exceptions manually."""
|
|
210
|
+
collector = ExceptionCollector()
|
|
211
|
+
exc = ValueError("test")
|
|
212
|
+
collector.add(exc)
|
|
213
|
+
assert collector.count == 1
|
|
214
|
+
assert collector.exceptions[0] is exc
|
|
215
|
+
|
|
216
|
+
def test_has_errors_property(self):
|
|
217
|
+
"""Test has_errors property."""
|
|
218
|
+
collector = ExceptionCollector()
|
|
219
|
+
assert not collector.has_errors
|
|
220
|
+
collector.catch(int, "bad")
|
|
221
|
+
assert collector.has_errors
|
|
222
|
+
|
|
223
|
+
def test_clear_method(self):
|
|
224
|
+
"""Test that clear removes all exceptions."""
|
|
225
|
+
collector = ExceptionCollector()
|
|
226
|
+
collector.catch(int, "bad1")
|
|
227
|
+
collector.catch(int, "bad2")
|
|
228
|
+
assert collector.count == 2
|
|
229
|
+
collector.clear()
|
|
230
|
+
assert collector.count == 0
|
|
231
|
+
assert not collector.has_errors
|
|
232
|
+
|
|
233
|
+
def test_raise_all_with_errors(self):
|
|
234
|
+
"""Test that raise_all raises ExceptionGroup when errors collected."""
|
|
235
|
+
collector = ExceptionCollector()
|
|
236
|
+
collector.catch(int, "bad1")
|
|
237
|
+
collector.catch(int, "bad2")
|
|
238
|
+
with pytest.raises(ExceptionGroup) as exc_info:
|
|
239
|
+
collector.raise_all()
|
|
240
|
+
assert exc_info.value.message == "collected errors"
|
|
241
|
+
assert len(exc_info.value.exceptions) == 2
|
|
242
|
+
|
|
243
|
+
def test_raise_all_with_custom_message(self):
|
|
244
|
+
"""Test that raise_all uses custom message."""
|
|
245
|
+
collector = ExceptionCollector()
|
|
246
|
+
collector.catch(int, "bad")
|
|
247
|
+
with pytest.raises(ExceptionGroup) as exc_info:
|
|
248
|
+
collector.raise_all("custom error message")
|
|
249
|
+
assert exc_info.value.message == "custom error message"
|
|
250
|
+
|
|
251
|
+
def test_raise_all_without_errors(self):
|
|
252
|
+
"""Test that raise_all does nothing when no errors collected."""
|
|
253
|
+
collector = ExceptionCollector()
|
|
254
|
+
collector.raise_all()
|
|
255
|
+
|
|
256
|
+
def test_context_manager_usage(self):
|
|
257
|
+
"""Test ExceptionCollector as context manager."""
|
|
258
|
+
with ExceptionCollector() as collector:
|
|
259
|
+
collector.catch(int, "bad1")
|
|
260
|
+
collector.catch(int, "bad2")
|
|
261
|
+
assert collector.count == 2
|
|
262
|
+
|
|
263
|
+
def test_context_manager_with_exception_in_block(self):
|
|
264
|
+
"""Test context manager when exception raised in block."""
|
|
265
|
+
with ExceptionCollector() as collector:
|
|
266
|
+
raise ValueError("in block")
|
|
267
|
+
assert collector.count == 1
|
|
268
|
+
assert isinstance(collector.exceptions[0], ValueError)
|
|
269
|
+
|
|
270
|
+
def test_stop_on_first_mode(self):
|
|
271
|
+
"""Test that stop_on_first=True raises immediately."""
|
|
272
|
+
collector = ExceptionCollector(stop_on_first=True)
|
|
273
|
+
with pytest.raises(ValueError):
|
|
274
|
+
collector.catch(int, "bad")
|
|
275
|
+
assert collector.count == 1
|
|
276
|
+
|
|
277
|
+
def test_stop_on_first_add_method(self):
|
|
278
|
+
"""Test that add method respects stop_on_first."""
|
|
279
|
+
collector = ExceptionCollector(stop_on_first=True)
|
|
280
|
+
with pytest.raises(RuntimeError):
|
|
281
|
+
collector.add(RuntimeError("test"))
|
|
282
|
+
|
|
283
|
+
def test_exceptions_property(self):
|
|
284
|
+
"""Test that exceptions property returns list of exceptions."""
|
|
285
|
+
collector = ExceptionCollector()
|
|
286
|
+
exc1 = ValueError("test1")
|
|
287
|
+
exc2 = TypeError("test2")
|
|
288
|
+
collector.add(exc1)
|
|
289
|
+
collector.add(exc2)
|
|
290
|
+
exceptions = collector.exceptions
|
|
291
|
+
assert len(exceptions) == 2
|
|
292
|
+
assert exceptions[0] is exc1
|
|
293
|
+
assert exceptions[1] is exc2
|
|
294
|
+
|
|
295
|
+
def test_count_property(self):
|
|
296
|
+
"""Test that count property returns correct number."""
|
|
297
|
+
collector = ExceptionCollector()
|
|
298
|
+
assert collector.count == 0
|
|
299
|
+
collector.catch(int, "bad1")
|
|
300
|
+
assert collector.count == 1
|
|
301
|
+
collector.catch(int, "bad2")
|
|
302
|
+
assert collector.count == 2
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import errno
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def get_errno_name(code: int) -> str | None:
|
|
5
|
-
"""Get the symbolic name for an errno code.
|
|
6
|
-
|
|
7
|
-
Args:
|
|
8
|
-
code: The errno code (e.g., 2 for ENOENT)
|
|
9
|
-
|
|
10
|
-
Returns:
|
|
11
|
-
The errno name (e.g., "ENOENT") or None if not found
|
|
12
|
-
"""
|
|
13
|
-
for name in dir(errno):
|
|
14
|
-
if name.isupper() and getattr(errno, name) == code:
|
|
15
|
-
return name
|
|
16
|
-
return None
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def get_errno_message(code: int) -> str:
|
|
20
|
-
"""Get the error message for an errno code.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
code: The errno code
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
The error message string
|
|
27
|
-
"""
|
|
28
|
-
try:
|
|
29
|
-
return errno.errorcode.get(code, f"Unknown error {code}")
|
|
30
|
-
except (AttributeError, KeyError):
|
|
31
|
-
return f"Unknown error {code}"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def get_all_errno_codes() -> dict[str, int]:
|
|
35
|
-
"""Get a mapping of all errno names to their numeric codes.
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
Dictionary mapping errno names to their numeric values
|
|
39
|
-
"""
|
|
40
|
-
codes = {}
|
|
41
|
-
for name in dir(errno):
|
|
42
|
-
if name.isupper():
|
|
43
|
-
try:
|
|
44
|
-
value = getattr(errno, name)
|
|
45
|
-
if isinstance(value, int):
|
|
46
|
-
codes[name] = value
|
|
47
|
-
except (AttributeError, TypeError):
|
|
48
|
-
pass
|
|
49
|
-
return codes
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def is_valid_errno(code: int) -> bool:
|
|
53
|
-
"""Check if a code is a valid errno constant.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
code: The code to check
|
|
57
|
-
|
|
58
|
-
Returns:
|
|
59
|
-
True if the code is a valid errno, False otherwise
|
|
60
|
-
"""
|
|
61
|
-
return get_errno_name(code) is not None
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def strerror(code: int) -> str:
|
|
65
|
-
"""Get human-readable error message for errno code.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
code: The errno code
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
Error message string
|
|
72
|
-
"""
|
|
73
|
-
import os
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
return os.strerror(code)
|
|
77
|
-
except (ValueError, OSError):
|
|
78
|
-
return f"Unknown error {code}"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Futures classes and functions."""
|
|
2
|
-
|
|
3
|
-
from typing import TypeAlias
|
|
4
|
-
|
|
5
|
-
_ExcType: TypeAlias = type[BaseException]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class super_fast_ignore:
|
|
9
|
-
"""Ultra-lightweight context manager to suppress exceptions."""
|
|
10
|
-
|
|
11
|
-
__slots__ = ("excs",)
|
|
12
|
-
|
|
13
|
-
def __init__(self, *excs: _ExcType) -> None:
|
|
14
|
-
self.excs = excs
|
|
15
|
-
|
|
16
|
-
def __enter__(self) -> None:
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
def __exit__(self, typ: _ExcType | None, *_) -> bool:
|
|
20
|
-
if typ is None:
|
|
21
|
-
return False
|
|
22
|
-
excs = self.excs
|
|
23
|
-
return issubclass(typ, excs)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|