errortools 3.1.0__tar.gz → 3.2.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-3.2.0/PKG-INFO +151 -0
- errortools-3.2.0/README.md +105 -0
- errortools-3.2.0/_errortools/__main__.py +109 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/_speedup.c +49 -1
- errortools-3.2.0/_errortools/classes/__init__.py +1 -0
- errortools-3.2.0/_errortools/classes/abc.py +207 -0
- errortools-3.2.0/_errortools/classes/errorcodes.py +273 -0
- errortools-3.2.0/_errortools/classes/group.py +121 -0
- errortools-3.2.0/_errortools/classes/warn.py +124 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/cli.py +34 -41
- errortools-3.2.0/_errortools/decorator/__init__.py +1 -0
- errortools-3.2.0/_errortools/decorator/cache.py +80 -0
- errortools-3.2.0/_errortools/decorator/deprecated.py +61 -0
- errortools-3.2.0/_errortools/decorator/handlers.py +90 -0
- errortools-3.2.0/_errortools/decorator/retry.py +99 -0
- errortools-3.2.0/_errortools/decorator/timeout.py +38 -0
- errortools-3.2.0/_errortools/descriptor/__init__.py +2 -0
- errortools-3.2.0/_errortools/descriptor/base.py +25 -0
- errortools-3.2.0/_errortools/descriptor/errormsg.py +33 -0
- errortools-3.2.0/_errortools/descriptor/nonblankmsg.py +46 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/future.py +14 -8
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/ignore.py +2 -135
- errortools-3.2.0/_errortools/logging/__init__.py +43 -0
- errortools-3.2.0/_errortools/logging/base.py +462 -0
- errortools-3.2.0/_errortools/logging/level.py +85 -0
- errortools-3.2.0/_errortools/logging/logger.py +13 -0
- errortools-3.2.0/_errortools/logging/record.py +116 -0
- errortools-3.2.0/_errortools/logging/sink.py +243 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/metadata.py +1 -3
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/partial.py +1 -3
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/raises.py +2 -5
- errortools-3.2.0/_errortools/version.py +17 -0
- errortools-3.2.0/_errortools/wrappers/__init__.py +2 -0
- errortools-3.2.0/_errortools/wrappers/cache.py +93 -0
- errortools-3.2.0/_errortools/wrappers/ignore.py +120 -0
- errortools-3.2.0/docs/conf.py +56 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools/__init__.py +37 -10
- errortools-3.2.0/errortools.egg-info/PKG-INFO +151 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/SOURCES.txt +32 -2
- errortools-3.2.0/errortools.egg-info/requires.txt +2 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/top_level.txt +5 -0
- errortools-3.2.0/pyproject.toml +88 -0
- errortools-3.2.0/testing/__init__.py +33 -0
- errortools-3.2.0/testing/benchmark/__init__.py +1 -0
- errortools-3.2.0/testing/benchmark/test_future_perf.py +239 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/run_tests.py +3 -3
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_decorator.py +288 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_descriptor.py +54 -10
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_ignore.py +1 -174
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_logging.py +0 -1
- errortools-3.2.0/testing/test_testing/__init__.py +1 -0
- errortools-3.2.0/testing/test_testing/test_testing.py +41 -0
- errortools-3.1.0/PKG-INFO +0 -367
- errortools-3.1.0/README.md +0 -329
- errortools-3.1.0/_errortools/version.py +0 -7
- errortools-3.1.0/errortools.egg-info/PKG-INFO +0 -367
- errortools-3.1.0/errortools.egg-info/requires.txt +0 -2
- errortools-3.1.0/setup.py +0 -58
- errortools-3.1.0/testing/__init__.py +0 -11
- {errortools-3.1.0 → errortools-3.2.0}/AUTHORS.txt +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/LICENSE.txt +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/__init__.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/_cli.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/const.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/errno.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/py.typed +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/_errortools/typing.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools/__main__.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools/future.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools/logging.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools/partial.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/dependency_links.txt +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/entry_points.txt +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/setup.cfg +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/__main__.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/conftest.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_abc.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_const.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_errno.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_errorcodes.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_future.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_groups.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_partials.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_raises.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_typing.py +0 -0
- {errortools-3.1.0 → errortools-3.2.0}/testing/test_warnings.py +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: errortools
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
|
|
5
|
+
Author-email: Evan Yang <quantbit@126.com>
|
|
6
|
+
License: Copyright (c) 2026 authors see AUTHORS.txt
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
9
|
+
a copy of this software and associated documentation files (the
|
|
10
|
+
"Software"), to deal in the Software without restriction, including
|
|
11
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
12
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
13
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
14
|
+
the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be
|
|
17
|
+
included in all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
20
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
21
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
22
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
23
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
24
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
25
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/more-abc/errortools
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Typing :: Typed
|
|
39
|
+
Requires-Python: >=3.8
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE.txt
|
|
42
|
+
License-File: AUTHORS.txt
|
|
43
|
+
Requires-Dist: namebyauthor==1.0.0
|
|
44
|
+
Requires-Dist: typing_extensions>=4.15.0
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# errortools
|
|
48
|
+
|
|
49
|
+
A lightweight Python exception handling utility library.
|
|
50
|
+
|
|
51
|
+
[](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings)
|
|
52
|
+
[](https://pypi.org/project/errortools/)
|
|
53
|
+
[](https://pypi.org/project/errortools/)
|
|
54
|
+

|
|
55
|
+

|
|
56
|
+

|
|
57
|
+

|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
### Use pip...
|
|
62
|
+
```bash
|
|
63
|
+
pip install errortools
|
|
64
|
+
```
|
|
65
|
+
### ...or uv
|
|
66
|
+
```bash
|
|
67
|
+
uv add errortools
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- **Suppress**: `ignore()`, `super_fast_ignore()`, `@suppress()` — silence exceptions gracefully
|
|
73
|
+
- **Retry & Timeout**: `@retry()`, `@timeout()` — auto retry with delay, async timeout
|
|
74
|
+
- **Raise & Convert**: `raises()`, `reraise()`, `@convert()` — batch raise, type conversion
|
|
75
|
+
- **Catch & Collect**: `super_fast_catch()`, `ExceptionCollector` — inspect or batch exceptions
|
|
76
|
+
- **Caching**: `@error_cache` — LRU exception cache, like `functools.lru_cache`
|
|
77
|
+
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`
|
|
78
|
+
- **Logging**: structured logger with sinks, context binding, and exception capture
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from errortools import ignore, retry, reraise, error_cache, suppress, convert
|
|
84
|
+
from errortools.future import super_fast_ignore, super_fast_catch, ExceptionCollector
|
|
85
|
+
|
|
86
|
+
# ── Suppress ─────────────────────────────────────────────────
|
|
87
|
+
with ignore(KeyError) as err: # full metadata
|
|
88
|
+
_ = {}["missing"]
|
|
89
|
+
# err.be_ignore=True, err.name='KeyError', err.traceback=...
|
|
90
|
+
|
|
91
|
+
with super_fast_ignore(ValueError): # zero-overhead
|
|
92
|
+
int("bad")
|
|
93
|
+
|
|
94
|
+
@suppress(ZeroDivisionError, default=0) # decorator form
|
|
95
|
+
def divide(a, b): return a / b
|
|
96
|
+
|
|
97
|
+
# ── Retry ────────────────────────────────────────────────────
|
|
98
|
+
@retry(times=3, on=ConnectionError, delay=1.0)
|
|
99
|
+
def connect(host: str): ...
|
|
100
|
+
|
|
101
|
+
# ── Convert ──────────────────────────────────────────────────
|
|
102
|
+
@convert(KeyError, ValueError) # decorator
|
|
103
|
+
def lookup(d, key): return d[key]
|
|
104
|
+
|
|
105
|
+
with reraise(KeyError, ValueError): # context manager
|
|
106
|
+
raise KeyError("x") # → ValueError
|
|
107
|
+
|
|
108
|
+
# ── Catch & Collect ──────────────────────────────────────────
|
|
109
|
+
with super_fast_catch(ValueError) as ctx:
|
|
110
|
+
raise ValueError("oops")
|
|
111
|
+
# ctx.exception → ValueError('oops')
|
|
112
|
+
|
|
113
|
+
collector = ExceptionCollector()
|
|
114
|
+
collector.catch(int, "bad1")
|
|
115
|
+
collector.catch(int, "bad2")
|
|
116
|
+
collector.raise_all() # → ExceptionGroup
|
|
117
|
+
|
|
118
|
+
# ── Cache ────────────────────────────────────────────────────
|
|
119
|
+
@error_cache(maxsize=64)
|
|
120
|
+
def load(uid: int):
|
|
121
|
+
if uid < 0: raise ValueError(f"invalid: {uid}")
|
|
122
|
+
return {"id": uid}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Custom Exceptions
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from errortools import PureBaseException, ContextException, BaseErrorCodes
|
|
129
|
+
|
|
130
|
+
class AppError(PureBaseException):
|
|
131
|
+
code = 9000
|
|
132
|
+
default_detail = "Application error."
|
|
133
|
+
|
|
134
|
+
err = ContextException("failed").with_context(service="db")
|
|
135
|
+
raise BaseErrorCodes.not_found("user #42") # NotFoundError [3001]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Logging
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from errortools.logging import logger
|
|
142
|
+
|
|
143
|
+
logger.info("Server started on port {}", 8080)
|
|
144
|
+
logger.add("app.log", rotation=10_485_760, retention=5)
|
|
145
|
+
with logger.catch():
|
|
146
|
+
int("not a number") # logged + suppressed
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Documentation
|
|
150
|
+
|
|
151
|
+
Full docs: [docs](https://errortools.readthedocs.io) | License: [LICENSE](LICENSE.txt)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# errortools
|
|
2
|
+
|
|
3
|
+
A lightweight Python exception handling utility library.
|
|
4
|
+
|
|
5
|
+
[](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings)
|
|
6
|
+
[](https://pypi.org/project/errortools/)
|
|
7
|
+
[](https://pypi.org/project/errortools/)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
### Use pip...
|
|
16
|
+
```bash
|
|
17
|
+
pip install errortools
|
|
18
|
+
```
|
|
19
|
+
### ...or uv
|
|
20
|
+
```bash
|
|
21
|
+
uv add errortools
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Suppress**: `ignore()`, `super_fast_ignore()`, `@suppress()` — silence exceptions gracefully
|
|
27
|
+
- **Retry & Timeout**: `@retry()`, `@timeout()` — auto retry with delay, async timeout
|
|
28
|
+
- **Raise & Convert**: `raises()`, `reraise()`, `@convert()` — batch raise, type conversion
|
|
29
|
+
- **Catch & Collect**: `super_fast_catch()`, `ExceptionCollector` — inspect or batch exceptions
|
|
30
|
+
- **Caching**: `@error_cache` — LRU exception cache, like `functools.lru_cache`
|
|
31
|
+
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`
|
|
32
|
+
- **Logging**: structured logger with sinks, context binding, and exception capture
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from errortools import ignore, retry, reraise, error_cache, suppress, convert
|
|
38
|
+
from errortools.future import super_fast_ignore, super_fast_catch, ExceptionCollector
|
|
39
|
+
|
|
40
|
+
# ── Suppress ─────────────────────────────────────────────────
|
|
41
|
+
with ignore(KeyError) as err: # full metadata
|
|
42
|
+
_ = {}["missing"]
|
|
43
|
+
# err.be_ignore=True, err.name='KeyError', err.traceback=...
|
|
44
|
+
|
|
45
|
+
with super_fast_ignore(ValueError): # zero-overhead
|
|
46
|
+
int("bad")
|
|
47
|
+
|
|
48
|
+
@suppress(ZeroDivisionError, default=0) # decorator form
|
|
49
|
+
def divide(a, b): return a / b
|
|
50
|
+
|
|
51
|
+
# ── Retry ────────────────────────────────────────────────────
|
|
52
|
+
@retry(times=3, on=ConnectionError, delay=1.0)
|
|
53
|
+
def connect(host: str): ...
|
|
54
|
+
|
|
55
|
+
# ── Convert ──────────────────────────────────────────────────
|
|
56
|
+
@convert(KeyError, ValueError) # decorator
|
|
57
|
+
def lookup(d, key): return d[key]
|
|
58
|
+
|
|
59
|
+
with reraise(KeyError, ValueError): # context manager
|
|
60
|
+
raise KeyError("x") # → ValueError
|
|
61
|
+
|
|
62
|
+
# ── Catch & Collect ──────────────────────────────────────────
|
|
63
|
+
with super_fast_catch(ValueError) as ctx:
|
|
64
|
+
raise ValueError("oops")
|
|
65
|
+
# ctx.exception → ValueError('oops')
|
|
66
|
+
|
|
67
|
+
collector = ExceptionCollector()
|
|
68
|
+
collector.catch(int, "bad1")
|
|
69
|
+
collector.catch(int, "bad2")
|
|
70
|
+
collector.raise_all() # → ExceptionGroup
|
|
71
|
+
|
|
72
|
+
# ── Cache ────────────────────────────────────────────────────
|
|
73
|
+
@error_cache(maxsize=64)
|
|
74
|
+
def load(uid: int):
|
|
75
|
+
if uid < 0: raise ValueError(f"invalid: {uid}")
|
|
76
|
+
return {"id": uid}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Custom Exceptions
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from errortools import PureBaseException, ContextException, BaseErrorCodes
|
|
83
|
+
|
|
84
|
+
class AppError(PureBaseException):
|
|
85
|
+
code = 9000
|
|
86
|
+
default_detail = "Application error."
|
|
87
|
+
|
|
88
|
+
err = ContextException("failed").with_context(service="db")
|
|
89
|
+
raise BaseErrorCodes.not_found("user #42") # NotFoundError [3001]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Logging
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from errortools.logging import logger
|
|
96
|
+
|
|
97
|
+
logger.info("Server started on port {}", 8080)
|
|
98
|
+
logger.add("app.log", rotation=10_485_760, retention=5)
|
|
99
|
+
with logger.catch():
|
|
100
|
+
int("not a number") # logged + suppressed
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Documentation
|
|
104
|
+
|
|
105
|
+
Full docs: [docs](https://errortools.readthedocs.io) | License: [LICENSE](LICENSE.txt)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Private debug CLI — run via `python -m _errortools`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from _errortools.version import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
12
|
+
desc = "errortools internal debug tools"
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 14):
|
|
15
|
+
parser = argparse.ArgumentParser(prog="_errortools", description=desc, color=True)
|
|
16
|
+
else:
|
|
17
|
+
parser = argparse.ArgumentParser(prog="_errortools", description=desc)
|
|
18
|
+
|
|
19
|
+
parser.add_argument("--debug", action="store_true", help="Show debug/environment information")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--reset", action="store_true", help="Clear all cached data (__pycache__, htmlcov, .pytest_cache, .mypy_cache)"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--check", action="store_true", help="Verify installation and dependencies")
|
|
24
|
+
|
|
25
|
+
return parser.parse_args(args)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _debug_info() -> None:
|
|
29
|
+
import platform
|
|
30
|
+
|
|
31
|
+
print(f"errortools v{__version__}")
|
|
32
|
+
print(f" Python: {sys.version}")
|
|
33
|
+
print(f" Platform: {platform.platform()}")
|
|
34
|
+
print(f" Arch: {platform.machine()}")
|
|
35
|
+
print(f" Prefix: {sys.prefix}")
|
|
36
|
+
print(f" Exec: {sys.executable}")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
__import__("_errortools._speedup")
|
|
40
|
+
print(" C speedup: available")
|
|
41
|
+
except ImportError:
|
|
42
|
+
print(" C speedup: not available (pure Python fallback)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _reset_cache() -> None:
|
|
46
|
+
root = Path(__file__).resolve().parent.parent
|
|
47
|
+
count = 0
|
|
48
|
+
|
|
49
|
+
for d in root.rglob("__pycache__"):
|
|
50
|
+
if d.is_dir():
|
|
51
|
+
shutil.rmtree(d)
|
|
52
|
+
count += 1
|
|
53
|
+
|
|
54
|
+
for name in ["htmlcov", ".pytest_cache", ".mypy_cache"]:
|
|
55
|
+
d = root / name
|
|
56
|
+
if d.is_dir():
|
|
57
|
+
shutil.rmtree(d)
|
|
58
|
+
count += 1
|
|
59
|
+
|
|
60
|
+
print(f"Reset complete. Cleared {count} cached directories.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_install() -> None:
|
|
64
|
+
checks: list[tuple[str, bool]] = [("errortools importable", True)]
|
|
65
|
+
|
|
66
|
+
for label, import_path in [
|
|
67
|
+
("C extension (_speedup)", "_errortools._speedup"),
|
|
68
|
+
("pytest", "pytest"),
|
|
69
|
+
("logging module", "_errortools.logging"),
|
|
70
|
+
("future module", "_errortools.future"),
|
|
71
|
+
("typing_extensions", "typing_extensions"),
|
|
72
|
+
]:
|
|
73
|
+
try:
|
|
74
|
+
__import__(import_path)
|
|
75
|
+
checks.append((label, True))
|
|
76
|
+
except ImportError:
|
|
77
|
+
checks.append((label, False))
|
|
78
|
+
|
|
79
|
+
for name, ok in checks:
|
|
80
|
+
status = "OK" if ok else "MISSING"
|
|
81
|
+
print(f" [{status:>7s}] {name}")
|
|
82
|
+
|
|
83
|
+
print()
|
|
84
|
+
if all(ok for _, ok in checks):
|
|
85
|
+
print("All checks passed.")
|
|
86
|
+
else:
|
|
87
|
+
print("Some checks failed. Install missing dependencies.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_FLAG_ACTIONS = {
|
|
91
|
+
"debug": _debug_info,
|
|
92
|
+
"reset": _reset_cache,
|
|
93
|
+
"check": _check_install,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main() -> None:
|
|
98
|
+
args = _parse_args(sys.argv[1:])
|
|
99
|
+
|
|
100
|
+
for flag, action in _FLAG_ACTIONS.items():
|
|
101
|
+
if getattr(args, flag, False):
|
|
102
|
+
action()
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
_parse_args(["--help"])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -87,6 +87,45 @@ static PyObject* fast_append_exception(PyObject* self, PyObject* const* args, Py
|
|
|
87
87
|
Py_RETURN_NONE;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/* Fast suppress exit for context managers
|
|
91
|
+
*
|
|
92
|
+
* Combined None-check + issubclass optimized for __exit__ methods.
|
|
93
|
+
* Returns True (suppress) if typ is not None and is a subclass of excs,
|
|
94
|
+
* False otherwise. Never raises on None — just returns False.
|
|
95
|
+
*
|
|
96
|
+
* Args:
|
|
97
|
+
* typ: The exception type (or None if no exception)
|
|
98
|
+
* excs: The exception class(es) to match against (tuple)
|
|
99
|
+
*
|
|
100
|
+
* Returns:
|
|
101
|
+
* True if exception should be suppressed, False otherwise
|
|
102
|
+
* NULL on error with exception set
|
|
103
|
+
*/
|
|
104
|
+
static PyObject* fast_suppress_exit(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
105
|
+
if (nargs != 2) {
|
|
106
|
+
PyErr_Format(PyExc_TypeError,
|
|
107
|
+
"fast_suppress_exit() takes exactly 2 arguments (%zd given)",
|
|
108
|
+
nargs);
|
|
109
|
+
return NULL;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
PyObject *typ = args[0];
|
|
113
|
+
|
|
114
|
+
if (typ == Py_None) {
|
|
115
|
+
Py_RETURN_FALSE;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
int result = PyObject_IsSubclass(typ, args[1]);
|
|
119
|
+
if (result == -1) {
|
|
120
|
+
return NULL;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (result) {
|
|
124
|
+
Py_RETURN_TRUE;
|
|
125
|
+
}
|
|
126
|
+
Py_RETURN_FALSE;
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
/* Method definitions */
|
|
91
130
|
static PyMethodDef SpeedupMethods[] = {
|
|
92
131
|
{
|
|
@@ -106,6 +145,14 @@ static PyMethodDef SpeedupMethods[] = {
|
|
|
106
145
|
"Append an exception to a list with minimal overhead.\n"
|
|
107
146
|
"Validates that the first argument is a list."
|
|
108
147
|
},
|
|
148
|
+
{
|
|
149
|
+
"fast_suppress_exit",
|
|
150
|
+
(PyCFunction)fast_suppress_exit,
|
|
151
|
+
METH_FASTCALL,
|
|
152
|
+
"fast_suppress_exit(typ, excs) -> bool\n\n"
|
|
153
|
+
"Return True if typ is not None and is a subclass of excs.\n"
|
|
154
|
+
"Optimized for context manager __exit__ methods."
|
|
155
|
+
},
|
|
109
156
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
110
157
|
};
|
|
111
158
|
|
|
@@ -116,7 +163,8 @@ static struct PyModuleDef speedupmodule = {
|
|
|
116
163
|
"C speedup module for errortools\n\n"
|
|
117
164
|
"Provides optimized implementations of exception handling operations:\n"
|
|
118
165
|
" - fast_issubclass_check: Quick exception type hierarchy checking\n"
|
|
119
|
-
" - fast_append_exception: Efficient exception list append operations"
|
|
166
|
+
" - fast_append_exception: Efficient exception list append operations\n"
|
|
167
|
+
" - fast_suppress_exit: Combined None-check + issubclass for __exit__",
|
|
120
168
|
-1, /* size */
|
|
121
169
|
SpeedupMethods /* methods */
|
|
122
170
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Base classes."""
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from typing import Any, Literal, Union
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
import copy
|
|
4
|
+
import shutil
|
|
5
|
+
import csv
|
|
6
|
+
import configparser
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 15):
|
|
10
|
+
from typing import disjoint_base
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import disjoint_base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _check_methods(C: type[Any], *methods: str) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
16
|
+
"""Check methods in `C`. If has, return `True`, else `NotImplemented`."""
|
|
17
|
+
# from `_collections_abc.py`.
|
|
18
|
+
# Copyright 2007 Google, Inc. All Rights Reserved.
|
|
19
|
+
# Licensed to PSF under a Contributor Agreement.
|
|
20
|
+
mro: tuple[type[Any], ...] = C.__mro__ # Added type hints for mro var
|
|
21
|
+
for method in methods:
|
|
22
|
+
for B in mro:
|
|
23
|
+
if method in B.__dict__:
|
|
24
|
+
if B.__dict__[method] is None:
|
|
25
|
+
return NotImplemented
|
|
26
|
+
break
|
|
27
|
+
else:
|
|
28
|
+
return NotImplemented
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------
|
|
33
|
+
# ErrorCodeable
|
|
34
|
+
# ----------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@disjoint_base
|
|
38
|
+
class ErrorCodeable(ABC):
|
|
39
|
+
"""Abstract Base Class for exceptions that carry a machine-readable error code.
|
|
40
|
+
|
|
41
|
+
Follows the ``collections.abc`` pattern: any class that exposes both a
|
|
42
|
+
``code`` class attribute (``int``) and a ``default_detail`` class attribute
|
|
43
|
+
(``str``) is recognised as a virtual subclass automatically, without
|
|
44
|
+
explicit inheritance.
|
|
45
|
+
|
|
46
|
+
Concrete subclasses **must** implement:
|
|
47
|
+
- ``code`` — integer error code (class variable)
|
|
48
|
+
- ``default_detail`` — fallback human-readable message (class variable)
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
|
|
52
|
+
>>> class PaymentError(ErrorCodeable, Exception):
|
|
53
|
+
... code = 6001
|
|
54
|
+
... default_detail = "Payment failed."
|
|
55
|
+
>>> issubclass(PaymentError, ErrorCodeable)
|
|
56
|
+
True
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
__slots__ = ()
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
63
|
+
"""Recognise any class that defines ``code`` and ``default_detail``."""
|
|
64
|
+
if cls is ErrorCodeable:
|
|
65
|
+
return _check_methods(C, "code", "default_detail")
|
|
66
|
+
return NotImplemented
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def code(self) -> int:
|
|
71
|
+
"""Integer error code identifying this exception type."""
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def default_detail(self) -> str:
|
|
76
|
+
"""Fallback human-readable message used when no detail is provided."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ----------------------------------------------------------------------
|
|
80
|
+
# Warnable
|
|
81
|
+
# ----------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Warnable(ABC):
|
|
85
|
+
"""Abstract Base Class for warning classes that can emit themselves.
|
|
86
|
+
|
|
87
|
+
Any class that exposes an ``emit`` classmethod is recognised as a
|
|
88
|
+
virtual subclass automatically via ``__subclasshook__``.
|
|
89
|
+
|
|
90
|
+
Concrete subclasses **must** implement:
|
|
91
|
+
- ``emit(cls, detail, stacklevel)`` — issue the warning via ``warnings.warn``
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
|
|
95
|
+
>>> class SlowWarning(Warnable, Warning):
|
|
96
|
+
... default_detail = "This operation is slow."
|
|
97
|
+
... @classmethod
|
|
98
|
+
... def emit(cls, detail=None, stacklevel=2):
|
|
99
|
+
... import warnings
|
|
100
|
+
... warnings.warn(cls(detail), stacklevel=stacklevel)
|
|
101
|
+
>>> issubclass(SlowWarning, Warnable)
|
|
102
|
+
True
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
__slots__ = ()
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
109
|
+
"""Recognise any class that defines an ``emit`` classmethod."""
|
|
110
|
+
if cls is Warnable:
|
|
111
|
+
return _check_methods(C, "emit")
|
|
112
|
+
return NotImplemented
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def emit(cls, detail: str | None = None, stacklevel: int = 2) -> None:
|
|
117
|
+
"""Issue this warning via ``warnings.warn``.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
detail: Optional message override.
|
|
121
|
+
stacklevel: Passed to ``warnings.warn``; ``2`` points at the
|
|
122
|
+
caller of ``emit``.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ----------------------------------------------------------------------
|
|
127
|
+
# Raiseable
|
|
128
|
+
# ----------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Raiseable(ABC):
|
|
132
|
+
"""Abstract Base Class for objects that know how to raise themselves.
|
|
133
|
+
|
|
134
|
+
Concrete subclasses **must** implement ``raise_it()``, which should
|
|
135
|
+
raise ``self`` (or a derived exception). Any class that exposes a
|
|
136
|
+
``raise_it`` method is recognised as a virtual subclass automatically.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
|
|
140
|
+
>>> class MyError(Raiseable, Exception):
|
|
141
|
+
... def raise_it(self):
|
|
142
|
+
... raise self
|
|
143
|
+
>>> e = MyError("oops")
|
|
144
|
+
>>> e.raise_it()
|
|
145
|
+
Traceback (most recent call last):
|
|
146
|
+
...
|
|
147
|
+
MyError: oops
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
__slots__ = ()
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def __subclasshook__(cls, C: type[Any]) -> Union[bool, Literal[NotImplemented]]: # type: ignore
|
|
154
|
+
"""Recognise any class that defines a ``raise_it`` method."""
|
|
155
|
+
if cls is Raiseable:
|
|
156
|
+
return _check_methods(C, "raise_it")
|
|
157
|
+
return NotImplemented
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
def raise_it(self) -> None:
|
|
161
|
+
"""Raise this object as an exception.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
self: Or a derived exception wrapping this object.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ----------------------------------------------------------------------
|
|
169
|
+
# Error
|
|
170
|
+
# ----------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Error(Exception, ABC):
|
|
174
|
+
"""Abstract Base Class for module-level Error exceptions.
|
|
175
|
+
|
|
176
|
+
Any class named **"Error"** (like copy.Error, shutil.Error, csv.Error)
|
|
177
|
+
is automatically recognised as a virtual subclass of this ABC.
|
|
178
|
+
|
|
179
|
+
Virtual subclasses do NOT need to explicitly inherit from this class.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
|
|
183
|
+
>>> import copy
|
|
184
|
+
>>> import shutil
|
|
185
|
+
>>> isinstance(copy.Error(), Error)
|
|
186
|
+
True
|
|
187
|
+
>>> isinstance(shutil.Error(), Error)
|
|
188
|
+
True
|
|
189
|
+
>>> class MyError:
|
|
190
|
+
... __name__ = "Error"
|
|
191
|
+
>>> isinstance(MyError(), Error)
|
|
192
|
+
True
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
__slots__ = ()
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def __subclasshook__(cls, subclass: type[Any]) -> bool:
|
|
199
|
+
if cls is Error:
|
|
200
|
+
return subclass.__name__ == "Error"
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
Error.register(copy.Error)
|
|
205
|
+
Error.register(shutil.Error)
|
|
206
|
+
Error.register(csv.Error)
|
|
207
|
+
Error.register(configparser.Error)
|