errortools 2.2.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {errortools-2.2.0/errortools.egg-info → errortools-2.4.0}/PKG-INFO +27 -5
- {errortools-2.2.0 → errortools-2.4.0}/README.md +25 -4
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/_cli.py +33 -32
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/classes/abc.py +3 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/cli.py +42 -54
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/const.py +6 -4
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/decorator/deprecated.py +28 -0
- errortools-2.4.0/_errortools/errno.py +86 -0
- errortools-2.4.0/_errortools/future.py +165 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/ignore.py +23 -19
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/logging/level.py +4 -4
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/typing.py +4 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/version.py +2 -2
- {errortools-2.2.0 → errortools-2.4.0}/errortools/__init__.py +14 -1
- {errortools-2.2.0 → errortools-2.4.0/errortools.egg-info}/PKG-INFO +27 -5
- {errortools-2.2.0 → errortools-2.4.0}/errortools.egg-info/SOURCES.txt +3 -1
- {errortools-2.2.0 → errortools-2.4.0}/errortools.egg-info/entry_points.txt +1 -1
- errortools-2.4.0/errortools.egg-info/requires.txt +2 -0
- {errortools-2.2.0 → errortools-2.4.0}/setup.py +3 -3
- {errortools-2.2.0 → errortools-2.4.0}/tests/__init__.py +1 -1
- errortools-2.2.0/tests/test_cache.py → errortools-2.4.0/tests/test_decorator.py +153 -2
- errortools-2.4.0/tests/test_errno.py +115 -0
- errortools-2.4.0/tests/test_future.py +302 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_typing.py +9 -0
- errortools-2.2.0/_errortools/future.py +0 -23
- errortools-2.2.0/errortools.egg-info/requires.txt +0 -1
- errortools-2.2.0/tests/test_decorator.py +0 -85
- {errortools-2.2.0 → errortools-2.4.0}/AUTHORS.txt +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/LICENSE.txt +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/classes/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/classes/errorcodes.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/classes/group.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/classes/warn.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/decorator/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/decorator/cache.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/descriptor/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/descriptor/errormsg.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/descriptor/nonblankmsg.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/logging/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/logging/base.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/logging/logger.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/logging/record.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/logging/sink.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/metadata.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/methods/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/methods/errorattr.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/methods/errordelattr.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/methods/errorhasattr.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/methods/errorsetattr.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/partial.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/py.typed +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/raises.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/wrappers/__init__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/wrappers/cache.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/_errortools/wrappers/ignore.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/errortools/__main__.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/errortools.egg-info/dependency_links.txt +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/errortools.egg-info/top_level.txt +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/setup.cfg +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/conftest.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/run_tests.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_abc.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_const.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_descriptor.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_errorcodes.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_groups.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_ignore.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_logging.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_mixins.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_partials.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_raises.py +0 -0
- {errortools-2.2.0 → errortools-2.4.0}/tests/test_warnings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: errortools
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
|
|
5
5
|
Home-page: https://github.com/more-abc/errortools
|
|
6
6
|
Author: Evan Yang
|
|
@@ -20,6 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE.txt
|
|
21
21
|
License-File: AUTHORS.txt
|
|
22
22
|
Requires-Dist: namebyauthor==1.0.0
|
|
23
|
+
Requires-Dist: typing_extensions>=4.8.0
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: author-email
|
|
25
26
|
Dynamic: classifier
|
|
@@ -38,6 +39,7 @@ A lightweight Python exception handling utility library.
|
|
|
38
39
|
## Features
|
|
39
40
|
- **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
|
|
40
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
|
|
41
43
|
- **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
|
|
42
44
|
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
|
|
43
45
|
- **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
|
|
@@ -61,7 +63,7 @@ from errortools import (
|
|
|
61
63
|
error_cache,
|
|
62
64
|
PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
|
|
63
65
|
)
|
|
64
|
-
from errortools.future import super_fast_ignore
|
|
66
|
+
from errortools.future import super_fast_ignore, super_fast_catch, super_fast_reraise, ExceptionCollector
|
|
65
67
|
|
|
66
68
|
# ── 1. ignore ── context manager with full metadata ──────────────────────────
|
|
67
69
|
with ignore(KeyError) as err:
|
|
@@ -153,7 +155,27 @@ raises_all(
|
|
|
153
155
|
exc = assert_raises(int, [ValueError], "not-a-number")
|
|
154
156
|
print(exc) # invalid literal for int() with base 10: 'not-a-number'
|
|
155
157
|
|
|
156
|
-
# ── 10.
|
|
158
|
+
# ── 10. super_fast_catch ── lightweight exception capture ──────────────────────
|
|
159
|
+
with super_fast_catch(ValueError) as ctx:
|
|
160
|
+
raise ValueError("oops")
|
|
161
|
+
|
|
162
|
+
assert ctx.exception is not None
|
|
163
|
+
print(ctx.exception) # ValueError('oops')
|
|
164
|
+
|
|
165
|
+
# ── 11. super_fast_reraise ── lightweight exception type conversion ────────────
|
|
166
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
167
|
+
raise KeyError("missing") # → ValueError: 'missing'
|
|
168
|
+
|
|
169
|
+
# ── 12. ExceptionCollector ── batch collect exceptions ───────────────────────────
|
|
170
|
+
collector = ExceptionCollector()
|
|
171
|
+
collector.catch(int, "bad1")
|
|
172
|
+
collector.catch(int, "bad2")
|
|
173
|
+
|
|
174
|
+
if collector.has_errors:
|
|
175
|
+
print(f"Collected {collector.count} errors")
|
|
176
|
+
collector.raise_all("batch operation failed") # → ExceptionGroup (2 sub-exceptions)
|
|
177
|
+
|
|
178
|
+
# ── 13. error_cache ── cache exceptions by call arguments ─────────────────────
|
|
157
179
|
@error_cache(maxsize=64)
|
|
158
180
|
def load(user_id: int) -> dict:
|
|
159
181
|
if user_id < 0:
|
|
@@ -166,7 +188,7 @@ with ignore(ValueError):
|
|
|
166
188
|
print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
|
|
167
189
|
load.clear_cache()
|
|
168
190
|
|
|
169
|
-
# ──
|
|
191
|
+
# ── 14. Custom exceptions — three layers ──────────────────────────────────────
|
|
170
192
|
|
|
171
193
|
# Layer 1: PureBaseException — code + detail only
|
|
172
194
|
class AppError(PureBaseException):
|
|
@@ -202,7 +224,7 @@ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [
|
|
|
202
224
|
raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
|
|
203
225
|
raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
|
|
204
226
|
|
|
205
|
-
# ──
|
|
227
|
+
# ── 15. BaseWarning ── structured warnings with factory methods ───────────────
|
|
206
228
|
class ExperimentalWarning(BaseWarning):
|
|
207
229
|
default_detail = "This feature is experimental."
|
|
208
230
|
|
|
@@ -4,6 +4,7 @@ A lightweight Python exception handling utility library.
|
|
|
4
4
|
## Features
|
|
5
5
|
- **Raise Exceptions**: `raises()`, `raises_all()`, `reraise()` — batch raising and exception conversion
|
|
6
6
|
- **Catch & Suppress**: `ignore()`, `ignore_subclass()`, `ignore_warns()`, `fast_ignore()`, `super_fast_ignore()`, `timeout()`, `retry()` — graceful suppression of exceptions and warnings, with automatic retry
|
|
7
|
+
- **Future Utilities**: `super_fast_catch()`, `super_fast_reraise()`, `ExceptionCollector` — lightweight exception handling for high-performance scenarios
|
|
7
8
|
- **Exception Caching**: `error_cache` — cache exceptions raised by functions (similar to `lru_cache`)
|
|
8
9
|
- **Custom Exceptions**: `PureBaseException`, `ContextException`, `BaseErrorCodes`, `BaseWarning` — structured exception classes with error codes, trace IDs, and context
|
|
9
10
|
- **Attribute Error Mixin**: Customize error behavior for attribute access, assignment, and deletion
|
|
@@ -27,7 +28,7 @@ from errortools import (
|
|
|
27
28
|
error_cache,
|
|
28
29
|
PureBaseException, ContextException, BaseErrorCodes, BaseWarning,
|
|
29
30
|
)
|
|
30
|
-
from errortools.future import super_fast_ignore
|
|
31
|
+
from errortools.future import super_fast_ignore, super_fast_catch, super_fast_reraise, ExceptionCollector
|
|
31
32
|
|
|
32
33
|
# ── 1. ignore ── context manager with full metadata ──────────────────────────
|
|
33
34
|
with ignore(KeyError) as err:
|
|
@@ -119,7 +120,27 @@ raises_all(
|
|
|
119
120
|
exc = assert_raises(int, [ValueError], "not-a-number")
|
|
120
121
|
print(exc) # invalid literal for int() with base 10: 'not-a-number'
|
|
121
122
|
|
|
122
|
-
# ── 10.
|
|
123
|
+
# ── 10. super_fast_catch ── lightweight exception capture ──────────────────────
|
|
124
|
+
with super_fast_catch(ValueError) as ctx:
|
|
125
|
+
raise ValueError("oops")
|
|
126
|
+
|
|
127
|
+
assert ctx.exception is not None
|
|
128
|
+
print(ctx.exception) # ValueError('oops')
|
|
129
|
+
|
|
130
|
+
# ── 11. super_fast_reraise ── lightweight exception type conversion ────────────
|
|
131
|
+
with super_fast_reraise(KeyError, ValueError):
|
|
132
|
+
raise KeyError("missing") # → ValueError: 'missing'
|
|
133
|
+
|
|
134
|
+
# ── 12. ExceptionCollector ── batch collect exceptions ───────────────────────────
|
|
135
|
+
collector = ExceptionCollector()
|
|
136
|
+
collector.catch(int, "bad1")
|
|
137
|
+
collector.catch(int, "bad2")
|
|
138
|
+
|
|
139
|
+
if collector.has_errors:
|
|
140
|
+
print(f"Collected {collector.count} errors")
|
|
141
|
+
collector.raise_all("batch operation failed") # → ExceptionGroup (2 sub-exceptions)
|
|
142
|
+
|
|
143
|
+
# ── 13. error_cache ── cache exceptions by call arguments ─────────────────────
|
|
123
144
|
@error_cache(maxsize=64)
|
|
124
145
|
def load(user_id: int) -> dict:
|
|
125
146
|
if user_id < 0:
|
|
@@ -132,7 +153,7 @@ with ignore(ValueError):
|
|
|
132
153
|
print(load.cache_info()) # CacheInfo(hits=0, misses=1, maxsize=64, currsize=1)
|
|
133
154
|
load.clear_cache()
|
|
134
155
|
|
|
135
|
-
# ──
|
|
156
|
+
# ── 14. Custom exceptions — three layers ──────────────────────────────────────
|
|
136
157
|
|
|
137
158
|
# Layer 1: PureBaseException — code + detail only
|
|
138
159
|
class AppError(PureBaseException):
|
|
@@ -168,7 +189,7 @@ raise BaseErrorCodes.runtime_failure("crash") # RuntimeFailure [
|
|
|
168
189
|
raise BaseErrorCodes.timeout_failure() # TimeoutFailure [4002]
|
|
169
190
|
raise BaseErrorCodes.configuration_error("missing key") # ConfigurationError [5001]
|
|
170
191
|
|
|
171
|
-
# ──
|
|
192
|
+
# ── 15. BaseWarning ── structured warnings with factory methods ───────────────
|
|
172
193
|
class ExperimentalWarning(BaseWarning):
|
|
173
194
|
default_detail = "This feature is experimental."
|
|
174
195
|
|
|
@@ -1,32 +1,33 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from _errortools.metadata import (
|
|
4
|
-
__author__,
|
|
5
|
-
__author_email__,
|
|
6
|
-
__copyright__,
|
|
7
|
-
__description__,
|
|
8
|
-
__license__,
|
|
9
|
-
__url__,
|
|
10
|
-
)
|
|
11
|
-
from _errortools.version import __version__
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _cmd_log(message: str, level: str, output: str) -> None:
|
|
15
|
-
"""Emit a single log message via the errortools logger."""
|
|
16
|
-
from .logging import BaseLogger
|
|
17
|
-
from .logging.level import get_level
|
|
18
|
-
|
|
19
|
-
stream = sys.stdout if output == "stdout" else sys.stderr
|
|
20
|
-
log = BaseLogger(name="errortools-cli")
|
|
21
|
-
log.
|
|
22
|
-
log.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
print(f"
|
|
29
|
-
print(f"
|
|
30
|
-
print(f"
|
|
31
|
-
print(f"
|
|
32
|
-
print(f"
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from _errortools.metadata import (
|
|
4
|
+
__author__,
|
|
5
|
+
__author_email__,
|
|
6
|
+
__copyright__,
|
|
7
|
+
__description__,
|
|
8
|
+
__license__,
|
|
9
|
+
__url__,
|
|
10
|
+
)
|
|
11
|
+
from _errortools.version import __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cmd_log(message: str, level: str, output: str) -> None:
|
|
15
|
+
"""Emit a single log message via the errortools logger."""
|
|
16
|
+
from .logging import BaseLogger
|
|
17
|
+
from .logging.level import get_level
|
|
18
|
+
|
|
19
|
+
stream = sys.stdout if output == "stdout" else sys.stderr
|
|
20
|
+
log = BaseLogger(name="errortools-cli")
|
|
21
|
+
log.set_level("TRACE")
|
|
22
|
+
log.add(stream, level=level, colorize=None)
|
|
23
|
+
log.log(get_level(level), message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print_info() -> None:
|
|
27
|
+
"""Print a summary of all package metadata."""
|
|
28
|
+
print(f"errortools v{__version__}")
|
|
29
|
+
print(f" {__description__}")
|
|
30
|
+
print(f" Author: {__author__} <{__author_email__}>")
|
|
31
|
+
print(f" License: {__license__}")
|
|
32
|
+
print(f" URL: {__url__}")
|
|
33
|
+
print(f" Copyright: {__copyright__}")
|
|
@@ -5,6 +5,7 @@ import shutil
|
|
|
5
5
|
import csv
|
|
6
6
|
import configparser
|
|
7
7
|
|
|
8
|
+
from typing_extensions import disjoint_base # I use 3.14.3
|
|
8
9
|
from ..methods import (
|
|
9
10
|
ErrorAttrMixin,
|
|
10
11
|
ErrorAttrCheckMixin,
|
|
@@ -30,6 +31,7 @@ def _check_methods(C: type[Any], *methods: str) -> Union[bool, Literal[NotImplem
|
|
|
30
31
|
return True
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
@disjoint_base
|
|
33
35
|
class ErrorAttrable(ABC):
|
|
34
36
|
"""
|
|
35
37
|
Abstract Base Class (ABC) for classes supporting custom attribute error handling.
|
|
@@ -175,6 +177,7 @@ ErrorAttrable.register(ErrorSetAttrMixin)
|
|
|
175
177
|
# ----------------------------------------------------------------------
|
|
176
178
|
|
|
177
179
|
|
|
180
|
+
@disjoint_base
|
|
178
181
|
class ErrorCodeable(ABC):
|
|
179
182
|
"""Abstract Base Class for exceptions that carry a machine-readable error code.
|
|
180
183
|
|
|
@@ -14,107 +14,95 @@ from .metadata import (
|
|
|
14
14
|
__url__,
|
|
15
15
|
)
|
|
16
16
|
from .version import __version__
|
|
17
|
-
from tests.run_tests import run_tests
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
21
20
|
"""Parse command line arguments."""
|
|
21
|
+
is_logger = "logger" in sys.argv[0]
|
|
22
|
+
|
|
23
|
+
if is_logger:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="logger",
|
|
26
|
+
description="Logger CLI - Emit log messages from command line",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument("message", help="Log message")
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--level",
|
|
31
|
+
"-l",
|
|
32
|
+
default="info",
|
|
33
|
+
choices=[
|
|
34
|
+
"trace",
|
|
35
|
+
"debug",
|
|
36
|
+
"info",
|
|
37
|
+
"success",
|
|
38
|
+
"warning",
|
|
39
|
+
"error",
|
|
40
|
+
"critical",
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--output", "-o", choices=["stderr", "stdout"], default="stderr"
|
|
45
|
+
)
|
|
46
|
+
return parser.parse_args(args)
|
|
47
|
+
|
|
48
|
+
prog = "errortools"
|
|
49
|
+
desc = __description__
|
|
50
|
+
|
|
22
51
|
if sys.version_info >= (3, 14):
|
|
23
|
-
parser = argparse.ArgumentParser(description=
|
|
52
|
+
parser = argparse.ArgumentParser(description=desc, prog=prog, color=True)
|
|
24
53
|
else:
|
|
25
|
-
parser = argparse.ArgumentParser(description=
|
|
54
|
+
parser = argparse.ArgumentParser(description=desc, prog=prog)
|
|
26
55
|
|
|
27
56
|
parser.add_argument(
|
|
28
57
|
"-v", "--version", action="store_true", help="Show version and exit"
|
|
29
58
|
)
|
|
30
|
-
|
|
31
59
|
parser.add_argument(
|
|
32
60
|
"-c", "--copyrights", action="store_true", help="Show copyright information"
|
|
33
61
|
)
|
|
34
|
-
|
|
35
62
|
parser.add_argument("-a", "--author", action="store_true", help="Show author name")
|
|
36
|
-
|
|
37
63
|
parser.add_argument("-e", "--email", action="store_true", help="Show author email")
|
|
38
|
-
|
|
39
64
|
parser.add_argument(
|
|
40
65
|
"-l", "--license", action="store_true", help="Show license type"
|
|
41
66
|
)
|
|
42
|
-
|
|
43
67
|
parser.add_argument("-u", "--url", action="store_true", help="Show project URL")
|
|
44
|
-
|
|
45
68
|
parser.add_argument(
|
|
46
69
|
"-i", "--info", action="store_true", help="Show all package information"
|
|
47
70
|
)
|
|
48
|
-
|
|
49
71
|
parser.add_argument(
|
|
50
|
-
"--run-tests",
|
|
51
|
-
action="store_true",
|
|
52
|
-
help="Run tests for errortools module. (Using pytest)",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
subparsers = parser.add_subparsers(dest="subcommand")
|
|
56
|
-
|
|
57
|
-
log_parser = subparsers.add_parser(
|
|
58
|
-
"log",
|
|
59
|
-
help="Emit a log message from the command line",
|
|
60
|
-
)
|
|
61
|
-
log_parser.add_argument(
|
|
62
|
-
"message",
|
|
63
|
-
help="The message to log",
|
|
64
|
-
)
|
|
65
|
-
log_parser.add_argument(
|
|
66
|
-
"--level",
|
|
67
|
-
"-l",
|
|
68
|
-
default="info",
|
|
69
|
-
choices=["trace", "debug", "info", "success", "warning", "error", "critical"],
|
|
70
|
-
help="Log level (default: info)",
|
|
71
|
-
)
|
|
72
|
-
log_parser.add_argument(
|
|
73
|
-
"--output",
|
|
74
|
-
"-o",
|
|
75
|
-
choices=["stderr", "stdout"],
|
|
76
|
-
default="stderr",
|
|
77
|
-
help="Output stream (default: stderr)",
|
|
72
|
+
"--run-tests", action="store_true", help="Run tests using pytest"
|
|
78
73
|
)
|
|
79
74
|
|
|
80
75
|
return parser.parse_args(args)
|
|
81
76
|
|
|
82
77
|
|
|
83
|
-
def
|
|
84
|
-
"""Logging main CLI entry point."""
|
|
78
|
+
def main() -> None:
|
|
85
79
|
args = parse_args(sys.argv[1:])
|
|
86
80
|
|
|
87
|
-
if
|
|
81
|
+
if "logger" in sys.argv[0]:
|
|
88
82
|
_cmd_log(args.message, args.level, args.output)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def main() -> None:
|
|
92
|
-
"""Main CLI entry point."""
|
|
93
|
-
args = parse_args(sys.argv[1:])
|
|
83
|
+
return
|
|
94
84
|
|
|
95
85
|
if args.version:
|
|
96
86
|
print(f"v{__version__}")
|
|
97
|
-
|
|
98
87
|
elif args.copyrights:
|
|
99
88
|
print(__copyright__)
|
|
100
|
-
|
|
101
89
|
elif args.author:
|
|
102
90
|
print(f"Author: {__author__}")
|
|
103
|
-
|
|
104
91
|
elif args.email:
|
|
105
92
|
print(f"Email: {__author_email__}")
|
|
106
|
-
|
|
107
93
|
elif args.license:
|
|
108
94
|
print(f"License: {__license__}")
|
|
109
|
-
|
|
110
95
|
elif args.url:
|
|
111
96
|
print(f"URL: {__url__}")
|
|
112
|
-
|
|
113
97
|
elif args.run_tests:
|
|
114
|
-
run_tests
|
|
98
|
+
from tests.run_tests import run_tests
|
|
115
99
|
|
|
100
|
+
run_tests()
|
|
116
101
|
elif args.info:
|
|
117
102
|
_print_info()
|
|
118
|
-
|
|
119
103
|
else:
|
|
120
104
|
parse_args(["--help"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -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
|
|
@@ -31,3 +31,31 @@ def deprecated(
|
|
|
31
31
|
return wrapper
|
|
32
32
|
|
|
33
33
|
return decorator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def experimental(
|
|
37
|
+
reason: str = "This function is experimental and may change in future versions.",
|
|
38
|
+
) -> Callable:
|
|
39
|
+
"""Decorator that marks a function as experimental.
|
|
40
|
+
|
|
41
|
+
Emits a UserWarning when the decorated function is called.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
reason: Optional message explaining the experimental status and any caveats.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> @experimental(reason="API may change without notice.")
|
|
48
|
+
... def new_feature():
|
|
49
|
+
... pass
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def decorator(func: Callable) -> Callable:
|
|
53
|
+
@wraps(func)
|
|
54
|
+
def wrapper(*args, **kwargs):
|
|
55
|
+
msg = f"{func.__name__} is experimental. {reason}"
|
|
56
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
57
|
+
return func(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
return wrapper
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
@@ -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()
|