errortools 3.4.0__tar.gz → 3.5.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.4.0/errortools.egg-info → errortools-3.5.0}/PKG-INFO +4 -2
- errortools-3.5.0/_errortools/_speedup.c +277 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/classes/group.py +3 -1
- errortools-3.5.0/_errortools/cli.py +259 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/future.py +4 -2
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/ignore.py +10 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/logging/sink.py +1 -1
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/metadata.py +17 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/plugins.py +4 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/typing.py +8 -4
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/version.py +6 -1
- {errortools-3.4.0 → errortools-3.5.0}/errortools/__init__.py +7 -2
- {errortools-3.4.0 → errortools-3.5.0/errortools.egg-info}/PKG-INFO +4 -2
- {errortools-3.4.0 → errortools-3.5.0}/errortools.egg-info/SOURCES.txt +3 -4
- {errortools-3.4.0 → errortools-3.5.0}/errortools.egg-info/requires.txt +1 -1
- {errortools-3.4.0 → errortools-3.5.0}/pyproject.toml +7 -4
- {errortools-3.4.0 → errortools-3.5.0}/testing/__init__.py +7 -1
- {errortools-3.4.0 → errortools-3.5.0}/testing/run_tests.py +1 -1
- errortools-3.5.0/testing/test_data_driven.py +584 -0
- errortools-3.5.0/testing/test_logger_shell.py +69 -0
- errortools-3.4.0/_errortools/_speedup.c +0 -175
- errortools-3.4.0/_errortools/cli.py +0 -101
- errortools-3.4.0/testing/test_testing/__init__.py +0 -1
- errortools-3.4.0/testing/test_testing/test_testing.py +0 -41
- errortools-3.4.0/testing/test_version.py +0 -71
- {errortools-3.4.0 → errortools-3.5.0}/AUTHORS.txt +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/LICENSE.txt +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/README.md +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/__main__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/_cli.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/classes/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/classes/abc.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/classes/errorcodes.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/classes/warn.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/decorator/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/decorator/cache.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/decorator/deprecated.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/decorator/handlers.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/decorator/retry.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/decorator/timeout.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/descriptor/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/descriptor/base.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/descriptor/errormsg.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/descriptor/nonblankmsg.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/errno.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/logging/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/logging/base.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/logging/level.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/logging/logger.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/logging/record.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/partial.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/py.typed +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/raises.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/wrappers/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/wrappers/cache.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/_errortools/wrappers/ignore.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools/__main__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools/future.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools/logging.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools/partial.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools.egg-info/dependency_links.txt +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools.egg-info/entry_points.txt +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/errortools.egg-info/top_level.txt +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/setup.cfg +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/__main__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/benchmark/__init__.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/benchmark/test_future_perf.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/conftest.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_decorator.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_descriptor.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_errno.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_groups.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_ignore.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_plugins.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_raises.py +0 -0
- {errortools-3.4.0 → errortools-3.5.0}/testing/test_typing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: errortools
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
|
|
5
5
|
Author-email: Evan Yang <quantbit@126.com>
|
|
6
6
|
License: Copyright (c) 2026 authors see AUTHORS.txt
|
|
@@ -35,13 +35,15 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
35
35
|
Classifier: Programming Language :: Python :: 3.12
|
|
36
36
|
Classifier: Programming Language :: Python :: 3.13
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.14
|
|
38
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
39
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
38
40
|
Classifier: Operating System :: OS Independent
|
|
39
41
|
Classifier: Typing :: Typed
|
|
40
42
|
Requires-Python: >=3.8
|
|
41
43
|
Description-Content-Type: text/markdown
|
|
42
44
|
License-File: LICENSE.txt
|
|
43
45
|
License-File: AUTHORS.txt
|
|
44
|
-
Requires-Dist: namebyauthor==1.
|
|
46
|
+
Requires-Dist: namebyauthor==1.2.0
|
|
45
47
|
Requires-Dist: typing_extensions>=4.15.0
|
|
46
48
|
Dynamic: license-file
|
|
47
49
|
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#define PY_SSIZE_T_CLEAN
|
|
2
|
+
#include <Python.h>
|
|
3
|
+
|
|
4
|
+
/* C speedup module for errortools.
|
|
5
|
+
*
|
|
6
|
+
* Provides optimized implementations of exception-handling hot paths used by
|
|
7
|
+
* ``_errortools.future``:
|
|
8
|
+
*
|
|
9
|
+
* - fast_issubclass_check: Quick exception type hierarchy checking
|
|
10
|
+
* - fast_append_exception : Efficient exception list append operation
|
|
11
|
+
* - fast_suppress_exit : Combined None-check + issubclass for __exit__
|
|
12
|
+
*
|
|
13
|
+
* The functions are tuned for the common case where the exception type raised
|
|
14
|
+
* is *exactly* one of the suppressed types. In that case we can avoid the
|
|
15
|
+
* relatively expensive ``PyObject_IsSubclass`` call (which walks the MRO and
|
|
16
|
+
* may invoke ``__subclasscheck__``) and answer with a simple pointer
|
|
17
|
+
* comparison.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/* --- Python version compatibility ---------------------------------------- */
|
|
21
|
+
|
|
22
|
+
/* ``Py_IsNone`` and ``Py_NewRef`` were added in Python 3.10; the GIL slot and
|
|
23
|
+
* multi-interpreter slots were added in 3.12/3.13. We feature-gate the use
|
|
24
|
+
* of those so the module still builds on the minimum supported version
|
|
25
|
+
* (Python 3.8). */
|
|
26
|
+
#if PY_VERSION_HEX >= 0x030A0000
|
|
27
|
+
#define SPEEDUP_IS_NONE(x) Py_IsNone(x)
|
|
28
|
+
#define SPEEDUP_NEW_REF(x) Py_NewRef(x)
|
|
29
|
+
#else
|
|
30
|
+
#define SPEEDUP_IS_NONE(x) ((x) == Py_None)
|
|
31
|
+
#define SPEEDUP_NEW_REF(x) \
|
|
32
|
+
do { \
|
|
33
|
+
Py_INCREF(x); \
|
|
34
|
+
} while (0)
|
|
35
|
+
#endif
|
|
36
|
+
|
|
37
|
+
/* --- Internal helpers --------------------------------------------------- */
|
|
38
|
+
|
|
39
|
+
/* Identity-based fast path for ``typ in excs``.
|
|
40
|
+
*
|
|
41
|
+
* Returns 1 if ``typ`` is exactly equal to one of the types in ``excs``,
|
|
42
|
+
* 0 if it is not, and -1 if the fast path could not answer the question
|
|
43
|
+
* (i.e. ``excs`` is neither a type nor a tuple of types).
|
|
44
|
+
*
|
|
45
|
+
* This avoids the relatively expensive ``PyObject_IsSubclass`` call in the
|
|
46
|
+
* overwhelmingly common case where the exception raised is exactly the type
|
|
47
|
+
* the caller asked about. Note that this is *not* a full subclass check:
|
|
48
|
+
* if the caller needs to also accept subclasses, the result of this
|
|
49
|
+
* function is only meaningful as a fast-path early-out. */
|
|
50
|
+
static inline int identity_match(PyObject *typ, PyObject *excs) {
|
|
51
|
+
if (PyType_Check(excs)) {
|
|
52
|
+
return (typ == excs) ? 1 : 0;
|
|
53
|
+
}
|
|
54
|
+
if (PyTuple_Check(excs)) {
|
|
55
|
+
Py_ssize_t n = PyTuple_GET_SIZE(excs);
|
|
56
|
+
for (Py_ssize_t i = 0; i < n; i++) {
|
|
57
|
+
if (PyTuple_GET_ITEM(excs, i) == typ) {
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
/* Unknown shape: defer to PyObject_IsSubclass. */
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Translate the tri-state return of ``PyObject_IsSubclass`` into a fresh
|
|
68
|
+
* ``bool`` reference (or ``NULL`` on error). */
|
|
69
|
+
static inline PyObject *bool_from_issubclass(int result) {
|
|
70
|
+
if (result == -1) {
|
|
71
|
+
return NULL;
|
|
72
|
+
}
|
|
73
|
+
return result ? Py_True : Py_False;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Combined fast-path answer for the two subclass-checking entry points.
|
|
77
|
+
* Returns:
|
|
78
|
+
* 1 if the identity fast path can answer ``True`` definitively,
|
|
79
|
+
* 0 if it can answer ``False`` definitively (i.e. ``excs`` is an
|
|
80
|
+
* empty tuple, which by definition matches nothing),
|
|
81
|
+
* -1 if the fast path is inconclusive and the caller must fall through
|
|
82
|
+
* to the full ``PyObject_IsSubclass`` check. */
|
|
83
|
+
static inline int fast_path_result(PyObject *typ, PyObject *excs) {
|
|
84
|
+
int fast = identity_match(typ, excs);
|
|
85
|
+
if (fast == 1) {
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
if (PyTuple_Check(excs) && PyTuple_GET_SIZE(excs) == 0) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* --- Public functions --------------------------------------------------- */
|
|
95
|
+
|
|
96
|
+
/* fast_issubclass_check(typ, excs) -> bool
|
|
97
|
+
*
|
|
98
|
+
* Check whether ``typ`` is a subclass of ``excs``. Returns ``False`` if
|
|
99
|
+
* ``typ`` is ``None``. ``excs`` must be a class or a tuple of classes;
|
|
100
|
+
* a ``TypeError`` is raised otherwise. */
|
|
101
|
+
static PyObject *fast_issubclass_check(PyObject *self, PyObject *const *args, Py_ssize_t nargs) {
|
|
102
|
+
(void)self; /* module method - self is unused */
|
|
103
|
+
if (nargs != 2) {
|
|
104
|
+
PyErr_Format(PyExc_TypeError,
|
|
105
|
+
"fast_issubclass_check() takes exactly 2 arguments (%zd given)",
|
|
106
|
+
nargs);
|
|
107
|
+
return NULL;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
PyObject *typ = args[0];
|
|
111
|
+
PyObject *excs = args[1];
|
|
112
|
+
|
|
113
|
+
/* None never matches. */
|
|
114
|
+
if (SPEEDUP_IS_NONE(typ)) {
|
|
115
|
+
Py_RETURN_FALSE;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ``excs`` must be a class or tuple of classes. */
|
|
119
|
+
if (!PyType_Check(excs) && !PyTuple_Check(excs)) {
|
|
120
|
+
PyErr_SetString(PyExc_TypeError,
|
|
121
|
+
"second argument must be a class or tuple of classes");
|
|
122
|
+
return NULL;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Fast path: identity match avoids the MRO walk entirely. */
|
|
126
|
+
int fast = fast_path_result(typ, excs);
|
|
127
|
+
if (fast == 1) {
|
|
128
|
+
Py_RETURN_TRUE;
|
|
129
|
+
}
|
|
130
|
+
if (fast == 0) {
|
|
131
|
+
Py_RETURN_FALSE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Slow path: defer to CPython's full implementation. */
|
|
135
|
+
return bool_from_issubclass(PyObject_IsSubclass(typ, excs));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* fast_append_exception(list, exc) -> None
|
|
139
|
+
*
|
|
140
|
+
* Append ``exc`` to ``list``. ``list`` must be a Python ``list``; a
|
|
141
|
+
* ``TypeError`` is raised otherwise. The explicit type check provides a
|
|
142
|
+
* friendlier error than the ``SystemError`` raised by ``PyList_Append`` for
|
|
143
|
+
* the same condition. */
|
|
144
|
+
static PyObject *fast_append_exception(PyObject *self, PyObject *const *args, Py_ssize_t nargs) {
|
|
145
|
+
(void)self; /* module method - self is unused */
|
|
146
|
+
if (nargs != 2) {
|
|
147
|
+
PyErr_Format(PyExc_TypeError,
|
|
148
|
+
"fast_append_exception() takes exactly 2 arguments (%zd given)",
|
|
149
|
+
nargs);
|
|
150
|
+
return NULL;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
PyObject *list = args[0];
|
|
154
|
+
PyObject *exc = args[1];
|
|
155
|
+
|
|
156
|
+
if (!PyList_Check(list)) {
|
|
157
|
+
PyErr_SetString(PyExc_TypeError, "first argument must be a list");
|
|
158
|
+
return NULL;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (PyList_Append(list, exc) == -1) {
|
|
162
|
+
return NULL; /* exception already set by PyList_Append */
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Py_RETURN_NONE;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* fast_suppress_exit(typ, excs) -> bool
|
|
169
|
+
*
|
|
170
|
+
* Return ``True`` if ``typ`` is not ``None`` and is a subclass of one of
|
|
171
|
+
* the types in ``excs``. Designed for the ``__exit__`` slot of context
|
|
172
|
+
* managers: it must never raise on the ``None`` case and should be as cheap
|
|
173
|
+
* as possible on the hot "no exception" path. */
|
|
174
|
+
static PyObject *fast_suppress_exit(PyObject *self, PyObject *const *args, Py_ssize_t nargs) {
|
|
175
|
+
(void)self; /* module method - self is unused */
|
|
176
|
+
if (nargs != 2) {
|
|
177
|
+
PyErr_Format(PyExc_TypeError,
|
|
178
|
+
"fast_suppress_exit() takes exactly 2 arguments (%zd given)",
|
|
179
|
+
nargs);
|
|
180
|
+
return NULL;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
PyObject *typ = args[0];
|
|
184
|
+
PyObject *excs = args[1];
|
|
185
|
+
|
|
186
|
+
/* Hot path: no exception pending. */
|
|
187
|
+
if (SPEEDUP_IS_NONE(typ)) {
|
|
188
|
+
Py_RETURN_FALSE;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Fast path: typ is exactly one of the suppressed types. */
|
|
192
|
+
int fast = fast_path_result(typ, excs);
|
|
193
|
+
if (fast == 1) {
|
|
194
|
+
Py_RETURN_TRUE;
|
|
195
|
+
}
|
|
196
|
+
if (fast == 0) {
|
|
197
|
+
Py_RETURN_FALSE;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Slow path: full subclass check. This is reached for:
|
|
201
|
+
* (a) identity miss on a single class,
|
|
202
|
+
* (b) identity miss on a non-empty tuple, and
|
|
203
|
+
* (c) ``excs`` of an unexpected shape. */
|
|
204
|
+
return bool_from_issubclass(PyObject_IsSubclass(typ, excs));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* --- Module definition -------------------------------------------------- */
|
|
208
|
+
|
|
209
|
+
static PyMethodDef SpeedupMethods[] = {
|
|
210
|
+
{
|
|
211
|
+
"fast_issubclass_check",
|
|
212
|
+
(PyCFunction)(void (*)(void))fast_issubclass_check,
|
|
213
|
+
METH_FASTCALL,
|
|
214
|
+
"fast_issubclass_check(typ, excs) -> bool\n\n"
|
|
215
|
+
"Check whether *typ* is a subclass of *excs*.\n"
|
|
216
|
+
"Returns ``False`` if *typ* is ``None``; *excs* must be a class or\n"
|
|
217
|
+
"a tuple of classes. Uses an identity fast path before falling back\n"
|
|
218
|
+
"to :c:func:`PyObject_IsSubclass`.",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"fast_append_exception",
|
|
222
|
+
(PyCFunction)(void (*)(void))fast_append_exception,
|
|
223
|
+
METH_FASTCALL,
|
|
224
|
+
"fast_append_exception(list, exc) -> None\n\n"
|
|
225
|
+
"Append *exc* to *list* (which must be a :class:`list`).",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"fast_suppress_exit",
|
|
229
|
+
(PyCFunction)(void (*)(void))fast_suppress_exit,
|
|
230
|
+
METH_FASTCALL,
|
|
231
|
+
"fast_suppress_exit(typ, excs) -> bool\n\n"
|
|
232
|
+
"Return ``True`` if *typ* is not ``None`` and is a subclass of any\n"
|
|
233
|
+
"type in *excs*. Tuned for ``__exit__`` context-manager methods.",
|
|
234
|
+
},
|
|
235
|
+
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/* Multi-interpreter / free-threading slots, gated by Python version.
|
|
239
|
+
*
|
|
240
|
+
* On Python 3.13+ we opt out of the GIL with ``Py_MOD_GIL_NOT_USED``. This
|
|
241
|
+
* is safe because the module owns no global mutable state. The slot
|
|
242
|
+
* mechanism requires multi-phase initialization (see ``PyInit__speedup``). */
|
|
243
|
+
#if PY_VERSION_HEX >= 0x030D0000
|
|
244
|
+
static PyModuleDef_Slot speedup_slots[] = {
|
|
245
|
+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
|
|
246
|
+
{0, NULL},
|
|
247
|
+
};
|
|
248
|
+
#endif
|
|
249
|
+
|
|
250
|
+
static struct PyModuleDef speedupmodule = {
|
|
251
|
+
PyModuleDef_HEAD_INIT,
|
|
252
|
+
"_speedup", /* m_name */
|
|
253
|
+
"C speedup module for errortools\n\n"
|
|
254
|
+
"Provides optimized implementations of exception handling operations:\n"
|
|
255
|
+
" - fast_issubclass_check: Quick exception type hierarchy checking\n"
|
|
256
|
+
" - fast_append_exception: Efficient exception list append operations\n"
|
|
257
|
+
" - fast_suppress_exit: Combined None-check + issubclass for __exit__",
|
|
258
|
+
0, /* m_size (0 = not per-interpreter) */
|
|
259
|
+
SpeedupMethods, /* m_methods */
|
|
260
|
+
#if PY_VERSION_HEX >= 0x030D0000
|
|
261
|
+
speedup_slots, /* m_slots */
|
|
262
|
+
#else
|
|
263
|
+
NULL, /* m_slots */
|
|
264
|
+
#endif
|
|
265
|
+
NULL, /* m_traverse */
|
|
266
|
+
NULL, /* m_clear */
|
|
267
|
+
NULL, /* m_free */
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
PyMODINIT_FUNC PyInit__speedup(void) {
|
|
271
|
+
/* ``PyModuleDef_Init`` must be used (instead of ``PyModule_Create``) when
|
|
272
|
+
* the module carries ``m_slots`` -- which we do on Python 3.13+ to opt
|
|
273
|
+
* out of the GIL. On older versions the two functions are functionally
|
|
274
|
+
* equivalent, so we use ``PyModuleDef_Init`` unconditionally to keep
|
|
275
|
+
* the init path single-source. */
|
|
276
|
+
return PyModuleDef_Init(&speedupmodule);
|
|
277
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod, ABC
|
|
4
4
|
import sys
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"BaseGroup",
|
|
@@ -21,7 +22,7 @@ class BaseGroup(Exception, ABC):
|
|
|
21
22
|
group_msg: The message attached to the raised group.
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
|
-
def __init__(self, group_msg: str = "multiple errors") -> None:
|
|
25
|
+
def __init__(self, group_msg: str = "multiple errors", *args: Any) -> None:
|
|
25
26
|
"""Initialise the group with a message.
|
|
26
27
|
|
|
27
28
|
Args:
|
|
@@ -29,6 +30,7 @@ class BaseGroup(Exception, ABC):
|
|
|
29
30
|
Defaults to ``"multiple errors"``.
|
|
30
31
|
"""
|
|
31
32
|
self.group_msg = group_msg
|
|
33
|
+
super().__init__(group_msg, *args)
|
|
32
34
|
|
|
33
35
|
@property
|
|
34
36
|
@abstractmethod
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Public command-line interface for errortools.
|
|
2
|
+
|
|
3
|
+
This module provides the entry points used by the ``errortools`` and
|
|
4
|
+
``logger`` console scripts declared in :file:`pyproject.toml`.
|
|
5
|
+
|
|
6
|
+
Two command families share this dispatcher:
|
|
7
|
+
|
|
8
|
+
* ``errortools`` - metadata, version and developer tooling flags.
|
|
9
|
+
* ``logger`` - log emission and an interactive debug shell.
|
|
10
|
+
|
|
11
|
+
The correct family is selected once, at module import time, by
|
|
12
|
+
inspecting :data:`sys.argv` rather than the parent process, so the
|
|
13
|
+
detection survives ``python -m _errortools`` invocations and works on
|
|
14
|
+
all supported platforms.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import Callable, Sequence
|
|
23
|
+
from typing import Final
|
|
24
|
+
|
|
25
|
+
from _errortools._cli import _cmd_log, _print_info
|
|
26
|
+
from _errortools.metadata import (
|
|
27
|
+
__description__,
|
|
28
|
+
__copyright__,
|
|
29
|
+
__author__,
|
|
30
|
+
__author_email__,
|
|
31
|
+
__license__,
|
|
32
|
+
__url__,
|
|
33
|
+
)
|
|
34
|
+
from _errortools.version import __version__
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Mode detection
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _detect_mode(argv0: str | None = None) -> str:
|
|
42
|
+
"""Return the CLI family inferred from ``argv0``.
|
|
43
|
+
|
|
44
|
+
The detection uses the *basename* of the executable/script rather
|
|
45
|
+
than a substring test, so paths such as ``/usr/bin/logger`` (a
|
|
46
|
+
real Unix utility) or ``my_logger_tool`` do not accidentally
|
|
47
|
+
trigger the logger dispatcher.
|
|
48
|
+
"""
|
|
49
|
+
raw = argv0 if argv0 is not None else sys.argv[0]
|
|
50
|
+
basename = os.path.basename(raw).lower()
|
|
51
|
+
# Strip common suffixes so ``logger-script.py`` / ``logger.exe``
|
|
52
|
+
# also collapse to the bare command name.
|
|
53
|
+
for suffix in (".exe", ".py", ".pyw", ".pyz", ".sh"):
|
|
54
|
+
if basename.endswith(suffix):
|
|
55
|
+
basename = basename[: -len(suffix)]
|
|
56
|
+
return "logger" if basename == "logger" else "errortools"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ``sys.argv[0]`` is stable for the lifetime of the process; cache
|
|
60
|
+
# the resolved mode to avoid recomputing the basename on every call.
|
|
61
|
+
_CLI_MODE: Final[str] = _detect_mode()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _make_parser(prog: str, description: str) -> argparse.ArgumentParser:
|
|
65
|
+
"""Build an `ArgumentParser`, enabling colour on 3.14+.
|
|
66
|
+
|
|
67
|
+
Colour output is only supported by :mod:`argparse` from Python 3.14,
|
|
68
|
+
so the ``color`` kwarg is conditionally supplied to stay compatible
|
|
69
|
+
with the project's ``requires-python = ">=3.8"``.
|
|
70
|
+
"""
|
|
71
|
+
if sys.version_info >= (3, 14):
|
|
72
|
+
return argparse.ArgumentParser(prog=prog, description=description, color=True)
|
|
73
|
+
return argparse.ArgumentParser(prog=prog, description=description)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# errortools family
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_errortools_parser() -> argparse.ArgumentParser:
|
|
82
|
+
"""Construct the argument parser for the ``errortools`` command."""
|
|
83
|
+
parser = _make_parser(prog="errortools", description=__description__)
|
|
84
|
+
parser.add_argument("-v", "--version", action="store_true", help="Show version and exit")
|
|
85
|
+
parser.add_argument("-c", "--copyrights", action="store_true", help="Show copyright information")
|
|
86
|
+
parser.add_argument("-a", "--author", action="store_true", help="Show author name")
|
|
87
|
+
parser.add_argument("-e", "--email", action="store_true", help="Show author email")
|
|
88
|
+
parser.add_argument("-l", "--license", action="store_true", help="Show license type")
|
|
89
|
+
parser.add_argument("-u", "--url", action="store_true", help="Show project URL")
|
|
90
|
+
parser.add_argument("-i", "--info", action="store_true", help="Show all package information")
|
|
91
|
+
parser.add_argument("--run-tests", action="store_true", help="Run tests using pytest")
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# logger family
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_logger_parser() -> argparse.ArgumentParser:
|
|
101
|
+
"""Construct the argument parser for the ``logger`` command."""
|
|
102
|
+
parser = _make_parser(
|
|
103
|
+
prog="logger",
|
|
104
|
+
description="Logger CLI - emit log records or open an interactive REPL.",
|
|
105
|
+
)
|
|
106
|
+
subparsers = parser.add_subparsers(
|
|
107
|
+
dest="subcmd",
|
|
108
|
+
required=True,
|
|
109
|
+
title="available subcommands",
|
|
110
|
+
metavar="{emit,shell}",
|
|
111
|
+
help="run `logger {subcommand} -h` to view subcommand details",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
parser_emit = subparsers.add_parser(
|
|
115
|
+
"emit",
|
|
116
|
+
help="Emit a single log message to stdout or stderr",
|
|
117
|
+
description="Emit a single log message to stdout or stderr.",
|
|
118
|
+
)
|
|
119
|
+
parser_emit.add_argument("message", help="Text content of the log record")
|
|
120
|
+
parser_emit.add_argument(
|
|
121
|
+
"--level",
|
|
122
|
+
"-l",
|
|
123
|
+
default="info",
|
|
124
|
+
choices=["trace", "debug", "info", "success", "warning", "error", "critical"],
|
|
125
|
+
help="Log severity level (default: info)",
|
|
126
|
+
)
|
|
127
|
+
parser_emit.add_argument(
|
|
128
|
+
"--output",
|
|
129
|
+
"-o",
|
|
130
|
+
choices=["stderr", "stdout"],
|
|
131
|
+
default="stderr",
|
|
132
|
+
help="Target output stream (default: stderr)",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
subparsers.add_parser(
|
|
136
|
+
"shell",
|
|
137
|
+
help="Launch an interactive logger REPL debug shell",
|
|
138
|
+
description="Launch an interactive logger REPL debug shell.",
|
|
139
|
+
)
|
|
140
|
+
return parser
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Argument parsing
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_args(args: Sequence[str] | None = None) -> argparse.Namespace:
|
|
149
|
+
"""Parse command-line arguments for the active CLI family."""
|
|
150
|
+
if _CLI_MODE == "logger":
|
|
151
|
+
return _build_logger_parser().parse_args(args)
|
|
152
|
+
return _build_errortools_parser().parse_args(args)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Flag dispatch (errortools family)
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _show_version() -> None:
|
|
161
|
+
"""Print ``errortools <version>`` in the same shape as ``--info``."""
|
|
162
|
+
print(f"errortools {__version__}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _show_copyrights() -> None:
|
|
166
|
+
"""Print the project's copyright line."""
|
|
167
|
+
print(__copyright__)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _show_author() -> None:
|
|
171
|
+
"""Print the author's name."""
|
|
172
|
+
print(f"Author: {__author__}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _show_email() -> None:
|
|
176
|
+
"""Print the author's contact email."""
|
|
177
|
+
print(f"Email: {__author_email__}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _show_license() -> None:
|
|
181
|
+
"""Print the project's licence."""
|
|
182
|
+
print(f"License: {__license__}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _show_url() -> None:
|
|
186
|
+
"""Print the project's homepage URL."""
|
|
187
|
+
print(f"URL: {__url__}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _run_tests() -> None:
|
|
191
|
+
"""Delegate to :func:`testing.run_tests.run_tests` with a helpful message on failure."""
|
|
192
|
+
try:
|
|
193
|
+
from testing.run_tests import run_tests
|
|
194
|
+
except ImportError as exc: # pragma: no cover - environment dependent
|
|
195
|
+
sys.stderr.write(
|
|
196
|
+
"errortools: cannot import `testing` package " f"({exc!r}). Install the project with its test extras.\n"
|
|
197
|
+
)
|
|
198
|
+
sys.exit(2)
|
|
199
|
+
run_tests()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
_FLAG_ACTIONS: Final[dict[str, Callable[[], None]]] = {
|
|
203
|
+
"version": _show_version,
|
|
204
|
+
"copyrights": _show_copyrights,
|
|
205
|
+
"author": _show_author,
|
|
206
|
+
"email": _show_email,
|
|
207
|
+
"license": _show_license,
|
|
208
|
+
"url": _show_url,
|
|
209
|
+
"run_tests": _run_tests,
|
|
210
|
+
"info": _print_info,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Entry point
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _dispatch_logger(args: argparse.Namespace) -> None:
|
|
220
|
+
"""Route a parsed ``logger`` namespace to its subcommand handler."""
|
|
221
|
+
if args.subcmd == "shell":
|
|
222
|
+
from _errortools._logger_shell import start_shell
|
|
223
|
+
|
|
224
|
+
start_shell()
|
|
225
|
+
return
|
|
226
|
+
# ``subcmd`` is constrained to {"emit", "shell"} by argparse, but
|
|
227
|
+
# keep a defensive fallback in case ``required=True`` is ever loosened.
|
|
228
|
+
if args.subcmd == "emit":
|
|
229
|
+
_cmd_log(args.message, args.level, args.output)
|
|
230
|
+
return
|
|
231
|
+
sys.stderr.write(f"errortools: unknown logger subcommand {args.subcmd!r}\n")
|
|
232
|
+
sys.exit(2)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _dispatch_errortools(args: argparse.Namespace) -> None:
|
|
236
|
+
"""Run the action selected by a flag on the ``errortools`` family."""
|
|
237
|
+
for flag, action in _FLAG_ACTIONS.items():
|
|
238
|
+
if getattr(args, flag, False):
|
|
239
|
+
action()
|
|
240
|
+
return
|
|
241
|
+
# No flag supplied → render the help screen.
|
|
242
|
+
_build_errortools_parser().parse_args(["--help"])
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
246
|
+
"""Module entry point dispatched to the active CLI family."""
|
|
247
|
+
if argv is None:
|
|
248
|
+
argv = sys.argv[1:]
|
|
249
|
+
args = parse_args(argv)
|
|
250
|
+
|
|
251
|
+
if _CLI_MODE == "logger":
|
|
252
|
+
_dispatch_logger(args)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
_dispatch_errortools(args)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
|
@@ -17,10 +17,12 @@ try:
|
|
|
17
17
|
)
|
|
18
18
|
except ImportError:
|
|
19
19
|
|
|
20
|
-
def fast_append_exception(lst, exc) -> None:
|
|
20
|
+
def fast_append_exception(lst: list[BaseException], exc: BaseException) -> None:
|
|
21
21
|
lst.append(exc)
|
|
22
22
|
|
|
23
|
-
def fast_suppress_exit(
|
|
23
|
+
def fast_suppress_exit(
|
|
24
|
+
typ: type[BaseException] | None, excs: type[BaseException] | tuple[type[BaseException], ...]
|
|
25
|
+
) -> bool:
|
|
24
26
|
return typ is not None and issubclass(typ, excs)
|
|
25
27
|
|
|
26
28
|
|
|
@@ -79,6 +79,9 @@ ignore = ErrorIgnoreWrapper
|
|
|
79
79
|
- `ignore_subclass` — suppress exceptions including subclasses
|
|
80
80
|
- `retry` — automatic retry on exception
|
|
81
81
|
"""
|
|
82
|
+
# NOTE: Any exception raised inside the ``with`` block that is an instance of one
|
|
83
|
+
# of *errors* is caught and discarded. All other exceptions propagate
|
|
84
|
+
# unchanged. Execution resumes after the ``with`` block.
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
class fast_ignore:
|
|
@@ -135,6 +138,10 @@ def ignore_subclass(base: ExceptionType) -> Iterator[None]:
|
|
|
135
138
|
... raise IndexError("out of range") # IndexError ⊆ LookupError
|
|
136
139
|
... # suppressed — execution continues here
|
|
137
140
|
"""
|
|
141
|
+
# NOTE: Similar to `ignore`, but accepts a single base class and suppresses
|
|
142
|
+
# every exception whose type satisfies ``issubclass(type(exc), base)``.
|
|
143
|
+
# Useful when you want to express intent explicitly — "ignore anything
|
|
144
|
+
# derived from X" — rather than listing concrete types.
|
|
138
145
|
try:
|
|
139
146
|
yield
|
|
140
147
|
except BaseException as exc:
|
|
@@ -155,6 +162,9 @@ def ignore_warns(*categories: type[Warning]) -> Iterator[None]:
|
|
|
155
162
|
... warnings.warn("old api", DeprecationWarning)
|
|
156
163
|
... # no warning emitted
|
|
157
164
|
"""
|
|
165
|
+
# NOTE: Uses `warnings.catch_warnings` and `warnings.simplefilter`
|
|
166
|
+
# to silence any warning whose category is one of *categories* for the
|
|
167
|
+
# duration of the ``with`` block. All other warnings are unaffected.
|
|
158
168
|
with warnings.catch_warnings():
|
|
159
169
|
for category in categories:
|
|
160
170
|
warnings.filterwarnings("ignore", category=category)
|
|
@@ -12,3 +12,20 @@ __fullname__ = na.generate_name(__title__, __author__)
|
|
|
12
12
|
__slug__ = na.generate_slug(__title__, __author__)
|
|
13
13
|
__signature__ = na.generate_signature(__title__, __author__)
|
|
14
14
|
__uid__ = na.generate_id(__title__, __author__)
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
show_items: list[tuple[str, str]] = [
|
|
18
|
+
("author", __author__),
|
|
19
|
+
("author email", __author_email__),
|
|
20
|
+
("title", __title__),
|
|
21
|
+
("description", __description__),
|
|
22
|
+
("license", __license__),
|
|
23
|
+
("url", __url__),
|
|
24
|
+
("fullname", __fullname__),
|
|
25
|
+
("slug", __slug__),
|
|
26
|
+
("signature", __signature__),
|
|
27
|
+
("uid", __uid__),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
for label, value in show_items:
|
|
31
|
+
print(f"{label}: {value}")
|