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.
Files changed (72) hide show
  1. {errortools-2.3.0/errortools.egg-info → errortools-2.4.0}/PKG-INFO +26 -5
  2. {errortools-2.3.0 → errortools-2.4.0}/README.md +25 -4
  3. {errortools-2.3.0 → errortools-2.4.0}/_errortools/const.py +6 -4
  4. errortools-2.4.0/_errortools/errno.py +86 -0
  5. errortools-2.4.0/_errortools/future.py +165 -0
  6. {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/level.py +4 -4
  7. {errortools-2.3.0 → errortools-2.4.0}/_errortools/version.py +2 -2
  8. {errortools-2.3.0 → errortools-2.4.0/errortools.egg-info}/PKG-INFO +26 -5
  9. {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/SOURCES.txt +1 -0
  10. {errortools-2.3.0 → errortools-2.4.0}/setup.py +1 -1
  11. {errortools-2.3.0 → errortools-2.4.0}/tests/__init__.py +1 -1
  12. {errortools-2.3.0 → errortools-2.4.0}/tests/test_errno.py +5 -2
  13. errortools-2.4.0/tests/test_future.py +302 -0
  14. errortools-2.3.0/_errortools/errno.py +0 -78
  15. errortools-2.3.0/_errortools/future.py +0 -23
  16. {errortools-2.3.0 → errortools-2.4.0}/AUTHORS.txt +0 -0
  17. {errortools-2.3.0 → errortools-2.4.0}/LICENSE.txt +0 -0
  18. {errortools-2.3.0 → errortools-2.4.0}/_errortools/__init__.py +0 -0
  19. {errortools-2.3.0 → errortools-2.4.0}/_errortools/_cli.py +0 -0
  20. {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/__init__.py +0 -0
  21. {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/abc.py +0 -0
  22. {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/errorcodes.py +0 -0
  23. {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/group.py +0 -0
  24. {errortools-2.3.0 → errortools-2.4.0}/_errortools/classes/warn.py +0 -0
  25. {errortools-2.3.0 → errortools-2.4.0}/_errortools/cli.py +0 -0
  26. {errortools-2.3.0 → errortools-2.4.0}/_errortools/decorator/__init__.py +0 -0
  27. {errortools-2.3.0 → errortools-2.4.0}/_errortools/decorator/cache.py +0 -0
  28. {errortools-2.3.0 → errortools-2.4.0}/_errortools/decorator/deprecated.py +0 -0
  29. {errortools-2.3.0 → errortools-2.4.0}/_errortools/descriptor/__init__.py +0 -0
  30. {errortools-2.3.0 → errortools-2.4.0}/_errortools/descriptor/errormsg.py +0 -0
  31. {errortools-2.3.0 → errortools-2.4.0}/_errortools/descriptor/nonblankmsg.py +0 -0
  32. {errortools-2.3.0 → errortools-2.4.0}/_errortools/ignore.py +0 -0
  33. {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/__init__.py +0 -0
  34. {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/base.py +0 -0
  35. {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/logger.py +0 -0
  36. {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/record.py +0 -0
  37. {errortools-2.3.0 → errortools-2.4.0}/_errortools/logging/sink.py +0 -0
  38. {errortools-2.3.0 → errortools-2.4.0}/_errortools/metadata.py +0 -0
  39. {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/__init__.py +0 -0
  40. {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errorattr.py +0 -0
  41. {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errordelattr.py +0 -0
  42. {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errorhasattr.py +0 -0
  43. {errortools-2.3.0 → errortools-2.4.0}/_errortools/methods/errorsetattr.py +0 -0
  44. {errortools-2.3.0 → errortools-2.4.0}/_errortools/partial.py +0 -0
  45. {errortools-2.3.0 → errortools-2.4.0}/_errortools/py.typed +0 -0
  46. {errortools-2.3.0 → errortools-2.4.0}/_errortools/raises.py +0 -0
  47. {errortools-2.3.0 → errortools-2.4.0}/_errortools/typing.py +0 -0
  48. {errortools-2.3.0 → errortools-2.4.0}/_errortools/wrappers/__init__.py +0 -0
  49. {errortools-2.3.0 → errortools-2.4.0}/_errortools/wrappers/cache.py +0 -0
  50. {errortools-2.3.0 → errortools-2.4.0}/_errortools/wrappers/ignore.py +0 -0
  51. {errortools-2.3.0 → errortools-2.4.0}/errortools/__init__.py +0 -0
  52. {errortools-2.3.0 → errortools-2.4.0}/errortools/__main__.py +0 -0
  53. {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/dependency_links.txt +0 -0
  54. {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/entry_points.txt +0 -0
  55. {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/requires.txt +0 -0
  56. {errortools-2.3.0 → errortools-2.4.0}/errortools.egg-info/top_level.txt +0 -0
  57. {errortools-2.3.0 → errortools-2.4.0}/setup.cfg +0 -0
  58. {errortools-2.3.0 → errortools-2.4.0}/tests/conftest.py +0 -0
  59. {errortools-2.3.0 → errortools-2.4.0}/tests/run_tests.py +0 -0
  60. {errortools-2.3.0 → errortools-2.4.0}/tests/test_abc.py +0 -0
  61. {errortools-2.3.0 → errortools-2.4.0}/tests/test_const.py +0 -0
  62. {errortools-2.3.0 → errortools-2.4.0}/tests/test_decorator.py +0 -0
  63. {errortools-2.3.0 → errortools-2.4.0}/tests/test_descriptor.py +0 -0
  64. {errortools-2.3.0 → errortools-2.4.0}/tests/test_errorcodes.py +0 -0
  65. {errortools-2.3.0 → errortools-2.4.0}/tests/test_groups.py +0 -0
  66. {errortools-2.3.0 → errortools-2.4.0}/tests/test_ignore.py +0 -0
  67. {errortools-2.3.0 → errortools-2.4.0}/tests/test_logging.py +0 -0
  68. {errortools-2.3.0 → errortools-2.4.0}/tests/test_mixins.py +0 -0
  69. {errortools-2.3.0 → errortools-2.4.0}/tests/test_partials.py +0 -0
  70. {errortools-2.3.0 → errortools-2.4.0}/tests/test_raises.py +0 -0
  71. {errortools-2.3.0 → errortools-2.4.0}/tests/test_typing.py +0 -0
  72. {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.0
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. error_cache ── cache exceptions by call arguments ─────────────────────
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
- # ── 11. Custom exceptions — three layers ──────────────────────────────────────
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
- # ── 12. BaseWarning ── structured warnings with factory methods ───────────────
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. error_cache ── cache exceptions by call arguments ─────────────────────
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
- # ── 11. Custom exceptions — three layers ──────────────────────────────────────
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
- # ── 12. BaseWarning ── structured warnings with factory methods ───────────────
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,5 +1,5 @@
1
- __version__: str = "2.3.0"
2
- __version_tuple__: tuple[int, int, int] = (2, 3, 0)
1
+ __version__: str = "2.4.0"
2
+ __version_tuple__: tuple[int, int, int] = (2, 4, 0)
3
3
  __commit_id__: str | None = None
4
4
 
5
5
  version = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errortools
3
- Version: 2.3.0
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. error_cache ── cache exceptions by call arguments ─────────────────────
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
- # ── 11. Custom exceptions — three layers ──────────────────────────────────────
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
- # ── 12. BaseWarning ── structured warnings with factory methods ───────────────
227
+ # ── 15. BaseWarning ── structured warnings with factory methods ───────────────
207
228
  class ExperimentalWarning(BaseWarning):
208
229
  default_detail = "This feature is experimental."
209
230
 
@@ -57,6 +57,7 @@ tests/test_decorator.py
57
57
  tests/test_descriptor.py
58
58
  tests/test_errno.py
59
59
  tests/test_errorcodes.py
60
+ tests/test_future.py
60
61
  tests/test_groups.py
61
62
  tests/test_ignore.py
62
63
  tests/test_logging.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="errortools",
5
- version="2.3.0",
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",
@@ -1,6 +1,6 @@
1
1
  """Tests for `errortools` module. Using pytest."""
2
2
 
3
- __version__ = "1.9"
3
+ __version__ = "1.11"
4
4
 
5
5
  try:
6
6
  import pytest
@@ -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
- message = get_errno_message(9999)
41
- assert "Unknown error" in message or len(message) > 0
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