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.
Files changed (86) hide show
  1. errortools-3.2.0/PKG-INFO +151 -0
  2. errortools-3.2.0/README.md +105 -0
  3. errortools-3.2.0/_errortools/__main__.py +109 -0
  4. {errortools-3.1.0 → errortools-3.2.0}/_errortools/_speedup.c +49 -1
  5. errortools-3.2.0/_errortools/classes/__init__.py +1 -0
  6. errortools-3.2.0/_errortools/classes/abc.py +207 -0
  7. errortools-3.2.0/_errortools/classes/errorcodes.py +273 -0
  8. errortools-3.2.0/_errortools/classes/group.py +121 -0
  9. errortools-3.2.0/_errortools/classes/warn.py +124 -0
  10. {errortools-3.1.0 → errortools-3.2.0}/_errortools/cli.py +34 -41
  11. errortools-3.2.0/_errortools/decorator/__init__.py +1 -0
  12. errortools-3.2.0/_errortools/decorator/cache.py +80 -0
  13. errortools-3.2.0/_errortools/decorator/deprecated.py +61 -0
  14. errortools-3.2.0/_errortools/decorator/handlers.py +90 -0
  15. errortools-3.2.0/_errortools/decorator/retry.py +99 -0
  16. errortools-3.2.0/_errortools/decorator/timeout.py +38 -0
  17. errortools-3.2.0/_errortools/descriptor/__init__.py +2 -0
  18. errortools-3.2.0/_errortools/descriptor/base.py +25 -0
  19. errortools-3.2.0/_errortools/descriptor/errormsg.py +33 -0
  20. errortools-3.2.0/_errortools/descriptor/nonblankmsg.py +46 -0
  21. {errortools-3.1.0 → errortools-3.2.0}/_errortools/future.py +14 -8
  22. {errortools-3.1.0 → errortools-3.2.0}/_errortools/ignore.py +2 -135
  23. errortools-3.2.0/_errortools/logging/__init__.py +43 -0
  24. errortools-3.2.0/_errortools/logging/base.py +462 -0
  25. errortools-3.2.0/_errortools/logging/level.py +85 -0
  26. errortools-3.2.0/_errortools/logging/logger.py +13 -0
  27. errortools-3.2.0/_errortools/logging/record.py +116 -0
  28. errortools-3.2.0/_errortools/logging/sink.py +243 -0
  29. {errortools-3.1.0 → errortools-3.2.0}/_errortools/metadata.py +1 -3
  30. {errortools-3.1.0 → errortools-3.2.0}/_errortools/partial.py +1 -3
  31. {errortools-3.1.0 → errortools-3.2.0}/_errortools/raises.py +2 -5
  32. errortools-3.2.0/_errortools/version.py +17 -0
  33. errortools-3.2.0/_errortools/wrappers/__init__.py +2 -0
  34. errortools-3.2.0/_errortools/wrappers/cache.py +93 -0
  35. errortools-3.2.0/_errortools/wrappers/ignore.py +120 -0
  36. errortools-3.2.0/docs/conf.py +56 -0
  37. {errortools-3.1.0 → errortools-3.2.0}/errortools/__init__.py +37 -10
  38. errortools-3.2.0/errortools.egg-info/PKG-INFO +151 -0
  39. {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/SOURCES.txt +32 -2
  40. errortools-3.2.0/errortools.egg-info/requires.txt +2 -0
  41. {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/top_level.txt +5 -0
  42. errortools-3.2.0/pyproject.toml +88 -0
  43. errortools-3.2.0/testing/__init__.py +33 -0
  44. errortools-3.2.0/testing/benchmark/__init__.py +1 -0
  45. errortools-3.2.0/testing/benchmark/test_future_perf.py +239 -0
  46. {errortools-3.1.0 → errortools-3.2.0}/testing/run_tests.py +3 -3
  47. {errortools-3.1.0 → errortools-3.2.0}/testing/test_decorator.py +288 -0
  48. {errortools-3.1.0 → errortools-3.2.0}/testing/test_descriptor.py +54 -10
  49. {errortools-3.1.0 → errortools-3.2.0}/testing/test_ignore.py +1 -174
  50. {errortools-3.1.0 → errortools-3.2.0}/testing/test_logging.py +0 -1
  51. errortools-3.2.0/testing/test_testing/__init__.py +1 -0
  52. errortools-3.2.0/testing/test_testing/test_testing.py +41 -0
  53. errortools-3.1.0/PKG-INFO +0 -367
  54. errortools-3.1.0/README.md +0 -329
  55. errortools-3.1.0/_errortools/version.py +0 -7
  56. errortools-3.1.0/errortools.egg-info/PKG-INFO +0 -367
  57. errortools-3.1.0/errortools.egg-info/requires.txt +0 -2
  58. errortools-3.1.0/setup.py +0 -58
  59. errortools-3.1.0/testing/__init__.py +0 -11
  60. {errortools-3.1.0 → errortools-3.2.0}/AUTHORS.txt +0 -0
  61. {errortools-3.1.0 → errortools-3.2.0}/LICENSE.txt +0 -0
  62. {errortools-3.1.0 → errortools-3.2.0}/_errortools/__init__.py +0 -0
  63. {errortools-3.1.0 → errortools-3.2.0}/_errortools/_cli.py +0 -0
  64. {errortools-3.1.0 → errortools-3.2.0}/_errortools/const.py +0 -0
  65. {errortools-3.1.0 → errortools-3.2.0}/_errortools/errno.py +0 -0
  66. {errortools-3.1.0 → errortools-3.2.0}/_errortools/py.typed +0 -0
  67. {errortools-3.1.0 → errortools-3.2.0}/_errortools/typing.py +0 -0
  68. {errortools-3.1.0 → errortools-3.2.0}/errortools/__main__.py +0 -0
  69. {errortools-3.1.0 → errortools-3.2.0}/errortools/future.py +0 -0
  70. {errortools-3.1.0 → errortools-3.2.0}/errortools/logging.py +0 -0
  71. {errortools-3.1.0 → errortools-3.2.0}/errortools/partial.py +0 -0
  72. {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/dependency_links.txt +0 -0
  73. {errortools-3.1.0 → errortools-3.2.0}/errortools.egg-info/entry_points.txt +0 -0
  74. {errortools-3.1.0 → errortools-3.2.0}/setup.cfg +0 -0
  75. {errortools-3.1.0 → errortools-3.2.0}/testing/__main__.py +0 -0
  76. {errortools-3.1.0 → errortools-3.2.0}/testing/conftest.py +0 -0
  77. {errortools-3.1.0 → errortools-3.2.0}/testing/test_abc.py +0 -0
  78. {errortools-3.1.0 → errortools-3.2.0}/testing/test_const.py +0 -0
  79. {errortools-3.1.0 → errortools-3.2.0}/testing/test_errno.py +0 -0
  80. {errortools-3.1.0 → errortools-3.2.0}/testing/test_errorcodes.py +0 -0
  81. {errortools-3.1.0 → errortools-3.2.0}/testing/test_future.py +0 -0
  82. {errortools-3.1.0 → errortools-3.2.0}/testing/test_groups.py +0 -0
  83. {errortools-3.1.0 → errortools-3.2.0}/testing/test_partials.py +0 -0
  84. {errortools-3.1.0 → errortools-3.2.0}/testing/test_raises.py +0 -0
  85. {errortools-3.1.0 → errortools-3.2.0}/testing/test_typing.py +0 -0
  86. {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
+ [![Code Style: Google](https://img.shields.io/badge/style-google-3666d6.svg)](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings)
52
+ [![PyPI Version](https://img.shields.io/pypi/v/errortools)](https://pypi.org/project/errortools/)
53
+ [![Python Versions](https://img.shields.io/pypi/pyversions/errortools)](https://pypi.org/project/errortools/)
54
+ ![This week commits](https://img.shields.io/github/commit-activity/w/more-abc/errortools)
55
+ ![This month commits](https://img.shields.io/github/commit-activity/m/more-abc/errortools)
56
+ ![Past year commits](https://img.shields.io/github/commit-activity/y/more-abc/errortools)
57
+ ![Total commits badge](https://img.shields.io/github/commit-activity/t/more-abc/errortools)
58
+ ![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red)
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
+ [![Code Style: Google](https://img.shields.io/badge/style-google-3666d6.svg)](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings)
6
+ [![PyPI Version](https://img.shields.io/pypi/v/errortools)](https://pypi.org/project/errortools/)
7
+ [![Python Versions](https://img.shields.io/pypi/pyversions/errortools)](https://pypi.org/project/errortools/)
8
+ ![This week commits](https://img.shields.io/github/commit-activity/w/more-abc/errortools)
9
+ ![This month commits](https://img.shields.io/github/commit-activity/m/more-abc/errortools)
10
+ ![Past year commits](https://img.shields.io/github/commit-activity/y/more-abc/errortools)
11
+ ![Total commits badge](https://img.shields.io/github/commit-activity/t/more-abc/errortools)
12
+ ![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red)
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)