dycw-utilities 0.129.8__py3-none-any.whl → 0.129.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.129.8
3
+ Version: 0.129.9
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=nud8TbHJzm02w0ycVV5vCV-F8CpfmCs3mB9e6p4DrdQ,60
1
+ utilities/__init__.py,sha256=uFYFepkZNArEi_sHiw7tlOn88WgLGjxnMihyD4GzfOQ,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=3n5EIcSq2xtEF1i4oR0oY2JmBq3NyugeHKFK39Mt22s,37987
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -78,7 +78,7 @@ utilities/tenacity.py,sha256=1PUvODiBVgeqIh7G5TRt5WWMSqjLYkEqP53itT97WQc,4914
78
78
  utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
79
79
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
80
80
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
81
- utilities/traceback.py,sha256=Rf_4XIz6AQaBcTRr7Tiw7RCIv_O_bN7Hd-Cnr8SPXN4,28920
81
+ utilities/traceback.py,sha256=EUm-5jhH1RICBmsG_H77-CXvD-IVErfkzGRSqnvR0dI,35782
82
82
  utilities/types.py,sha256=gP04CcCOyFrG7BgblVCsrrChiuO2x842NDVW-GF7odo,18370
83
83
  utilities/typing.py,sha256=H6ysJkI830aRwLsMKz0SZIw4cpcsm7d6KhQOwr-SDh0,13817
84
84
  utilities/tzdata.py,sha256=yCf70NICwAeazN3_JcXhWvRqCy06XJNQ42j7r6gw3HY,1217
@@ -89,7 +89,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
89
89
  utilities/whenever.py,sha256=QbXgFAPuUL7PCp2hajmIP-FFIfIR1J6Y0TxJbeoj60I,18434
90
90
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
91
91
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
92
- dycw_utilities-0.129.8.dist-info/METADATA,sha256=JaBYjbXepIOBDjYciDSC1_bSCE8VlzxhpOPAs22-nMI,12723
93
- dycw_utilities-0.129.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- dycw_utilities-0.129.8.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
- dycw_utilities-0.129.8.dist-info/RECORD,,
92
+ dycw_utilities-0.129.9.dist-info/METADATA,sha256=c0KdCG0ORHKUnqbJ4eaGnDoZNUDbRtkjupd_8Oqg22c,12723
93
+ dycw_utilities-0.129.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ dycw_utilities-0.129.9.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.129.9.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.129.8"
3
+ __version__ = "0.129.9"
utilities/traceback.py CHANGED
@@ -1,10 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
4
+ import sys
5
+ from asyncio import run
3
6
  from collections.abc import Callable, Iterable
4
7
  from dataclasses import dataclass, field, replace
5
- from functools import wraps
8
+ from functools import partial, wraps
6
9
  from getpass import getuser
7
10
  from inspect import iscoroutinefunction, signature
11
+ from itertools import repeat
8
12
  from logging import Formatter, Handler, LogRecord
9
13
  from pathlib import Path
10
14
  from socket import gethostname
@@ -26,8 +30,8 @@ from typing import (
26
30
  runtime_checkable,
27
31
  )
28
32
 
29
- from utilities.datetime import get_datetime, get_now
30
- from utilities.errors import ImpossibleCaseError
33
+ from utilities.datetime import get_datetime, get_now, serialize_compact
34
+ from utilities.errors import ImpossibleCaseError, repr_error
31
35
  from utilities.functions import (
32
36
  ensure_not_none,
33
37
  ensure_str,
@@ -35,7 +39,8 @@ from utilities.functions import (
35
39
  get_func_name,
36
40
  get_func_qualname,
37
41
  )
38
- from utilities.iterables import always_iterable, one
42
+ from utilities.iterables import OneEmptyError, always_iterable, one
43
+ from utilities.pathlib import get_path
39
44
  from utilities.reprlib import (
40
45
  RICH_EXPAND_ALL,
41
46
  RICH_INDENT_SIZE,
@@ -46,12 +51,18 @@ from utilities.reprlib import (
46
51
  yield_call_args_repr,
47
52
  yield_mapping_repr,
48
53
  )
49
- from utilities.types import MaybeCallableDateTime, TBaseException, TCallable
54
+ from utilities.types import (
55
+ MaybeCallableDateTime,
56
+ MaybeCallablePathLike,
57
+ PathLike,
58
+ TBaseException,
59
+ TCallable,
60
+ )
50
61
  from utilities.version import get_version
51
62
  from utilities.whenever import serialize_duration
52
63
 
53
64
  if TYPE_CHECKING:
54
- from collections.abc import Callable, Iterable, Iterator
65
+ from collections.abc import Callable, Iterable, Iterator, Sequence
55
66
  from logging import _FormatStyle
56
67
  from types import FrameType, TracebackType
57
68
 
@@ -68,6 +79,131 @@ _START = get_now()
68
79
  ##
69
80
 
70
81
 
82
+ def format_exception_stack(
83
+ error: BaseException,
84
+ /,
85
+ *,
86
+ header: bool = False,
87
+ start: MaybeCallableDateTime | None = _START,
88
+ version: MaybeCallableVersionLike | None = None,
89
+ capture_locals: bool = False,
90
+ max_width: int = RICH_MAX_WIDTH,
91
+ indent_size: int = RICH_INDENT_SIZE,
92
+ max_length: int | None = RICH_MAX_LENGTH,
93
+ max_string: int | None = RICH_MAX_STRING,
94
+ max_depth: int | None = RICH_MAX_DEPTH,
95
+ expand_all: bool = RICH_EXPAND_ALL,
96
+ ) -> str:
97
+ """Format an exception stack."""
98
+ lines: Sequence[str] = []
99
+ if header:
100
+ lines.extend(_yield_header_lines(start=start, version=version))
101
+ lines.extend(
102
+ _yield_formatted_frame_summary(
103
+ error,
104
+ capture_locals=capture_locals,
105
+ max_width=max_width,
106
+ indent_size=indent_size,
107
+ max_length=max_length,
108
+ max_string=max_string,
109
+ max_depth=max_depth,
110
+ expand_all=expand_all,
111
+ )
112
+ )
113
+ return "\n".join(lines)
114
+
115
+
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
+
71
207
  class RichTracebackFormatter(Formatter):
72
208
  """Formatter for rich tracebacks."""
73
209
 
@@ -811,6 +947,9 @@ def _merge_frames(
811
947
  return values[::-1]
812
948
 
813
949
 
950
+ ##
951
+
952
+
814
953
  def _yield_header_lines(
815
954
  *,
816
955
  start: MaybeCallableDateTime | None = _START,
@@ -838,12 +977,108 @@ def _yield_header_lines(
838
977
  yield ""
839
978
 
840
979
 
980
+ ##
981
+
982
+
983
+ def _yield_formatted_frame_summary(
984
+ error: BaseException,
985
+ /,
986
+ *,
987
+ capture_locals: bool = False,
988
+ max_width: int = RICH_MAX_WIDTH,
989
+ indent_size: int = RICH_INDENT_SIZE,
990
+ max_length: int | None = RICH_MAX_LENGTH,
991
+ max_string: int | None = RICH_MAX_STRING,
992
+ max_depth: int | None = RICH_MAX_DEPTH,
993
+ expand_all: bool = RICH_EXPAND_ALL,
994
+ ) -> Iterator[str]:
995
+ """Yield the formatted frame summary lines."""
996
+ stack = TracebackException.from_exception(
997
+ error, capture_locals=capture_locals
998
+ ).stack
999
+ n = len(stack)
1000
+ for i, frame in enumerate(stack, start=1):
1001
+ num = f"{i}/{n}"
1002
+ first, *rest = _yield_frame_summary_lines(
1003
+ frame,
1004
+ max_width=max_width,
1005
+ indent_size=indent_size,
1006
+ max_length=max_length,
1007
+ max_string=max_string,
1008
+ max_depth=max_depth,
1009
+ expand_all=expand_all,
1010
+ )
1011
+ yield f"{num} | {first}"
1012
+ blank = "".join(repeat(" ", len(num)))
1013
+ for rest_i in rest:
1014
+ yield f"{blank} | {rest_i}"
1015
+ yield repr_error(error)
1016
+
1017
+
1018
+ def _yield_frame_summary_lines(
1019
+ frame: FrameSummary,
1020
+ /,
1021
+ *,
1022
+ max_width: int = RICH_MAX_WIDTH,
1023
+ indent_size: int = RICH_INDENT_SIZE,
1024
+ max_length: int | None = RICH_MAX_LENGTH,
1025
+ max_string: int | None = RICH_MAX_STRING,
1026
+ max_depth: int | None = RICH_MAX_DEPTH,
1027
+ expand_all: bool = RICH_EXPAND_ALL,
1028
+ ) -> Iterator[str]:
1029
+ module = _path_to_dots(frame.filename)
1030
+ yield f"{module}:{frame.lineno} | {frame.name} | {frame.line}"
1031
+ if frame.locals is not None:
1032
+ yield from yield_mapping_repr(
1033
+ frame.locals,
1034
+ _max_width=max_width,
1035
+ _indent_size=indent_size,
1036
+ _max_length=max_length,
1037
+ _max_string=max_string,
1038
+ _max_depth=max_depth,
1039
+ _expand_all=expand_all,
1040
+ )
1041
+
1042
+
1043
+ def _path_to_dots(path: PathLike, /) -> str:
1044
+ new_path: Path | None = None
1045
+ for pattern in [
1046
+ "site-packages",
1047
+ ".venv", # after site-packages
1048
+ "src",
1049
+ r"python\d+\.\d+",
1050
+ ]:
1051
+ if (new_path := _trim_path(path, pattern)) is not None:
1052
+ break
1053
+ path_use = Path(path) if new_path is None else new_path
1054
+ return ".".join(path_use.with_suffix("").parts)
1055
+
1056
+
1057
+ def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
1058
+ parts = Path(path).parts
1059
+ compiled = re.compile(f"^{pattern}$")
1060
+ try:
1061
+ i = one(i for i, p in enumerate(parts) if compiled.search(p))
1062
+ except OneEmptyError:
1063
+ return None
1064
+ return Path(*parts[i + 1 :])
1065
+
1066
+
1067
+ @dataclass(kw_only=True, slots=True)
1068
+ class MakeExceptHookError(Exception):
1069
+ @override
1070
+ def __str__(self) -> str:
1071
+ return "No exception to log"
1072
+
1073
+
841
1074
  __all__ = [
842
1075
  "ExcChainTB",
843
1076
  "ExcGroupTB",
844
1077
  "ExcTB",
845
1078
  "RichTracebackFormatter",
1079
+ "format_exception_stack",
846
1080
  "get_rich_traceback",
1081
+ "make_except_hook",
847
1082
  "trace",
848
1083
  "yield_exceptions",
849
1084
  "yield_extended_frame_summaries",