dycw-utilities 0.129.14__py3-none-any.whl → 0.130.0__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.
utilities/traceback.py CHANGED
@@ -3,43 +3,19 @@ from __future__ import annotations
3
3
  import re
4
4
  import sys
5
5
  from asyncio import run
6
- from collections.abc import Callable, Iterable
7
- from dataclasses import dataclass, field, replace
8
- from functools import partial, wraps
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from functools import partial
9
9
  from getpass import getuser
10
- from inspect import iscoroutinefunction, signature
11
10
  from itertools import repeat
12
- from logging import Formatter, Handler, LogRecord
13
11
  from pathlib import Path
14
12
  from socket import gethostname
15
- from sys import exc_info
16
- from textwrap import indent
17
- from traceback import FrameSummary, TracebackException, format_exception
18
- from typing import (
19
- TYPE_CHECKING,
20
- Any,
21
- Generic,
22
- Protocol,
23
- Self,
24
- TypeGuard,
25
- TypeVar,
26
- assert_never,
27
- cast,
28
- overload,
29
- override,
30
- runtime_checkable,
31
- )
13
+ from traceback import TracebackException
14
+ from typing import TYPE_CHECKING, override
32
15
 
33
16
  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
17
+ from utilities.errors import repr_error
18
+ from utilities.iterables import OneEmptyError, one
43
19
  from utilities.pathlib import get_path
44
20
  from utilities.reprlib import (
45
21
  RICH_EXPAND_ALL,
@@ -48,35 +24,24 @@ 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.types import (
55
- MaybeCallableDateTime,
56
- MaybeCallablePathLike,
57
- PathLike,
58
- TBaseException,
59
- TCallable,
60
- )
61
29
  from utilities.version import get_version
62
30
  from utilities.whenever import serialize_duration
63
31
 
64
32
  if TYPE_CHECKING:
65
- from collections.abc import Callable, Iterable, Iterator, Sequence
66
- from logging import _FormatStyle
67
- from types import FrameType, TracebackType
33
+ from collections.abc import Callable, Iterator, Sequence
34
+ from traceback import FrameSummary
35
+ from types import TracebackType
68
36
 
69
- from utilities.types import Coroutine1, StrMapping
37
+ from utilities.types import MaybeCallableDateTime, MaybeCallablePathLike, PathLike
70
38
  from utilities.version import MaybeCallableVersionLike
71
39
 
72
40
 
73
- _T = TypeVar("_T")
74
- _CALL_ARGS = "_CALL_ARGS"
75
- _INDENT = 4 * " "
76
- _START = get_now()
41
+ ##
77
42
 
78
43
 
79
- ##
44
+ _START = get_now()
80
45
 
81
46
 
82
47
  def format_exception_stack(
@@ -113,843 +78,6 @@ def format_exception_stack(
113
78
  return "\n".join(lines)
114
79
 
115
80
 
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
81
  def _yield_header_lines(
954
82
  *,
955
83
  start: MaybeCallableDateTime | None = _START,
@@ -977,9 +105,6 @@ def _yield_header_lines(
977
105
  yield ""
978
106
 
979
107
 
980
- ##
981
-
982
-
983
108
  def _yield_formatted_frame_summary(
984
109
  error: BaseException,
985
110
  /,
@@ -1064,6 +189,94 @@ def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
1064
189
  return Path(*parts[i + 1 :])
1065
190
 
1066
191
 
192
+ ##
193
+
194
+
195
+ def make_except_hook(
196
+ *,
197
+ start: MaybeCallableDateTime | None = _START,
198
+ version: MaybeCallableVersionLike | None = None,
199
+ path: MaybeCallablePathLike | None = None,
200
+ max_width: int = RICH_MAX_WIDTH,
201
+ indent_size: int = RICH_INDENT_SIZE,
202
+ max_length: int | None = RICH_MAX_LENGTH,
203
+ max_string: int | None = RICH_MAX_STRING,
204
+ max_depth: int | None = RICH_MAX_DEPTH,
205
+ expand_all: bool = RICH_EXPAND_ALL,
206
+ slack_url: str | None = None,
207
+ ) -> Callable[
208
+ [type[BaseException] | None, BaseException | None, TracebackType | None], None
209
+ ]:
210
+ """Exception hook to log the traceback."""
211
+ return partial(
212
+ _make_except_hook_inner,
213
+ start=start,
214
+ version=version,
215
+ path=path,
216
+ max_width=max_width,
217
+ indent_size=indent_size,
218
+ max_length=max_length,
219
+ max_string=max_string,
220
+ max_depth=max_depth,
221
+ expand_all=expand_all,
222
+ slack_url=slack_url,
223
+ )
224
+
225
+
226
+ def _make_except_hook_inner(
227
+ exc_type: type[BaseException] | None,
228
+ exc_val: BaseException | None,
229
+ traceback: TracebackType | None,
230
+ /,
231
+ *,
232
+ start: MaybeCallableDateTime | None = _START,
233
+ version: MaybeCallableVersionLike | None = None,
234
+ path: MaybeCallablePathLike | None = None,
235
+ max_width: int = RICH_MAX_WIDTH,
236
+ indent_size: int = RICH_INDENT_SIZE,
237
+ max_length: int | None = RICH_MAX_LENGTH,
238
+ max_string: int | None = RICH_MAX_STRING,
239
+ max_depth: int | None = RICH_MAX_DEPTH,
240
+ expand_all: bool = RICH_EXPAND_ALL,
241
+ slack_url: str | None = None,
242
+ ) -> None:
243
+ """Exception hook to log the traceback."""
244
+ _ = (exc_type, traceback)
245
+ if exc_val is None:
246
+ raise MakeExceptHookError
247
+ slim = format_exception_stack(exc_val, header=True, start=start, version=version)
248
+ _ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
249
+ if path is not None:
250
+ from utilities.atomicwrites import writer
251
+ from utilities.tzlocal import get_now_local
252
+
253
+ path = (
254
+ get_path(path=path)
255
+ .joinpath(serialize_compact(get_now_local()))
256
+ .with_suffix(".txt")
257
+ )
258
+ full = format_exception_stack(
259
+ exc_val,
260
+ header=True,
261
+ start=start,
262
+ version=version,
263
+ capture_locals=True,
264
+ max_width=max_width,
265
+ indent_size=indent_size,
266
+ max_length=max_length,
267
+ max_string=max_string,
268
+ max_depth=max_depth,
269
+ expand_all=expand_all,
270
+ )
271
+ with writer(path, overwrite=True) as temp:
272
+ _ = temp.write_text(full)
273
+ if slack_url is not None: # pragma: no cover
274
+ from utilities.slack_sdk import send_to_slack
275
+
276
+ send = f"```{slim}```"
277
+ run(send_to_slack(slack_url, send))
278
+
279
+
1067
280
  @dataclass(kw_only=True, slots=True)
1068
281
  class MakeExceptHookError(Exception):
1069
282
  @override
@@ -1071,16 +284,4 @@ class MakeExceptHookError(Exception):
1071
284
  return "No exception to log"
1072
285
 
1073
286
 
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
- ]
287
+ __all__ = ["format_exception_stack", "make_except_hook"]