dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__py3-none-any.whl
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.
- dycw_utilities-0.175.17.dist-info/METADATA +34 -0
- dycw_utilities-0.175.17.dist-info/RECORD +103 -0
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +14 -14
- utilities/asyncio.py +350 -819
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +77 -22
- utilities/cachetools.py +24 -29
- utilities/click.py +393 -237
- utilities/concurrent.py +8 -11
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +83 -118
- utilities/docker.py +293 -0
- utilities/enum.py +26 -23
- utilities/errors.py +17 -3
- utilities/fastapi.py +29 -65
- utilities/fpdf2.py +3 -3
- utilities/functions.py +169 -416
- utilities/functools.py +18 -19
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +738 -589
- utilities/importlib.py +17 -1
- utilities/inflect.py +25 -0
- utilities/iterables.py +194 -262
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +5 -9
- utilities/logging.py +345 -543
- utilities/math.py +18 -13
- utilities/memory_profiler.py +11 -15
- utilities/more_itertools.py +200 -131
- utilities/operator.py +33 -29
- utilities/optuna.py +6 -6
- utilities/orjson.py +272 -137
- utilities/os.py +61 -4
- utilities/parse.py +59 -61
- utilities/pathlib.py +281 -40
- utilities/permissions.py +298 -0
- utilities/pickle.py +2 -2
- utilities/platform.py +24 -5
- utilities/polars.py +1214 -430
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +113 -26
- utilities/pqdm.py +10 -11
- utilities/psutil.py +6 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -54
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +8 -10
- utilities/pytest.py +227 -121
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +13 -9
- utilities/re.py +58 -28
- utilities/redis.py +401 -550
- utilities/scipy.py +1 -1
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +36 -106
- utilities/sqlalchemy.py +502 -473
- utilities/sqlalchemy_polars.py +38 -94
- utilities/string.py +2 -3
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +86 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +37 -65
- utilities/traceback.py +158 -929
- utilities/types.py +146 -116
- utilities/typing.py +531 -71
- utilities/tzdata.py +1 -53
- utilities/tzlocal.py +6 -23
- utilities/uuid.py +43 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1776 -386
- utilities/zoneinfo.py +84 -22
- dycw_utilities-0.129.10.dist-info/METADATA +0 -241
- dycw_utilities-0.129.10.dist-info/RECORD +0 -96
- dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
- dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
- utilities/datetime.py +0 -1409
- utilities/eventkit.py +0 -402
- utilities/loguru.py +0 -144
- utilities/luigi.py +0 -228
- utilities/period.py +0 -324
- utilities/pyrsistent.py +0 -89
- utilities/python_dotenv.py +0 -105
- utilities/streamlit.py +0 -105
- utilities/sys.py +0 -87
- utilities/tenacity.py +0 -145
utilities/traceback.py
CHANGED
|
@@ -2,45 +2,21 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import sys
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from dataclasses import dataclass, field, replace
|
|
8
|
-
from functools import partial, wraps
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import partial
|
|
9
7
|
from getpass import getuser
|
|
10
|
-
from inspect import iscoroutinefunction, signature
|
|
11
8
|
from itertools import repeat
|
|
12
|
-
from
|
|
9
|
+
from os import getpid
|
|
13
10
|
from pathlib import Path
|
|
14
11
|
from socket import gethostname
|
|
15
|
-
from sys import
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Self,
|
|
24
|
-
TypeGuard,
|
|
25
|
-
TypeVar,
|
|
26
|
-
assert_never,
|
|
27
|
-
cast,
|
|
28
|
-
overload,
|
|
29
|
-
override,
|
|
30
|
-
runtime_checkable,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
from utilities.datetime import get_datetime, get_now, serialize_compact
|
|
34
|
-
from utilities.errors import ImpossibleCaseError, repr_error
|
|
35
|
-
from utilities.functions import (
|
|
36
|
-
ensure_not_none,
|
|
37
|
-
ensure_str,
|
|
38
|
-
get_class_name,
|
|
39
|
-
get_func_name,
|
|
40
|
-
get_func_qualname,
|
|
41
|
-
)
|
|
42
|
-
from utilities.iterables import OneEmptyError, always_iterable, one
|
|
43
|
-
from utilities.pathlib import get_path
|
|
12
|
+
from sys import stderr
|
|
13
|
+
from traceback import TracebackException
|
|
14
|
+
from typing import TYPE_CHECKING, override
|
|
15
|
+
|
|
16
|
+
from utilities.atomicwrites import writer
|
|
17
|
+
from utilities.errors import repr_error
|
|
18
|
+
from utilities.iterables import OneEmptyError, one
|
|
19
|
+
from utilities.pathlib import module_path, to_path
|
|
44
20
|
from utilities.reprlib import (
|
|
45
21
|
RICH_EXPAND_ALL,
|
|
46
22
|
RICH_INDENT_SIZE,
|
|
@@ -48,34 +24,33 @@ from utilities.reprlib import (
|
|
|
48
24
|
RICH_MAX_LENGTH,
|
|
49
25
|
RICH_MAX_STRING,
|
|
50
26
|
RICH_MAX_WIDTH,
|
|
51
|
-
yield_call_args_repr,
|
|
52
27
|
yield_mapping_repr,
|
|
53
28
|
)
|
|
54
|
-
from utilities.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
29
|
+
from utilities.text import to_bool
|
|
30
|
+
from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
|
|
31
|
+
from utilities.version import to_version
|
|
32
|
+
from utilities.whenever import (
|
|
33
|
+
format_compact,
|
|
34
|
+
get_now,
|
|
35
|
+
get_now_local,
|
|
36
|
+
to_zoned_date_time,
|
|
60
37
|
)
|
|
61
|
-
from utilities.version import get_version
|
|
62
|
-
from utilities.whenever import serialize_duration
|
|
63
38
|
|
|
64
39
|
if TYPE_CHECKING:
|
|
65
|
-
from collections.abc import Callable,
|
|
66
|
-
from
|
|
67
|
-
from types import
|
|
68
|
-
|
|
69
|
-
from utilities.types import
|
|
40
|
+
from collections.abc import Callable, Iterator
|
|
41
|
+
from traceback import FrameSummary
|
|
42
|
+
from types import TracebackType
|
|
43
|
+
|
|
44
|
+
from utilities.types import (
|
|
45
|
+
Delta,
|
|
46
|
+
MaybeCallableBoolLike,
|
|
47
|
+
MaybeCallablePathLike,
|
|
48
|
+
MaybeCallableZonedDateTimeLike,
|
|
49
|
+
PathLike,
|
|
50
|
+
)
|
|
70
51
|
from utilities.version import MaybeCallableVersionLike
|
|
71
52
|
|
|
72
53
|
|
|
73
|
-
_T = TypeVar("_T")
|
|
74
|
-
_CALL_ARGS = "_CALL_ARGS"
|
|
75
|
-
_INDENT = 4 * " "
|
|
76
|
-
_START = get_now()
|
|
77
|
-
|
|
78
|
-
|
|
79
54
|
##
|
|
80
55
|
|
|
81
56
|
|
|
@@ -84,7 +59,7 @@ def format_exception_stack(
|
|
|
84
59
|
/,
|
|
85
60
|
*,
|
|
86
61
|
header: bool = False,
|
|
87
|
-
start:
|
|
62
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
88
63
|
version: MaybeCallableVersionLike | None = None,
|
|
89
64
|
capture_locals: bool = False,
|
|
90
65
|
max_width: int = RICH_MAX_WIDTH,
|
|
@@ -95,7 +70,7 @@ def format_exception_stack(
|
|
|
95
70
|
expand_all: bool = RICH_EXPAND_ALL,
|
|
96
71
|
) -> str:
|
|
97
72
|
"""Format an exception stack."""
|
|
98
|
-
lines:
|
|
73
|
+
lines: list[str] = []
|
|
99
74
|
if header:
|
|
100
75
|
lines.extend(_yield_header_lines(start=start, version=version))
|
|
101
76
|
lines.extend(
|
|
@@ -113,873 +88,25 @@ def format_exception_stack(
|
|
|
113
88
|
return "\n".join(lines)
|
|
114
89
|
|
|
115
90
|
|
|
116
|
-
##
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def make_except_hook(
|
|
120
|
-
*,
|
|
121
|
-
start: MaybeCallableDateTime | None = _START,
|
|
122
|
-
version: MaybeCallableVersionLike | None = None,
|
|
123
|
-
path: MaybeCallablePathLike | None = None,
|
|
124
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
125
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
126
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
127
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
128
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
129
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
130
|
-
slack_url: str | None = None,
|
|
131
|
-
) -> Callable[
|
|
132
|
-
[type[BaseException] | None, BaseException | None, TracebackType | None], None
|
|
133
|
-
]:
|
|
134
|
-
"""Exception hook to log the traceback."""
|
|
135
|
-
return partial(
|
|
136
|
-
_make_except_hook_inner,
|
|
137
|
-
start=start,
|
|
138
|
-
version=version,
|
|
139
|
-
path=path,
|
|
140
|
-
max_width=max_width,
|
|
141
|
-
indent_size=indent_size,
|
|
142
|
-
max_length=max_length,
|
|
143
|
-
max_string=max_string,
|
|
144
|
-
max_depth=max_depth,
|
|
145
|
-
expand_all=expand_all,
|
|
146
|
-
slack_url=slack_url,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _make_except_hook_inner(
|
|
151
|
-
exc_type: type[BaseException] | None,
|
|
152
|
-
exc_val: BaseException | None,
|
|
153
|
-
traceback: TracebackType | None,
|
|
154
|
-
/,
|
|
155
|
-
*,
|
|
156
|
-
start: MaybeCallableDateTime | None = _START,
|
|
157
|
-
version: MaybeCallableVersionLike | None = None,
|
|
158
|
-
path: MaybeCallablePathLike | None = None,
|
|
159
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
160
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
161
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
162
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
163
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
164
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
165
|
-
slack_url: str | None = None,
|
|
166
|
-
) -> None:
|
|
167
|
-
"""Exception hook to log the traceback."""
|
|
168
|
-
_ = (exc_type, traceback)
|
|
169
|
-
if exc_val is None:
|
|
170
|
-
raise MakeExceptHookError
|
|
171
|
-
slim = format_exception_stack(exc_val, header=True, start=start, version=version)
|
|
172
|
-
_ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
|
|
173
|
-
if path is not None:
|
|
174
|
-
from utilities.atomicwrites import writer
|
|
175
|
-
from utilities.tzlocal import get_now_local
|
|
176
|
-
|
|
177
|
-
path = (
|
|
178
|
-
get_path(path=path)
|
|
179
|
-
.joinpath(serialize_compact(get_now_local()))
|
|
180
|
-
.with_suffix(".txt")
|
|
181
|
-
)
|
|
182
|
-
full = format_exception_stack(
|
|
183
|
-
exc_val,
|
|
184
|
-
header=True,
|
|
185
|
-
start=start,
|
|
186
|
-
version=version,
|
|
187
|
-
capture_locals=True,
|
|
188
|
-
max_width=max_width,
|
|
189
|
-
indent_size=indent_size,
|
|
190
|
-
max_length=max_length,
|
|
191
|
-
max_string=max_string,
|
|
192
|
-
max_depth=max_depth,
|
|
193
|
-
expand_all=expand_all,
|
|
194
|
-
)
|
|
195
|
-
with writer(path, overwrite=True) as temp:
|
|
196
|
-
_ = temp.write_text(full)
|
|
197
|
-
if slack_url is not None: # pragma: no cover
|
|
198
|
-
from utilities.slack_sdk import send_to_slack
|
|
199
|
-
|
|
200
|
-
send = f"```{slim}```"
|
|
201
|
-
run(send_to_slack(slack_url, send))
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
##
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class RichTracebackFormatter(Formatter):
|
|
208
|
-
"""Formatter for rich tracebacks."""
|
|
209
|
-
|
|
210
|
-
@override
|
|
211
|
-
def __init__(
|
|
212
|
-
self,
|
|
213
|
-
fmt: str | None = None,
|
|
214
|
-
datefmt: str | None = None,
|
|
215
|
-
style: _FormatStyle = "%",
|
|
216
|
-
validate: bool = True,
|
|
217
|
-
/,
|
|
218
|
-
*,
|
|
219
|
-
defaults: StrMapping | None = None,
|
|
220
|
-
start: MaybeCallableDateTime | None = _START,
|
|
221
|
-
version: MaybeCallableVersionLike | None = None,
|
|
222
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
223
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
224
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
225
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
226
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
227
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
228
|
-
detail: bool = False,
|
|
229
|
-
post: Callable[[str], str] | None = None,
|
|
230
|
-
) -> None:
|
|
231
|
-
super().__init__(fmt, datefmt, style, validate, defaults=defaults)
|
|
232
|
-
self._start = get_datetime(datetime=start)
|
|
233
|
-
self._version = get_version(version=version)
|
|
234
|
-
self._max_width = max_width
|
|
235
|
-
self._indent_size = indent_size
|
|
236
|
-
self._max_length = max_length
|
|
237
|
-
self._max_string = max_string
|
|
238
|
-
self._max_depth = max_depth
|
|
239
|
-
self._expand_all = expand_all
|
|
240
|
-
self._detail = detail
|
|
241
|
-
self._post = post
|
|
242
|
-
|
|
243
|
-
@override
|
|
244
|
-
def format(self, record: LogRecord) -> str:
|
|
245
|
-
"""Format the record."""
|
|
246
|
-
if record.exc_info is None:
|
|
247
|
-
return f"ERROR: record.exc_info is None\n{record=}"
|
|
248
|
-
_, exc_value, _ = record.exc_info
|
|
249
|
-
if exc_value is None:
|
|
250
|
-
return f"ERROR: record.exc_info[1] is None\n{record=}" # pragma: no cover
|
|
251
|
-
exc_value = ensure_not_none(exc_value, desc="exc_value")
|
|
252
|
-
error = get_rich_traceback(
|
|
253
|
-
exc_value,
|
|
254
|
-
start=self._start,
|
|
255
|
-
version=self._version,
|
|
256
|
-
max_width=self._max_width,
|
|
257
|
-
indent_size=self._indent_size,
|
|
258
|
-
max_length=self._max_length,
|
|
259
|
-
max_string=self._max_string,
|
|
260
|
-
max_depth=self._max_depth,
|
|
261
|
-
expand_all=self._expand_all,
|
|
262
|
-
)
|
|
263
|
-
match error:
|
|
264
|
-
case ExcChainTB() | ExcGroupTB() | ExcTB():
|
|
265
|
-
text = error.format(header=True, detail=self._detail)
|
|
266
|
-
case BaseException():
|
|
267
|
-
text = "\n".join(format_exception(error))
|
|
268
|
-
case _ as never:
|
|
269
|
-
assert_never(never)
|
|
270
|
-
if self._post is not None:
|
|
271
|
-
text = self._post(text)
|
|
272
|
-
return text
|
|
273
|
-
|
|
274
|
-
@classmethod
|
|
275
|
-
def create_and_set(
|
|
276
|
-
cls,
|
|
277
|
-
handler: Handler,
|
|
278
|
-
/,
|
|
279
|
-
*,
|
|
280
|
-
fmt: str | None = None,
|
|
281
|
-
datefmt: str | None = None,
|
|
282
|
-
style: _FormatStyle = "%",
|
|
283
|
-
validate: bool = True,
|
|
284
|
-
defaults: StrMapping | None = None,
|
|
285
|
-
version: MaybeCallableVersionLike | None = None,
|
|
286
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
287
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
288
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
289
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
290
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
291
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
292
|
-
detail: bool = False,
|
|
293
|
-
post: Callable[[str], str] | None = None,
|
|
294
|
-
) -> Self:
|
|
295
|
-
"""Create an instance and set it on a handler."""
|
|
296
|
-
formatter = cls(
|
|
297
|
-
fmt,
|
|
298
|
-
datefmt,
|
|
299
|
-
style,
|
|
300
|
-
validate,
|
|
301
|
-
defaults=defaults,
|
|
302
|
-
version=version,
|
|
303
|
-
max_width=max_width,
|
|
304
|
-
indent_size=indent_size,
|
|
305
|
-
max_length=max_length,
|
|
306
|
-
max_string=max_string,
|
|
307
|
-
max_depth=max_depth,
|
|
308
|
-
expand_all=expand_all,
|
|
309
|
-
detail=detail,
|
|
310
|
-
post=post,
|
|
311
|
-
)
|
|
312
|
-
handler.addFilter(cls._has_exc_info)
|
|
313
|
-
handler.setFormatter(formatter)
|
|
314
|
-
return formatter
|
|
315
|
-
|
|
316
|
-
@classmethod
|
|
317
|
-
def _has_exc_info(cls, record: LogRecord, /) -> bool:
|
|
318
|
-
return record.exc_info is not None
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
##
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
@dataclass(repr=False, kw_only=True, slots=True)
|
|
325
|
-
class _CallArgs:
|
|
326
|
-
"""A collection of call arguments."""
|
|
327
|
-
|
|
328
|
-
func: Callable[..., Any]
|
|
329
|
-
args: tuple[Any, ...] = field(default_factory=tuple)
|
|
330
|
-
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
331
|
-
|
|
332
|
-
@override
|
|
333
|
-
def __repr__(self) -> str:
|
|
334
|
-
cls = get_class_name(self)
|
|
335
|
-
parts: list[tuple[str, Any]] = [
|
|
336
|
-
("func", get_func_qualname(self.func)),
|
|
337
|
-
("args", self.args),
|
|
338
|
-
("kwargs", self.kwargs),
|
|
339
|
-
]
|
|
340
|
-
joined = ", ".join(f"{k}={v!r}" for k, v in parts)
|
|
341
|
-
return f"{cls}({joined})"
|
|
342
|
-
|
|
343
|
-
@classmethod
|
|
344
|
-
def create(cls, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Self:
|
|
345
|
-
"""Make the initial trace data."""
|
|
346
|
-
sig = signature(func)
|
|
347
|
-
try:
|
|
348
|
-
bound_args = sig.bind(*args, **kwargs)
|
|
349
|
-
except TypeError as error:
|
|
350
|
-
orig = ensure_str(one(error.args))
|
|
351
|
-
lines: list[str] = [
|
|
352
|
-
f"Unable to bind arguments for {get_func_name(func)!r}; {orig}"
|
|
353
|
-
]
|
|
354
|
-
lines.extend(yield_call_args_repr(*args, **kwargs))
|
|
355
|
-
new = "\n".join(lines)
|
|
356
|
-
raise _CallArgsError(new) from None
|
|
357
|
-
return cls(func=func, args=bound_args.args, kwargs=bound_args.kwargs)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
class _CallArgsError(TypeError):
|
|
361
|
-
"""Raised when a set of call arguments cannot be created."""
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
@dataclass(kw_only=True, slots=True)
|
|
365
|
-
class _ExtFrameSummary(Generic[_T]):
|
|
366
|
-
"""An extended frame summary."""
|
|
367
|
-
|
|
368
|
-
filename: Path
|
|
369
|
-
module: str | None = None
|
|
370
|
-
name: str
|
|
371
|
-
qualname: str
|
|
372
|
-
code_line: str
|
|
373
|
-
first_line_num: int
|
|
374
|
-
line_num: int
|
|
375
|
-
end_line_num: int
|
|
376
|
-
col_num: int | None = None
|
|
377
|
-
end_col_num: int | None = None
|
|
378
|
-
locals: dict[str, Any] = field(default_factory=dict)
|
|
379
|
-
extra: _T
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
type _ExtFrameSummaryCAOpt = _ExtFrameSummary[_CallArgs | None]
|
|
383
|
-
type _ExtFrameSummaryCA = _ExtFrameSummary[_CallArgs]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
@dataclass(repr=False, kw_only=True, slots=True)
|
|
387
|
-
class _ExcTBInternal:
|
|
388
|
-
"""A rich traceback for an exception; internal use only."""
|
|
389
|
-
|
|
390
|
-
raw: list[_ExtFrameSummaryCAOpt] = field(default_factory=list)
|
|
391
|
-
frames: list[_ExtFrameSummaryCA] = field(default_factory=list)
|
|
392
|
-
error: BaseException
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
@runtime_checkable
|
|
396
|
-
class _HasExceptionPath(Protocol):
|
|
397
|
-
@property
|
|
398
|
-
def exc_tb(self) -> _ExcTBInternal: ... # pragma: no cover
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
@dataclass(kw_only=True, slots=True)
|
|
402
|
-
class ExcChainTB(Generic[TBaseException]):
|
|
403
|
-
"""A rich traceback for an exception chain."""
|
|
404
|
-
|
|
405
|
-
errors: list[
|
|
406
|
-
ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException
|
|
407
|
-
] = field(default_factory=list)
|
|
408
|
-
start: MaybeCallableDateTime | None = field(default=_START, repr=False)
|
|
409
|
-
version: MaybeCallableVersionLike | None = field(default=None, repr=False)
|
|
410
|
-
max_width: int = RICH_MAX_WIDTH
|
|
411
|
-
indent_size: int = RICH_INDENT_SIZE
|
|
412
|
-
max_length: int | None = RICH_MAX_LENGTH
|
|
413
|
-
max_string: int | None = RICH_MAX_STRING
|
|
414
|
-
max_depth: int | None = RICH_MAX_DEPTH
|
|
415
|
-
expand_all: bool = RICH_EXPAND_ALL
|
|
416
|
-
|
|
417
|
-
def __getitem__(
|
|
418
|
-
self, i: int, /
|
|
419
|
-
) -> ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException:
|
|
420
|
-
return self.errors[i]
|
|
421
|
-
|
|
422
|
-
def __iter__(
|
|
423
|
-
self,
|
|
424
|
-
) -> Iterator[ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException]:
|
|
425
|
-
yield from self.errors
|
|
426
|
-
|
|
427
|
-
def __len__(self) -> int:
|
|
428
|
-
return len(self.errors)
|
|
429
|
-
|
|
430
|
-
@override
|
|
431
|
-
def __repr__(self) -> str:
|
|
432
|
-
return self.format(header=True, detail=True)
|
|
433
|
-
|
|
434
|
-
def format(self, *, header: bool = False, detail: bool = False) -> str:
|
|
435
|
-
"""Format the traceback."""
|
|
436
|
-
lines: list[str] = []
|
|
437
|
-
if header: # pragma: no cover
|
|
438
|
-
lines.extend(_yield_header_lines(start=self.start, version=self.version))
|
|
439
|
-
total = len(self.errors)
|
|
440
|
-
for i, errors in enumerate(self.errors, start=1):
|
|
441
|
-
lines.append(f"Exception chain {i}/{total}:")
|
|
442
|
-
match errors:
|
|
443
|
-
case ExcGroupTB() | ExcTB():
|
|
444
|
-
lines.append(errors.format(header=False, detail=detail, depth=1))
|
|
445
|
-
case BaseException(): # pragma: no cover
|
|
446
|
-
lines.append(_format_exception(errors, depth=1))
|
|
447
|
-
case _ as never:
|
|
448
|
-
assert_never(never)
|
|
449
|
-
lines.append("")
|
|
450
|
-
return "\n".join(lines)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
@dataclass(kw_only=True, slots=True)
|
|
454
|
-
class ExcGroupTB(Generic[TBaseException]):
|
|
455
|
-
"""A rich traceback for an exception group."""
|
|
456
|
-
|
|
457
|
-
exc_group: ExcTB[ExceptionGroup[Any]] | ExceptionGroup[Any]
|
|
458
|
-
errors: list[
|
|
459
|
-
ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException
|
|
460
|
-
] = field(default_factory=list)
|
|
461
|
-
start: MaybeCallableDateTime | None = field(default=_START, repr=False)
|
|
462
|
-
version: MaybeCallableVersionLike | None = field(default=None, repr=False)
|
|
463
|
-
max_width: int = RICH_MAX_WIDTH
|
|
464
|
-
indent_size: int = RICH_INDENT_SIZE
|
|
465
|
-
max_length: int | None = RICH_MAX_LENGTH
|
|
466
|
-
max_string: int | None = RICH_MAX_STRING
|
|
467
|
-
max_depth: int | None = RICH_MAX_DEPTH
|
|
468
|
-
expand_all: bool = RICH_EXPAND_ALL
|
|
469
|
-
|
|
470
|
-
@override
|
|
471
|
-
def __repr__(self) -> str:
|
|
472
|
-
return self.format(header=True, detail=True) # skipif-ci
|
|
473
|
-
|
|
474
|
-
def format(
|
|
475
|
-
self, *, header: bool = False, detail: bool = False, depth: int = 0
|
|
476
|
-
) -> str:
|
|
477
|
-
"""Format the traceback."""
|
|
478
|
-
lines: list[str] = [] # skipif-ci
|
|
479
|
-
if header: # pragma: no cover
|
|
480
|
-
lines.extend(_yield_header_lines(start=self.start, version=self.version))
|
|
481
|
-
lines.append("Exception group:") # skipif-ci
|
|
482
|
-
match self.exc_group: # skipif-ci
|
|
483
|
-
case ExcTB() as exc_tb:
|
|
484
|
-
lines.append(exc_tb.format(header=False, detail=detail, depth=1))
|
|
485
|
-
case ExceptionGroup() as exc_group: # pragma: no cover
|
|
486
|
-
lines.append(_format_exception(exc_group, depth=1))
|
|
487
|
-
case _ as never:
|
|
488
|
-
assert_never(never)
|
|
489
|
-
lines.append("") # skipif-ci
|
|
490
|
-
total = len(self.errors) # skipif-ci
|
|
491
|
-
for i, errors in enumerate(self.errors, start=1): # skipif-ci
|
|
492
|
-
lines.append(indent(f"Exception group error {i}/{total}:", _INDENT))
|
|
493
|
-
match errors:
|
|
494
|
-
case ExcGroupTB() | ExcTB(): # pragma: no cover
|
|
495
|
-
lines.append(errors.format(header=False, detail=detail, depth=2))
|
|
496
|
-
case BaseException(): # pragma: no cover
|
|
497
|
-
lines.append(_format_exception(errors, depth=2))
|
|
498
|
-
case _ as never:
|
|
499
|
-
assert_never(never)
|
|
500
|
-
lines.append("")
|
|
501
|
-
return indent("\n".join(lines), depth * _INDENT) # skipif-ci
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
@dataclass(kw_only=True, slots=True)
|
|
505
|
-
class ExcTB(Generic[TBaseException]):
|
|
506
|
-
"""A rich traceback for a single exception."""
|
|
507
|
-
|
|
508
|
-
frames: list[_Frame] = field(default_factory=list)
|
|
509
|
-
error: TBaseException
|
|
510
|
-
start: MaybeCallableDateTime | None = field(default=_START, repr=False)
|
|
511
|
-
version: MaybeCallableVersionLike | None = field(default=None, repr=False)
|
|
512
|
-
max_width: int = RICH_MAX_WIDTH
|
|
513
|
-
indent_size: int = RICH_INDENT_SIZE
|
|
514
|
-
max_length: int | None = RICH_MAX_LENGTH
|
|
515
|
-
max_string: int | None = RICH_MAX_STRING
|
|
516
|
-
max_depth: int | None = RICH_MAX_DEPTH
|
|
517
|
-
expand_all: bool = RICH_EXPAND_ALL
|
|
518
|
-
|
|
519
|
-
def __getitem__(self, i: int, /) -> _Frame:
|
|
520
|
-
return self.frames[i]
|
|
521
|
-
|
|
522
|
-
def __iter__(self) -> Iterator[_Frame]:
|
|
523
|
-
yield from self.frames
|
|
524
|
-
|
|
525
|
-
def __len__(self) -> int:
|
|
526
|
-
return len(self.frames)
|
|
527
|
-
|
|
528
|
-
@override
|
|
529
|
-
def __repr__(self) -> str:
|
|
530
|
-
return self.format(header=True, detail=True)
|
|
531
|
-
|
|
532
|
-
def format(
|
|
533
|
-
self, *, header: bool = False, detail: bool = False, depth: int = 0
|
|
534
|
-
) -> str:
|
|
535
|
-
"""Format the traceback."""
|
|
536
|
-
total = len(self)
|
|
537
|
-
lines: list[str] = []
|
|
538
|
-
if header: # pragma: no cover
|
|
539
|
-
lines.extend(_yield_header_lines(start=self.start, version=self.version))
|
|
540
|
-
for i, frame in enumerate(self.frames):
|
|
541
|
-
is_head = i < total - 1
|
|
542
|
-
lines.append(
|
|
543
|
-
frame.format(
|
|
544
|
-
index=i,
|
|
545
|
-
total=total,
|
|
546
|
-
detail=detail,
|
|
547
|
-
error=None if is_head else self.error,
|
|
548
|
-
)
|
|
549
|
-
)
|
|
550
|
-
if detail and is_head:
|
|
551
|
-
lines.append("")
|
|
552
|
-
return indent("\n".join(lines), depth * _INDENT)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
@dataclass(kw_only=True, slots=True)
|
|
556
|
-
class _Frame:
|
|
557
|
-
module: str | None = None
|
|
558
|
-
name: str
|
|
559
|
-
code_line: str
|
|
560
|
-
line_num: int
|
|
561
|
-
args: tuple[Any, ...] = field(default_factory=tuple)
|
|
562
|
-
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
563
|
-
locals: dict[str, Any] = field(default_factory=dict)
|
|
564
|
-
max_width: int = RICH_MAX_WIDTH
|
|
565
|
-
indent_size: int = RICH_INDENT_SIZE
|
|
566
|
-
max_length: int | None = RICH_MAX_LENGTH
|
|
567
|
-
max_string: int | None = RICH_MAX_STRING
|
|
568
|
-
max_depth: int | None = RICH_MAX_DEPTH
|
|
569
|
-
expand_all: bool = RICH_EXPAND_ALL
|
|
570
|
-
|
|
571
|
-
@override
|
|
572
|
-
def __repr__(self) -> str:
|
|
573
|
-
return self.format(detail=True)
|
|
574
|
-
|
|
575
|
-
def format(
|
|
576
|
-
self,
|
|
577
|
-
*,
|
|
578
|
-
index: int = 0,
|
|
579
|
-
total: int = 1,
|
|
580
|
-
detail: bool = False,
|
|
581
|
-
error: BaseException | None = None,
|
|
582
|
-
depth: int = 0,
|
|
583
|
-
) -> str:
|
|
584
|
-
"""Format the traceback."""
|
|
585
|
-
lines: list[str] = [f"Frame {index + 1}/{total}: {self.name} ({self.module})"]
|
|
586
|
-
if detail:
|
|
587
|
-
lines.append(indent("Inputs:", _INDENT))
|
|
588
|
-
lines.extend(
|
|
589
|
-
indent(line, 2 * _INDENT)
|
|
590
|
-
for line in yield_call_args_repr(
|
|
591
|
-
*self.args,
|
|
592
|
-
_max_width=self.max_width,
|
|
593
|
-
_indent_size=self.indent_size,
|
|
594
|
-
_max_length=self.max_length,
|
|
595
|
-
_max_string=self.max_string,
|
|
596
|
-
_max_depth=self.max_depth,
|
|
597
|
-
_expand_all=self.expand_all,
|
|
598
|
-
**self.kwargs,
|
|
599
|
-
)
|
|
600
|
-
)
|
|
601
|
-
lines.append(indent("Locals:", _INDENT))
|
|
602
|
-
lines.extend(
|
|
603
|
-
indent(line, 2 * _INDENT)
|
|
604
|
-
for line in yield_mapping_repr(
|
|
605
|
-
self.locals,
|
|
606
|
-
_max_width=self.max_width,
|
|
607
|
-
_indent_size=self.indent_size,
|
|
608
|
-
_max_length=self.max_length,
|
|
609
|
-
_max_string=self.max_string,
|
|
610
|
-
_max_depth=self.max_depth,
|
|
611
|
-
_expand_all=self.expand_all,
|
|
612
|
-
)
|
|
613
|
-
)
|
|
614
|
-
lines.extend([
|
|
615
|
-
indent(f"Line {self.line_num}:", _INDENT),
|
|
616
|
-
indent(self.code_line, 2 * _INDENT),
|
|
617
|
-
])
|
|
618
|
-
if error is not None:
|
|
619
|
-
lines.extend([
|
|
620
|
-
indent("Raised:", _INDENT),
|
|
621
|
-
_format_exception(error, depth=2),
|
|
622
|
-
])
|
|
623
|
-
return indent("\n".join(lines), depth * _INDENT)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
##
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
def get_rich_traceback(
|
|
630
|
-
error: TBaseException,
|
|
631
|
-
/,
|
|
632
|
-
*,
|
|
633
|
-
start: MaybeCallableDateTime | None = _START,
|
|
634
|
-
version: MaybeCallableVersionLike | None = None,
|
|
635
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
636
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
637
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
638
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
639
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
640
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
641
|
-
) -> (
|
|
642
|
-
ExcChainTB[TBaseException]
|
|
643
|
-
| ExcGroupTB[TBaseException]
|
|
644
|
-
| ExcTB[TBaseException]
|
|
645
|
-
| TBaseException
|
|
646
|
-
):
|
|
647
|
-
"""Get a rich traceback."""
|
|
648
|
-
match list(yield_exceptions(error)):
|
|
649
|
-
case []: # pragma: no cover
|
|
650
|
-
raise ImpossibleCaseError(case=[f"{error}"])
|
|
651
|
-
case [err]:
|
|
652
|
-
err_recast = cast("TBaseException", err)
|
|
653
|
-
return _get_rich_traceback_non_chain(
|
|
654
|
-
err_recast,
|
|
655
|
-
start=start,
|
|
656
|
-
version=version,
|
|
657
|
-
max_width=max_width,
|
|
658
|
-
indent_size=indent_size,
|
|
659
|
-
max_length=max_length,
|
|
660
|
-
max_string=max_string,
|
|
661
|
-
max_depth=max_depth,
|
|
662
|
-
expand_all=expand_all,
|
|
663
|
-
)
|
|
664
|
-
case errs:
|
|
665
|
-
errs_recast = cast("list[TBaseException]", errs)
|
|
666
|
-
return ExcChainTB(
|
|
667
|
-
errors=[
|
|
668
|
-
_get_rich_traceback_non_chain(
|
|
669
|
-
e,
|
|
670
|
-
start=start,
|
|
671
|
-
version=version,
|
|
672
|
-
max_width=max_width,
|
|
673
|
-
indent_size=indent_size,
|
|
674
|
-
max_length=max_length,
|
|
675
|
-
max_string=max_string,
|
|
676
|
-
max_depth=max_depth,
|
|
677
|
-
expand_all=expand_all,
|
|
678
|
-
)
|
|
679
|
-
for e in errs_recast
|
|
680
|
-
],
|
|
681
|
-
start=start,
|
|
682
|
-
version=version,
|
|
683
|
-
max_width=max_width,
|
|
684
|
-
indent_size=indent_size,
|
|
685
|
-
max_length=max_length,
|
|
686
|
-
max_string=max_string,
|
|
687
|
-
max_depth=max_depth,
|
|
688
|
-
expand_all=expand_all,
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
def _get_rich_traceback_non_chain(
|
|
693
|
-
error: ExceptionGroup[Any] | TBaseException,
|
|
694
|
-
/,
|
|
695
|
-
*,
|
|
696
|
-
start: MaybeCallableDateTime | None = _START,
|
|
697
|
-
version: MaybeCallableVersionLike | None = None,
|
|
698
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
699
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
700
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
701
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
702
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
703
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
704
|
-
) -> ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException:
|
|
705
|
-
"""Get a rich traceback, for a non-chained error."""
|
|
706
|
-
match error:
|
|
707
|
-
case ExceptionGroup() as exc_group: # skipif-ci
|
|
708
|
-
exc_group_or_exc_tb = _get_rich_traceback_base_one(
|
|
709
|
-
exc_group,
|
|
710
|
-
max_width=max_width,
|
|
711
|
-
indent_size=indent_size,
|
|
712
|
-
max_length=max_length,
|
|
713
|
-
max_string=max_string,
|
|
714
|
-
max_depth=max_depth,
|
|
715
|
-
expand_all=expand_all,
|
|
716
|
-
)
|
|
717
|
-
errors = [
|
|
718
|
-
_get_rich_traceback_non_chain(
|
|
719
|
-
e,
|
|
720
|
-
start=start,
|
|
721
|
-
version=version,
|
|
722
|
-
max_width=max_width,
|
|
723
|
-
indent_size=indent_size,
|
|
724
|
-
max_length=max_length,
|
|
725
|
-
max_string=max_string,
|
|
726
|
-
max_depth=max_depth,
|
|
727
|
-
expand_all=expand_all,
|
|
728
|
-
)
|
|
729
|
-
for e in always_iterable(exc_group.exceptions)
|
|
730
|
-
]
|
|
731
|
-
return ExcGroupTB(
|
|
732
|
-
exc_group=exc_group_or_exc_tb,
|
|
733
|
-
errors=errors,
|
|
734
|
-
start=start,
|
|
735
|
-
version=version,
|
|
736
|
-
max_width=max_width,
|
|
737
|
-
indent_size=indent_size,
|
|
738
|
-
max_length=max_length,
|
|
739
|
-
max_string=max_string,
|
|
740
|
-
max_depth=max_depth,
|
|
741
|
-
expand_all=expand_all,
|
|
742
|
-
)
|
|
743
|
-
case BaseException() as base_exc:
|
|
744
|
-
return _get_rich_traceback_base_one(
|
|
745
|
-
base_exc,
|
|
746
|
-
start=start,
|
|
747
|
-
version=version,
|
|
748
|
-
max_width=max_width,
|
|
749
|
-
indent_size=indent_size,
|
|
750
|
-
max_length=max_length,
|
|
751
|
-
max_string=max_string,
|
|
752
|
-
max_depth=max_depth,
|
|
753
|
-
expand_all=expand_all,
|
|
754
|
-
)
|
|
755
|
-
case _ as never:
|
|
756
|
-
assert_never(never)
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
def _get_rich_traceback_base_one(
|
|
760
|
-
error: TBaseException,
|
|
761
|
-
/,
|
|
762
|
-
*,
|
|
763
|
-
start: MaybeCallableDateTime | None = _START,
|
|
764
|
-
version: MaybeCallableVersionLike | None = None,
|
|
765
|
-
max_width: int = RICH_MAX_WIDTH,
|
|
766
|
-
indent_size: int = RICH_INDENT_SIZE,
|
|
767
|
-
max_length: int | None = RICH_MAX_LENGTH,
|
|
768
|
-
max_string: int | None = RICH_MAX_STRING,
|
|
769
|
-
max_depth: int | None = RICH_MAX_DEPTH,
|
|
770
|
-
expand_all: bool = RICH_EXPAND_ALL,
|
|
771
|
-
) -> ExcTB[TBaseException] | TBaseException:
|
|
772
|
-
"""Get a rich traceback, for a single exception."""
|
|
773
|
-
if isinstance(error, _HasExceptionPath):
|
|
774
|
-
frames = [
|
|
775
|
-
_Frame(
|
|
776
|
-
module=f.module,
|
|
777
|
-
name=f.name,
|
|
778
|
-
code_line=f.code_line,
|
|
779
|
-
line_num=f.line_num,
|
|
780
|
-
args=f.extra.args,
|
|
781
|
-
kwargs=f.extra.kwargs,
|
|
782
|
-
locals=f.locals,
|
|
783
|
-
max_width=max_width,
|
|
784
|
-
indent_size=indent_size,
|
|
785
|
-
max_length=max_length,
|
|
786
|
-
max_string=max_string,
|
|
787
|
-
max_depth=max_depth,
|
|
788
|
-
expand_all=expand_all,
|
|
789
|
-
)
|
|
790
|
-
for f in error.exc_tb.frames
|
|
791
|
-
]
|
|
792
|
-
return ExcTB(
|
|
793
|
-
frames=frames,
|
|
794
|
-
error=error,
|
|
795
|
-
start=start,
|
|
796
|
-
version=version,
|
|
797
|
-
max_width=max_width,
|
|
798
|
-
indent_size=indent_size,
|
|
799
|
-
max_length=max_length,
|
|
800
|
-
max_string=max_string,
|
|
801
|
-
max_depth=max_depth,
|
|
802
|
-
expand_all=expand_all,
|
|
803
|
-
)
|
|
804
|
-
return error
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
def trace(func: TCallable, /) -> TCallable:
|
|
808
|
-
"""Trace a function call."""
|
|
809
|
-
match bool(iscoroutinefunction(func)):
|
|
810
|
-
case False:
|
|
811
|
-
func_typed = cast("Callable[..., Any]", func)
|
|
812
|
-
|
|
813
|
-
@wraps(func)
|
|
814
|
-
def trace_sync(*args: Any, **kwargs: Any) -> Any:
|
|
815
|
-
locals()[_CALL_ARGS] = _CallArgs.create(func, *args, **kwargs)
|
|
816
|
-
try:
|
|
817
|
-
return func_typed(*args, **kwargs)
|
|
818
|
-
except Exception as error:
|
|
819
|
-
cast("Any", error).exc_tb = _get_rich_traceback_internal(error)
|
|
820
|
-
raise
|
|
821
|
-
|
|
822
|
-
return cast("TCallable", trace_sync)
|
|
823
|
-
case True:
|
|
824
|
-
func_typed = cast("Callable[..., Coroutine1[Any]]", func)
|
|
825
|
-
|
|
826
|
-
@wraps(func)
|
|
827
|
-
async def trace_async(*args: Any, **kwargs: Any) -> Any:
|
|
828
|
-
locals()[_CALL_ARGS] = _CallArgs.create(func, *args, **kwargs)
|
|
829
|
-
try: # skipif-ci
|
|
830
|
-
return await func_typed(*args, **kwargs)
|
|
831
|
-
except Exception as error: # skipif-ci
|
|
832
|
-
cast("Any", error).exc_tb = _get_rich_traceback_internal(error)
|
|
833
|
-
raise
|
|
834
|
-
|
|
835
|
-
return cast("TCallable", trace_async)
|
|
836
|
-
case _ as never:
|
|
837
|
-
assert_never(never)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
@overload
|
|
841
|
-
def yield_extended_frame_summaries(
|
|
842
|
-
error: BaseException, /, *, extra: Callable[[FrameSummary, FrameType], _T]
|
|
843
|
-
) -> Iterator[_ExtFrameSummary[_T]]: ...
|
|
844
|
-
@overload
|
|
845
|
-
def yield_extended_frame_summaries(
|
|
846
|
-
error: BaseException, /, *, extra: None = None
|
|
847
|
-
) -> Iterator[_ExtFrameSummary[None]]: ...
|
|
848
|
-
def yield_extended_frame_summaries(
|
|
849
|
-
error: BaseException,
|
|
850
|
-
/,
|
|
851
|
-
*,
|
|
852
|
-
extra: Callable[[FrameSummary, FrameType], _T] | None = None,
|
|
853
|
-
) -> Iterator[_ExtFrameSummary[Any]]:
|
|
854
|
-
"""Yield the extended frame summaries."""
|
|
855
|
-
tb_exc = TracebackException.from_exception(error, capture_locals=True)
|
|
856
|
-
_, _, traceback = exc_info()
|
|
857
|
-
frames = yield_frames(traceback=traceback)
|
|
858
|
-
for summary, frame in zip(tb_exc.stack, frames, strict=True):
|
|
859
|
-
if extra is None:
|
|
860
|
-
extra_use: _T | None = None
|
|
861
|
-
else:
|
|
862
|
-
extra_use: _T | None = extra(summary, frame)
|
|
863
|
-
yield _ExtFrameSummary(
|
|
864
|
-
filename=Path(summary.filename),
|
|
865
|
-
module=frame.f_globals.get("__name__"),
|
|
866
|
-
name=summary.name,
|
|
867
|
-
qualname=frame.f_code.co_qualname,
|
|
868
|
-
code_line=ensure_not_none(summary.line, desc="summary.line"),
|
|
869
|
-
first_line_num=frame.f_code.co_firstlineno,
|
|
870
|
-
line_num=ensure_not_none(summary.lineno, desc="summary.lineno"),
|
|
871
|
-
end_line_num=ensure_not_none(summary.end_lineno, desc="summary.end_lineno"),
|
|
872
|
-
col_num=summary.colno,
|
|
873
|
-
end_col_num=summary.end_colno,
|
|
874
|
-
locals=frame.f_locals,
|
|
875
|
-
extra=extra_use,
|
|
876
|
-
)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
def yield_exceptions(error: BaseException, /) -> Iterator[BaseException]:
|
|
880
|
-
"""Yield the exceptions in a context chain."""
|
|
881
|
-
curr: BaseException | None = error
|
|
882
|
-
while curr is not None:
|
|
883
|
-
yield curr
|
|
884
|
-
curr = curr.__context__
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
def yield_frames(*, traceback: TracebackType | None = None) -> Iterator[FrameType]:
|
|
888
|
-
"""Yield the frames of a traceback."""
|
|
889
|
-
while traceback is not None:
|
|
890
|
-
yield traceback.tb_frame
|
|
891
|
-
traceback = traceback.tb_next
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
def _format_exception(error: BaseException, /, *, depth: int = 0) -> str:
|
|
895
|
-
"""Format an exception."""
|
|
896
|
-
name = get_class_name(error, qual=True)
|
|
897
|
-
line = f"{name}({error})"
|
|
898
|
-
return indent(line, depth * _INDENT)
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
def _get_rich_traceback_internal(error: BaseException, /) -> _ExcTBInternal:
|
|
902
|
-
"""Get a rich traceback; for internal use only."""
|
|
903
|
-
|
|
904
|
-
def extra(_: FrameSummary, frame: FrameType) -> _CallArgs | None:
|
|
905
|
-
return frame.f_locals.get(_CALL_ARGS)
|
|
906
|
-
|
|
907
|
-
raw = list(yield_extended_frame_summaries(error, extra=extra))
|
|
908
|
-
return _ExcTBInternal(raw=raw, frames=_merge_frames(raw), error=error)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
def _merge_frames(
|
|
912
|
-
frames: Iterable[_ExtFrameSummaryCAOpt], /
|
|
913
|
-
) -> list[_ExtFrameSummaryCA]:
|
|
914
|
-
"""Merge a set of frames."""
|
|
915
|
-
rev = list(frames)[::-1]
|
|
916
|
-
values: list[_ExtFrameSummaryCA] = []
|
|
917
|
-
|
|
918
|
-
def get_solution(
|
|
919
|
-
curr: _ExtFrameSummaryCAOpt, rev: list[_ExtFrameSummaryCAOpt], /
|
|
920
|
-
) -> _ExtFrameSummaryCA:
|
|
921
|
-
while True:
|
|
922
|
-
next_ = rev.pop(0)
|
|
923
|
-
if has_extra(next_) and is_match(curr, next_):
|
|
924
|
-
return next_
|
|
925
|
-
|
|
926
|
-
def has_extra(frame: _ExtFrameSummaryCAOpt, /) -> TypeGuard[_ExtFrameSummaryCA]:
|
|
927
|
-
return frame.extra is not None
|
|
928
|
-
|
|
929
|
-
def has_match(
|
|
930
|
-
curr: _ExtFrameSummaryCAOpt, rev: list[_ExtFrameSummaryCAOpt], /
|
|
931
|
-
) -> bool:
|
|
932
|
-
next_, *_ = filter(has_extra, rev)
|
|
933
|
-
return is_match(curr, next_)
|
|
934
|
-
|
|
935
|
-
def is_match(curr: _ExtFrameSummaryCAOpt, next_: _ExtFrameSummaryCA, /) -> bool:
|
|
936
|
-
return (curr.name == next_.extra.func.__name__) and (
|
|
937
|
-
(curr.module is None) or (curr.module == next_.extra.func.__module__)
|
|
938
|
-
)
|
|
939
|
-
|
|
940
|
-
while len(rev) >= 1:
|
|
941
|
-
curr = rev.pop(0)
|
|
942
|
-
if not has_match(curr, rev):
|
|
943
|
-
continue
|
|
944
|
-
next_ = get_solution(curr, rev)
|
|
945
|
-
new = cast("_ExtFrameSummaryCA", replace(curr, extra=next_.extra))
|
|
946
|
-
values.append(new)
|
|
947
|
-
return values[::-1]
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
##
|
|
951
|
-
|
|
952
|
-
|
|
953
91
|
def _yield_header_lines(
|
|
954
92
|
*,
|
|
955
|
-
start:
|
|
93
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
956
94
|
version: MaybeCallableVersionLike | None = None,
|
|
957
95
|
) -> Iterator[str]:
|
|
958
96
|
"""Yield the header lines."""
|
|
959
|
-
from utilities.tzlocal import get_local_time_zone, get_now_local
|
|
960
|
-
from utilities.whenever import serialize_zoned_datetime
|
|
961
|
-
|
|
962
97
|
now = get_now_local()
|
|
963
|
-
|
|
964
|
-
start_use = (
|
|
965
|
-
|
|
966
|
-
)
|
|
967
|
-
yield f"
|
|
968
|
-
|
|
969
|
-
yield f"
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
yield f"Duration | {duration_str}"
|
|
973
|
-
yield f"User | {getuser()}"
|
|
974
|
-
yield f"Host | {gethostname()}"
|
|
975
|
-
version_use = "" if version is None else get_version(version=version)
|
|
976
|
-
yield f"Version | {version_use}"
|
|
98
|
+
yield f"Date/time | {format_compact(now)}"
|
|
99
|
+
start_use = to_zoned_date_time(start).to_tz(LOCAL_TIME_ZONE_NAME)
|
|
100
|
+
yield f"Started | {format_compact(start_use)}"
|
|
101
|
+
yield f"Duration | {(now - start_use).format_iso()}"
|
|
102
|
+
yield f"User | {getuser()}"
|
|
103
|
+
yield f"Host | {gethostname()}"
|
|
104
|
+
yield f"Process ID | {getpid()}"
|
|
105
|
+
version_use = "" if version is None else to_version(version)
|
|
106
|
+
yield f"Version | {version_use}"
|
|
977
107
|
yield ""
|
|
978
108
|
|
|
979
109
|
|
|
980
|
-
##
|
|
981
|
-
|
|
982
|
-
|
|
983
110
|
def _yield_formatted_frame_summary(
|
|
984
111
|
error: BaseException,
|
|
985
112
|
/,
|
|
@@ -1051,7 +178,7 @@ def _path_to_dots(path: PathLike, /) -> str:
|
|
|
1051
178
|
if (new_path := _trim_path(path, pattern)) is not None:
|
|
1052
179
|
break
|
|
1053
180
|
path_use = Path(path) if new_path is None else new_path
|
|
1054
|
-
return
|
|
181
|
+
return module_path(path_use)
|
|
1055
182
|
|
|
1056
183
|
|
|
1057
184
|
def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
|
|
@@ -1064,6 +191,120 @@ def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
|
|
|
1064
191
|
return Path(*parts[i + 1 :])
|
|
1065
192
|
|
|
1066
193
|
|
|
194
|
+
##
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def make_except_hook(
|
|
198
|
+
*,
|
|
199
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
200
|
+
version: MaybeCallableVersionLike | None = None,
|
|
201
|
+
path: MaybeCallablePathLike | None = None,
|
|
202
|
+
path_max_age: Delta | None = None,
|
|
203
|
+
max_width: int = RICH_MAX_WIDTH,
|
|
204
|
+
indent_size: int = RICH_INDENT_SIZE,
|
|
205
|
+
max_length: int | None = RICH_MAX_LENGTH,
|
|
206
|
+
max_string: int | None = RICH_MAX_STRING,
|
|
207
|
+
max_depth: int | None = RICH_MAX_DEPTH,
|
|
208
|
+
expand_all: bool = RICH_EXPAND_ALL,
|
|
209
|
+
slack_url: str | None = None,
|
|
210
|
+
pudb: MaybeCallableBoolLike = False,
|
|
211
|
+
) -> Callable[
|
|
212
|
+
[type[BaseException] | None, BaseException | None, TracebackType | None], None
|
|
213
|
+
]:
|
|
214
|
+
"""Exception hook to log the traceback."""
|
|
215
|
+
return partial(
|
|
216
|
+
_make_except_hook_inner,
|
|
217
|
+
start=start,
|
|
218
|
+
version=version,
|
|
219
|
+
path=path,
|
|
220
|
+
path_max_age=path_max_age,
|
|
221
|
+
max_width=max_width,
|
|
222
|
+
indent_size=indent_size,
|
|
223
|
+
max_length=max_length,
|
|
224
|
+
max_string=max_string,
|
|
225
|
+
max_depth=max_depth,
|
|
226
|
+
expand_all=expand_all,
|
|
227
|
+
slack_url=slack_url,
|
|
228
|
+
pudb=pudb,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _make_except_hook_inner(
|
|
233
|
+
exc_type: type[BaseException] | None,
|
|
234
|
+
exc_val: BaseException | None,
|
|
235
|
+
traceback: TracebackType | None,
|
|
236
|
+
/,
|
|
237
|
+
*,
|
|
238
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
239
|
+
version: MaybeCallableVersionLike | None = None,
|
|
240
|
+
path: MaybeCallablePathLike | None = None,
|
|
241
|
+
path_max_age: Delta | None = None,
|
|
242
|
+
max_width: int = RICH_MAX_WIDTH,
|
|
243
|
+
indent_size: int = RICH_INDENT_SIZE,
|
|
244
|
+
max_length: int | None = RICH_MAX_LENGTH,
|
|
245
|
+
max_string: int | None = RICH_MAX_STRING,
|
|
246
|
+
max_depth: int | None = RICH_MAX_DEPTH,
|
|
247
|
+
expand_all: bool = RICH_EXPAND_ALL,
|
|
248
|
+
slack_url: str | None = None,
|
|
249
|
+
pudb: MaybeCallableBoolLike = False,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Exception hook to log the traceback."""
|
|
252
|
+
_ = (exc_type, traceback)
|
|
253
|
+
if exc_val is None:
|
|
254
|
+
raise MakeExceptHookError
|
|
255
|
+
slim = format_exception_stack(exc_val, header=True, start=start, version=version)
|
|
256
|
+
_ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
|
|
257
|
+
if path is not None:
|
|
258
|
+
path = to_path(path)
|
|
259
|
+
path_log = path.joinpath(
|
|
260
|
+
format_compact(get_now_local(), path=True)
|
|
261
|
+
).with_suffix(".txt")
|
|
262
|
+
full = format_exception_stack(
|
|
263
|
+
exc_val,
|
|
264
|
+
header=True,
|
|
265
|
+
start=start,
|
|
266
|
+
version=version,
|
|
267
|
+
capture_locals=True,
|
|
268
|
+
max_width=max_width,
|
|
269
|
+
indent_size=indent_size,
|
|
270
|
+
max_length=max_length,
|
|
271
|
+
max_string=max_string,
|
|
272
|
+
max_depth=max_depth,
|
|
273
|
+
expand_all=expand_all,
|
|
274
|
+
)
|
|
275
|
+
with writer(path_log, overwrite=True) as temp:
|
|
276
|
+
_ = temp.write_text(full)
|
|
277
|
+
if path_max_age is not None:
|
|
278
|
+
_make_except_hook_purge(path, path_max_age)
|
|
279
|
+
if slack_url is not None: # pragma: no cover
|
|
280
|
+
from utilities.slack_sdk import SendToSlackError, send_to_slack
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
send_to_slack(slack_url, f"```{slim}```")
|
|
284
|
+
except SendToSlackError as error:
|
|
285
|
+
_ = stderr.write(f"{error}\n")
|
|
286
|
+
if to_bool(pudb): # pragma: no cover
|
|
287
|
+
from pudb import post_mortem # pyright: ignore[reportMissingImports]
|
|
288
|
+
|
|
289
|
+
post_mortem(tb=traceback, e_type=exc_type, e_value=exc_val)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _make_except_hook_purge(path: PathLike, max_age: Delta, /) -> None:
|
|
293
|
+
threshold = get_now_local() - max_age
|
|
294
|
+
paths: set[Path] = set()
|
|
295
|
+
for p in Path(path).iterdir():
|
|
296
|
+
if p.is_file():
|
|
297
|
+
try:
|
|
298
|
+
date_time = to_zoned_date_time(p.stem)
|
|
299
|
+
except ValueError:
|
|
300
|
+
pass
|
|
301
|
+
else:
|
|
302
|
+
if date_time <= threshold:
|
|
303
|
+
paths.add(p)
|
|
304
|
+
for p in paths:
|
|
305
|
+
p.unlink(missing_ok=True)
|
|
306
|
+
|
|
307
|
+
|
|
1067
308
|
@dataclass(kw_only=True, slots=True)
|
|
1068
309
|
class MakeExceptHookError(Exception):
|
|
1069
310
|
@override
|
|
@@ -1071,16 +312,4 @@ class MakeExceptHookError(Exception):
|
|
|
1071
312
|
return "No exception to log"
|
|
1072
313
|
|
|
1073
314
|
|
|
1074
|
-
__all__ = [
|
|
1075
|
-
"ExcChainTB",
|
|
1076
|
-
"ExcGroupTB",
|
|
1077
|
-
"ExcTB",
|
|
1078
|
-
"RichTracebackFormatter",
|
|
1079
|
-
"format_exception_stack",
|
|
1080
|
-
"get_rich_traceback",
|
|
1081
|
-
"make_except_hook",
|
|
1082
|
-
"trace",
|
|
1083
|
-
"yield_exceptions",
|
|
1084
|
-
"yield_extended_frame_summaries",
|
|
1085
|
-
"yield_frames",
|
|
1086
|
-
]
|
|
315
|
+
__all__ = ["format_exception_stack", "make_except_hook"]
|