errortools 1.1.0__tar.gz → 1.3.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 (70) hide show
  1. errortools-1.3.0/AUTHORS.txt +2 -0
  2. {errortools-1.1.0 → errortools-1.3.0}/LICENSE.txt +1 -1
  3. errortools-1.3.0/PKG-INFO +318 -0
  4. errortools-1.3.0/README.md +286 -0
  5. {errortools-1.1.0/_errortools/cached → errortools-1.3.0/_errortools}/cache.py +82 -81
  6. {errortools-1.1.0 → errortools-1.3.0}/_errortools/classes/abc.py +351 -351
  7. {errortools-1.1.0 → errortools-1.3.0}/_errortools/classes/errorcodes.py +55 -34
  8. {errortools-1.1.0 → errortools-1.3.0}/_errortools/classes/group.py +118 -118
  9. errortools-1.3.0/_errortools/cli.py +94 -0
  10. errortools-1.3.0/_errortools/const.py +10 -0
  11. errortools-1.3.0/_errortools/decorator/__init__.py +1 -0
  12. errortools-1.3.0/_errortools/decorator/deprecated.py +33 -0
  13. errortools-1.3.0/_errortools/future.py +23 -0
  14. errortools-1.3.0/_errortools/ignore.py +244 -0
  15. errortools-1.3.0/_errortools/logging/__init__.py +43 -0
  16. errortools-1.3.0/_errortools/logging/base.py +467 -0
  17. errortools-1.3.0/_errortools/logging/level.py +85 -0
  18. errortools-1.3.0/_errortools/logging/logger.py +13 -0
  19. errortools-1.3.0/_errortools/logging/record.py +109 -0
  20. errortools-1.3.0/_errortools/logging/sink.py +236 -0
  21. errortools-1.1.0/_errortools/_metadata.py → errortools-1.3.0/_errortools/metadata.py +1 -1
  22. errortools-1.3.0/_errortools/partial.py +110 -0
  23. {errortools-1.1.0 → errortools-1.3.0}/_errortools/raises.py +183 -162
  24. {errortools-1.1.0 → errortools-1.3.0}/_errortools/typing.py +19 -1
  25. errortools-1.1.0/_errortools/_version.py → errortools-1.3.0/_errortools/version.py +7 -7
  26. {errortools-1.1.0 → errortools-1.3.0}/errortools/__init__.py +158 -138
  27. errortools-1.3.0/errortools/__main__.py +4 -0
  28. errortools-1.3.0/errortools.egg-info/PKG-INFO +318 -0
  29. {errortools-1.1.0 → errortools-1.3.0}/errortools.egg-info/SOURCES.txt +21 -7
  30. errortools-1.3.0/errortools.egg-info/entry_points.txt +2 -0
  31. {errortools-1.1.0 → errortools-1.3.0}/setup.py +34 -29
  32. errortools-1.3.0/tests/__init__.py +12 -0
  33. errortools-1.3.0/tests/run_tests.py +19 -0
  34. {errortools-1.1.0 → errortools-1.3.0}/tests/test_abc.py +303 -314
  35. {errortools-1.1.0 → errortools-1.3.0}/tests/test_cache.py +18 -15
  36. errortools-1.3.0/tests/test_decorator.py +84 -0
  37. {errortools-1.1.0 → errortools-1.3.0}/tests/test_descriptor.py +3 -12
  38. {errortools-1.1.0 → errortools-1.3.0}/tests/test_errorcodes.py +6 -3
  39. {errortools-1.1.0 → errortools-1.3.0}/tests/test_groups.py +3 -3
  40. errortools-1.3.0/tests/test_ignore.py +415 -0
  41. errortools-1.3.0/tests/test_logging.py +675 -0
  42. {errortools-1.1.0 → errortools-1.3.0}/tests/test_mixins.py +3 -5
  43. errortools-1.3.0/tests/test_partials.py +232 -0
  44. {errortools-1.1.0 → errortools-1.3.0}/tests/test_raises.py +3 -20
  45. {errortools-1.1.0 → errortools-1.3.0}/tests/test_typing.py +35 -6
  46. {errortools-1.1.0 → errortools-1.3.0}/tests/test_warnings.py +3 -0
  47. errortools-1.1.0/PKG-INFO +0 -62
  48. errortools-1.1.0/README.md +0 -31
  49. errortools-1.1.0/_errortools/_types.py +0 -21
  50. errortools-1.1.0/_errortools/cached/__init__.py +0 -1
  51. errortools-1.1.0/_errortools/ignore.py +0 -80
  52. errortools-1.1.0/_errortools/tools/__init__.py +0 -1
  53. errortools-1.1.0/_errortools/tools/_warps.py +0 -27
  54. errortools-1.1.0/errortools.egg-info/PKG-INFO +0 -62
  55. errortools-1.1.0/tests/__init__.py +0 -3
  56. errortools-1.1.0/tests/run_tests.py +0 -6
  57. errortools-1.1.0/tests/test_ignore.py +0 -193
  58. {errortools-1.1.0 → errortools-1.3.0}/_errortools/__init__.py +0 -0
  59. {errortools-1.1.0 → errortools-1.3.0}/_errortools/classes/__init__.py +0 -0
  60. {errortools-1.1.0 → errortools-1.3.0}/_errortools/classes/warn.py +0 -0
  61. {errortools-1.1.0 → errortools-1.3.0}/_errortools/methods/__init__.py +0 -0
  62. {errortools-1.1.0 → errortools-1.3.0}/_errortools/methods/errorattr.py +0 -0
  63. {errortools-1.1.0 → errortools-1.3.0}/_errortools/methods/errordelattr.py +0 -0
  64. {errortools-1.1.0 → errortools-1.3.0}/_errortools/methods/errorhasattr.py +0 -0
  65. {errortools-1.1.0 → errortools-1.3.0}/_errortools/methods/errorsetattr.py +0 -0
  66. {errortools-1.1.0 → errortools-1.3.0}/_errortools/py.typed +0 -0
  67. {errortools-1.1.0 → errortools-1.3.0}/errortools.egg-info/dependency_links.txt +0 -0
  68. {errortools-1.1.0 → errortools-1.3.0}/errortools.egg-info/top_level.txt +0 -0
  69. {errortools-1.1.0 → errortools-1.3.0}/setup.cfg +0 -0
  70. {errortools-1.1.0 → errortools-1.3.0}/tests/conftest.py +0 -0
@@ -0,0 +1,2 @@
1
+ aiwonderland
2
+ qorexdev
@@ -1,4 +1,4 @@
1
- Copyright (c) 2026 aiwonderland
1
+ Copyright (c) 2026 authors see AUTHORS.txt
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: errortools
3
+ Version: 1.3.0
4
+ Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
5
+ Home-page: https://github.com/more-abc/errortools
6
+ Author: Evan Yang
7
+ Author-email: quantbit@126.com
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.txt
21
+ License-File: AUTHORS.txt
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: license
29
+ Dynamic: license-file
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # errortools
34
+ A lightweight Python exception handling utility library.
35
+
36
+ ## Features
37
+ - **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
38
+ - **Catch & Suppress**: `ignore()`, `ignore_subclass()`, `ignore_warns()`, `fast_ignore()`, `super_fast_ignore()`, `timeout()`, `retry()` — graceful suppression of exceptions and warnings, with automatic retry
39
+ - **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
40
+ - **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
41
+ - **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
42
+ - **Type Aliases**: `ExceptionType`, `AnyErrorCode`, `BaseErrorCodesType`, and more
43
+ - **Logging**: `logger` — loguru-inspired structured logger with leveled output, multiple sinks, context binding, and exception capture
44
+
45
+ ## Installation
46
+ ```bash
47
+ pip install errortools
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Example
53
+
54
+ ```python
55
+ import warnings
56
+ from errortools import (
57
+ ignore, fast_ignore, ignore_subclass, ignore_warns, timeout, retry,
58
+ reraise, raises, raises_all, assert_raises,
59
+ error_cache,
60
+ PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
61
+ )
62
+ from errortools.future import super_fast_ignore
63
+
64
+ # ── 1. ignore ── context manager with full metadata ──────────────────────────
65
+ with ignore(KeyError) as err:
66
+ _ = {}["missing"]
67
+
68
+ assert err.be_ignore # True — was suppressed
69
+ assert err.name == "KeyError" # exception class name
70
+ assert err.count == 1 # how many times suppressed
71
+ assert err.exception # the original KeyError instance
72
+ assert err.traceback # full formatted traceback string
73
+
74
+ # ignore as a decorator
75
+ @ignore(ValueError, TypeError)
76
+ def parse(x: str) -> int:
77
+ return int(x)
78
+
79
+ parse("bad") # suppressed → returns None
80
+
81
+ # ── 2. fast_ignore / super_fast_ignore ── zero-overhead hot-path suppression ─
82
+ with fast_ignore(KeyError, IndexError):
83
+ _ = [][0] # suppressed, no metadata collected
84
+
85
+ with super_fast_ignore(KeyError):
86
+ _ = {}["x"] # absolute minimal overhead
87
+
88
+ # ── 3. ignore_subclass ── suppress any subclass of a base ────────────────────
89
+ with ignore_subclass(LookupError):
90
+ raise IndexError("out of range") # IndexError ⊆ LookupError — suppressed
91
+
92
+ # ── 4. ignore_warns ── silence warnings ──────────────────────────────────────
93
+ with ignore_warns(DeprecationWarning):
94
+ warnings.warn("old api", DeprecationWarning) # no output
95
+
96
+ with ignore_warns(): # suppress everything
97
+ warnings.warn("anything")
98
+
99
+ # ── 5. timeout ── cancel async functions that take too long ──────────────────
100
+ @timeout(5.0)
101
+ async def fetch_data(url: str) -> bytes:
102
+ ... # any async operation
103
+
104
+ # asyncio.TimeoutError raised automatically if it exceeds 5 s
105
+
106
+ # ── 6. retry ── automatically retry on failure ───────────────────────────────
107
+ @retry(times=3, on=ConnectionError, delay=1.0)
108
+ def connect(host: str) -> None:
109
+ ... # retried up to 3 times on ConnectionError
110
+
111
+ # works with async functions too
112
+ @retry(times=5, on=TimeoutError, delay=0.5)
113
+ async def fetch(url: str) -> bytes:
114
+ ...
115
+
116
+ # multiple exception types
117
+ @retry(times=2, on=(ValueError, KeyError))
118
+ def parse(data: dict) -> str:
119
+ return data["key"]
120
+
121
+ # ── 7. reraise ── convert exception types on the fly ─────────────────────────
122
+ with reraise(KeyError, ValueError):
123
+ raise KeyError("missing key") # → ValueError: 'missing key'
124
+
125
+ with reraise((KeyError, IndexError), RuntimeError):
126
+ _ = [][99] # → RuntimeError: list index out of range
127
+
128
+ # ── 8. raises / raises_all ── batch raise ────────────────────────────────────
129
+ raises([ValueError], ["bad input"]) # → ValueError: bad input
130
+
131
+ raises_all(
132
+ [ValueError, TypeError],
133
+ ["bad input"],
134
+ ) # → ExceptionGroup (2 sub-exceptions)
135
+
136
+ # ── 9. assert_raises ── assert a callable raises ─────────────────────────────
137
+ exc = assert_raises(int, [ValueError], "not-a-number")
138
+ print(exc) # invalid literal for int() with base 10: 'not-a-number'
139
+
140
+ # ── 10. error_cache ── cache exceptions by call arguments ─────────────────────
141
+ @error_cache(maxsize=64)
142
+ def load(user_id: int) -> dict:
143
+ if user_id < 0:
144
+ raise ValueError(f"invalid id: {user_id}")
145
+ return {"id": user_id}
146
+
147
+ with ignore(ValueError):
148
+ load(-1) # raises, exception cached for args (-1,)
149
+
150
+ print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
151
+ load.clear_cache()
152
+
153
+ # ── 11. Custom exceptions — three layers ──────────────────────────────────────
154
+
155
+ # Layer 1: PureBaseException — code + detail only
156
+ class AppError(PureBaseException):
157
+ code = 9000
158
+ default_detail = "Application error."
159
+
160
+ print(AppError()) # [9000] Application error.
161
+ print(repr(AppError())) # AppError(detail='Application error.', code=9000)
162
+
163
+ # Layer 2: ContextException — adds trace_id, context dict, exception chaining
164
+ class ServiceError(ContextException):
165
+ code = 9001
166
+ default_detail = "Service unavailable."
167
+
168
+ try:
169
+ raise ConnectionError("db timeout")
170
+ except ConnectionError as cause:
171
+ err = (
172
+ ServiceError("downstream failed")
173
+ .with_context(service="postgres", retries=3)
174
+ .with_cause(cause)
175
+ )
176
+ print(err.trace_id) # 'a3f1c8...' — unique per instance
177
+ print(err.context) # {'service': 'postgres', 'retries': 3}
178
+ print(err.chain) # [{'type': 'ServiceError', 'code': 9001, ...}]
179
+ print(err.traceback) # compact stack trace joined by |
180
+
181
+ # Layer 3: BaseErrorCodes — predefined factory methods
182
+ raise BaseErrorCodes.invalid_input("username too short") # InvalidInputError [1001]
183
+ raise BaseErrorCodes.access_denied() # AccessDeniedError [2001]
184
+ raise BaseErrorCodes.not_found("user #42") # NotFoundError [3001]
185
+ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [4001]
186
+ raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
187
+ raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
188
+
189
+ # ── 12. BaseWarning ── structured warnings with factory methods ───────────────
190
+ class ExperimentalWarning(BaseWarning):
191
+ default_detail = "This feature is experimental."
192
+
193
+ ExperimentalWarning.emit() # uses default_detail
194
+ ExperimentalWarning.emit("use at your own risk") # custom message
195
+
196
+ BaseWarning.deprecated("use new_api() instead").emit() # DeprecatedWarning
197
+ BaseWarning.performance("O(n²) detected").emit() # PerformanceWarning
198
+ BaseWarning.resource("file handle leak").emit() # ResourceUsageWarning
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Logging
204
+
205
+ `errortools.logging` is a loguru-inspired structured logger with no external dependencies.
206
+
207
+ ### Quick start
208
+
209
+ ```python
210
+ from errortools.logging import logger
211
+
212
+ logger.info("Server started on port {}", 8080)
213
+ logger.warning("Disk at {pct:.1f}%", pct=92.5)
214
+ logger.success("All systems operational")
215
+ ```
216
+
217
+ Output (colourised in a terminal):
218
+
219
+ ```
220
+ 2026-01-01 12:00:00.123 | INFO | app:main:42 - Server started on port 8080
221
+ 2026-01-01 12:00:00.124 | WARNING | app:main:43 - Disk at 92.5%
222
+ 2026-01-01 12:00:00.125 | SUCCESS | app:main:44 - All systems operational
223
+ ```
224
+
225
+ ### Log levels
226
+
227
+ | Method | Level | No |
228
+ |---|---|---|
229
+ | `logger.trace()` | TRACE | 5 |
230
+ | `logger.debug()` | DEBUG | 10 |
231
+ | `logger.info()` | INFO | 20 |
232
+ | `logger.success()` | SUCCESS | 25 |
233
+ | `logger.warning()` | WARNING | 30 |
234
+ | `logger.error()` | ERROR | 40 |
235
+ | `logger.critical()` | CRITICAL | 50 |
236
+
237
+ ### Sinks
238
+
239
+ Add and remove destinations at runtime. Each sink has its own level filter.
240
+
241
+ ```python
242
+ from errortools.logging import logger, Level
243
+
244
+ # stream (stderr by default, auto-detects TTY colour)
245
+ logger.add(sys.stdout, level="WARNING")
246
+
247
+ # file with rotation (bytes) and retention (number of old files to keep)
248
+ sid = logger.add("logs/app.log", rotation=10_485_760, retention=5)
249
+
250
+ # any callable
251
+ logger.add(print)
252
+
253
+ # remove by id, or pass no argument to remove all
254
+ logger.remove(sid)
255
+ logger.remove()
256
+ ```
257
+
258
+ ### Level filtering
259
+
260
+ ```python
261
+ logger.set_level("WARNING") # or Level.WARNING or numeric 30
262
+ logger.debug("dropped") # below threshold — not emitted
263
+ logger.warning("kept") # at threshold — emitted
264
+ ```
265
+
266
+ ### Context binding
267
+
268
+ `bind()` returns a **new** logger that carries extra fields in every record. The original logger is unmodified.
269
+
270
+ ```python
271
+ req_log = logger.bind(request_id="abc-123", user="alice")
272
+ req_log.info("Request received") # record.extra contains request_id and user
273
+
274
+ # Stacking
275
+ db_log = req_log.bind(db="postgres")
276
+ db_log.debug("Query OK") # extra: request_id, user, db
277
+ ```
278
+
279
+ ### Exception capture
280
+
281
+ ```python
282
+ # Attach the current traceback to any log call
283
+ try:
284
+ connect()
285
+ except ConnectionError:
286
+ logger.exception("DB connection failed") # logs at ERROR + traceback
287
+
288
+ # Equivalent long-hand
289
+ logger.opt(exception=True).error("DB connection failed")
290
+ ```
291
+
292
+ ### catch() — auto-log and suppress
293
+
294
+ ```python
295
+ # As a context manager
296
+ with logger.catch():
297
+ int("not a number") # logged at ERROR, then suppressed
298
+
299
+ # Re-raise after logging
300
+ with logger.catch(ConnectionError, reraise=True):
301
+ connect()
302
+
303
+ # As a decorator
304
+ @logger.catch(ValueError)
305
+ def parse(s: str) -> int:
306
+ return int(s)
307
+ ```
308
+
309
+ ### Custom format string
310
+
311
+ ```python
312
+ logger.add(
313
+ "debug.log",
314
+ fmt="{time} | {level} | {name}:{function}:{line} - {message}",
315
+ )
316
+ ```
317
+
318
+ Available placeholders: `{time}`, `{level}`, `{name}`, `{file}`, `{line}`, `{function}`, `{message}`.
@@ -0,0 +1,286 @@
1
+ # errortools
2
+ A lightweight Python exception handling utility library.
3
+
4
+ ## Features
5
+ - **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
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
+ - **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
8
+ - **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
9
+ - **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
10
+ - **Type Aliases**: `ExceptionType`, `AnyErrorCode`, `BaseErrorCodesType`, and more
11
+ - **Logging**: `logger` — loguru-inspired structured logger with leveled output, multiple sinks, context binding, and exception capture
12
+
13
+ ## Installation
14
+ ```bash
15
+ pip install errortools
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Example
21
+
22
+ ```python
23
+ import warnings
24
+ from errortools import (
25
+ ignore, fast_ignore, ignore_subclass, ignore_warns, timeout, retry,
26
+ reraise, raises, raises_all, assert_raises,
27
+ error_cache,
28
+ PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
29
+ )
30
+ from errortools.future import super_fast_ignore
31
+
32
+ # ── 1. ignore ── context manager with full metadata ──────────────────────────
33
+ with ignore(KeyError) as err:
34
+ _ = {}["missing"]
35
+
36
+ assert err.be_ignore # True — was suppressed
37
+ assert err.name == "KeyError" # exception class name
38
+ assert err.count == 1 # how many times suppressed
39
+ assert err.exception # the original KeyError instance
40
+ assert err.traceback # full formatted traceback string
41
+
42
+ # ignore as a decorator
43
+ @ignore(ValueError, TypeError)
44
+ def parse(x: str) -> int:
45
+ return int(x)
46
+
47
+ parse("bad") # suppressed → returns None
48
+
49
+ # ── 2. fast_ignore / super_fast_ignore ── zero-overhead hot-path suppression ─
50
+ with fast_ignore(KeyError, IndexError):
51
+ _ = [][0] # suppressed, no metadata collected
52
+
53
+ with super_fast_ignore(KeyError):
54
+ _ = {}["x"] # absolute minimal overhead
55
+
56
+ # ── 3. ignore_subclass ── suppress any subclass of a base ────────────────────
57
+ with ignore_subclass(LookupError):
58
+ raise IndexError("out of range") # IndexError ⊆ LookupError — suppressed
59
+
60
+ # ── 4. ignore_warns ── silence warnings ──────────────────────────────────────
61
+ with ignore_warns(DeprecationWarning):
62
+ warnings.warn("old api", DeprecationWarning) # no output
63
+
64
+ with ignore_warns(): # suppress everything
65
+ warnings.warn("anything")
66
+
67
+ # ── 5. timeout ── cancel async functions that take too long ──────────────────
68
+ @timeout(5.0)
69
+ async def fetch_data(url: str) -> bytes:
70
+ ... # any async operation
71
+
72
+ # asyncio.TimeoutError raised automatically if it exceeds 5 s
73
+
74
+ # ── 6. retry ── automatically retry on failure ───────────────────────────────
75
+ @retry(times=3, on=ConnectionError, delay=1.0)
76
+ def connect(host: str) -> None:
77
+ ... # retried up to 3 times on ConnectionError
78
+
79
+ # works with async functions too
80
+ @retry(times=5, on=TimeoutError, delay=0.5)
81
+ async def fetch(url: str) -> bytes:
82
+ ...
83
+
84
+ # multiple exception types
85
+ @retry(times=2, on=(ValueError, KeyError))
86
+ def parse(data: dict) -> str:
87
+ return data["key"]
88
+
89
+ # ── 7. reraise ── convert exception types on the fly ─────────────────────────
90
+ with reraise(KeyError, ValueError):
91
+ raise KeyError("missing key") # → ValueError: 'missing key'
92
+
93
+ with reraise((KeyError, IndexError), RuntimeError):
94
+ _ = [][99] # → RuntimeError: list index out of range
95
+
96
+ # ── 8. raises / raises_all ── batch raise ────────────────────────────────────
97
+ raises([ValueError], ["bad input"]) # → ValueError: bad input
98
+
99
+ raises_all(
100
+ [ValueError, TypeError],
101
+ ["bad input"],
102
+ ) # → ExceptionGroup (2 sub-exceptions)
103
+
104
+ # ── 9. assert_raises ── assert a callable raises ─────────────────────────────
105
+ exc = assert_raises(int, [ValueError], "not-a-number")
106
+ print(exc) # invalid literal for int() with base 10: 'not-a-number'
107
+
108
+ # ── 10. error_cache ── cache exceptions by call arguments ─────────────────────
109
+ @error_cache(maxsize=64)
110
+ def load(user_id: int) -> dict:
111
+ if user_id < 0:
112
+ raise ValueError(f"invalid id: {user_id}")
113
+ return {"id": user_id}
114
+
115
+ with ignore(ValueError):
116
+ load(-1) # raises, exception cached for args (-1,)
117
+
118
+ print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
119
+ load.clear_cache()
120
+
121
+ # ── 11. Custom exceptions — three layers ──────────────────────────────────────
122
+
123
+ # Layer 1: PureBaseException — code + detail only
124
+ class AppError(PureBaseException):
125
+ code = 9000
126
+ default_detail = "Application error."
127
+
128
+ print(AppError()) # [9000] Application error.
129
+ print(repr(AppError())) # AppError(detail='Application error.', code=9000)
130
+
131
+ # Layer 2: ContextException — adds trace_id, context dict, exception chaining
132
+ class ServiceError(ContextException):
133
+ code = 9001
134
+ default_detail = "Service unavailable."
135
+
136
+ try:
137
+ raise ConnectionError("db timeout")
138
+ except ConnectionError as cause:
139
+ err = (
140
+ ServiceError("downstream failed")
141
+ .with_context(service="postgres", retries=3)
142
+ .with_cause(cause)
143
+ )
144
+ print(err.trace_id) # 'a3f1c8...' — unique per instance
145
+ print(err.context) # {'service': 'postgres', 'retries': 3}
146
+ print(err.chain) # [{'type': 'ServiceError', 'code': 9001, ...}]
147
+ print(err.traceback) # compact stack trace joined by |
148
+
149
+ # Layer 3: BaseErrorCodes — predefined factory methods
150
+ raise BaseErrorCodes.invalid_input("username too short") # InvalidInputError [1001]
151
+ raise BaseErrorCodes.access_denied() # AccessDeniedError [2001]
152
+ raise BaseErrorCodes.not_found("user #42") # NotFoundError [3001]
153
+ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [4001]
154
+ raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
155
+ raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
156
+
157
+ # ── 12. BaseWarning ── structured warnings with factory methods ───────────────
158
+ class ExperimentalWarning(BaseWarning):
159
+ default_detail = "This feature is experimental."
160
+
161
+ ExperimentalWarning.emit() # uses default_detail
162
+ ExperimentalWarning.emit("use at your own risk") # custom message
163
+
164
+ BaseWarning.deprecated("use new_api() instead").emit() # DeprecatedWarning
165
+ BaseWarning.performance("O(n²) detected").emit() # PerformanceWarning
166
+ BaseWarning.resource("file handle leak").emit() # ResourceUsageWarning
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Logging
172
+
173
+ `errortools.logging` is a loguru-inspired structured logger with no external dependencies.
174
+
175
+ ### Quick start
176
+
177
+ ```python
178
+ from errortools.logging import logger
179
+
180
+ logger.info("Server started on port {}", 8080)
181
+ logger.warning("Disk at {pct:.1f}%", pct=92.5)
182
+ logger.success("All systems operational")
183
+ ```
184
+
185
+ Output (colourised in a terminal):
186
+
187
+ ```
188
+ 2026-01-01 12:00:00.123 | INFO | app:main:42 - Server started on port 8080
189
+ 2026-01-01 12:00:00.124 | WARNING | app:main:43 - Disk at 92.5%
190
+ 2026-01-01 12:00:00.125 | SUCCESS | app:main:44 - All systems operational
191
+ ```
192
+
193
+ ### Log levels
194
+
195
+ | Method | Level | No |
196
+ |---|---|---|
197
+ | `logger.trace()` | TRACE | 5 |
198
+ | `logger.debug()` | DEBUG | 10 |
199
+ | `logger.info()` | INFO | 20 |
200
+ | `logger.success()` | SUCCESS | 25 |
201
+ | `logger.warning()` | WARNING | 30 |
202
+ | `logger.error()` | ERROR | 40 |
203
+ | `logger.critical()` | CRITICAL | 50 |
204
+
205
+ ### Sinks
206
+
207
+ Add and remove destinations at runtime. Each sink has its own level filter.
208
+
209
+ ```python
210
+ from errortools.logging import logger, Level
211
+
212
+ # stream (stderr by default, auto-detects TTY colour)
213
+ logger.add(sys.stdout, level="WARNING")
214
+
215
+ # file with rotation (bytes) and retention (number of old files to keep)
216
+ sid = logger.add("logs/app.log", rotation=10_485_760, retention=5)
217
+
218
+ # any callable
219
+ logger.add(print)
220
+
221
+ # remove by id, or pass no argument to remove all
222
+ logger.remove(sid)
223
+ logger.remove()
224
+ ```
225
+
226
+ ### Level filtering
227
+
228
+ ```python
229
+ logger.set_level("WARNING") # or Level.WARNING or numeric 30
230
+ logger.debug("dropped") # below threshold — not emitted
231
+ logger.warning("kept") # at threshold — emitted
232
+ ```
233
+
234
+ ### Context binding
235
+
236
+ `bind()` returns a **new** logger that carries extra fields in every record. The original logger is unmodified.
237
+
238
+ ```python
239
+ req_log = logger.bind(request_id="abc-123", user="alice")
240
+ req_log.info("Request received") # record.extra contains request_id and user
241
+
242
+ # Stacking
243
+ db_log = req_log.bind(db="postgres")
244
+ db_log.debug("Query OK") # extra: request_id, user, db
245
+ ```
246
+
247
+ ### Exception capture
248
+
249
+ ```python
250
+ # Attach the current traceback to any log call
251
+ try:
252
+ connect()
253
+ except ConnectionError:
254
+ logger.exception("DB connection failed") # logs at ERROR + traceback
255
+
256
+ # Equivalent long-hand
257
+ logger.opt(exception=True).error("DB connection failed")
258
+ ```
259
+
260
+ ### catch() — auto-log and suppress
261
+
262
+ ```python
263
+ # As a context manager
264
+ with logger.catch():
265
+ int("not a number") # logged at ERROR, then suppressed
266
+
267
+ # Re-raise after logging
268
+ with logger.catch(ConnectionError, reraise=True):
269
+ connect()
270
+
271
+ # As a decorator
272
+ @logger.catch(ValueError)
273
+ def parse(s: str) -> int:
274
+ return int(s)
275
+ ```
276
+
277
+ ### Custom format string
278
+
279
+ ```python
280
+ logger.add(
281
+ "debug.log",
282
+ fmt="{time} | {level} | {name}:{function}:{line} - {message}",
283
+ )
284
+ ```
285
+
286
+ Available placeholders: `{time}`, `{level}`, `{name}`, `{file}`, `{line}`, `{function}`, `{message}`.