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.
Files changed (103) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.17.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +14 -14
  7. utilities/asyncio.py +350 -819
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +77 -22
  10. utilities/cachetools.py +24 -29
  11. utilities/click.py +393 -237
  12. utilities/concurrent.py +8 -11
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +83 -118
  17. utilities/docker.py +293 -0
  18. utilities/enum.py +26 -23
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +29 -65
  21. utilities/fpdf2.py +3 -3
  22. utilities/functions.py +169 -416
  23. utilities/functools.py +18 -19
  24. utilities/git.py +9 -30
  25. utilities/grp.py +28 -0
  26. utilities/gzip.py +31 -0
  27. utilities/http.py +3 -2
  28. utilities/hypothesis.py +738 -589
  29. utilities/importlib.py +17 -1
  30. utilities/inflect.py +25 -0
  31. utilities/iterables.py +194 -262
  32. utilities/jinja2.py +148 -0
  33. utilities/json.py +70 -0
  34. utilities/libcst.py +38 -17
  35. utilities/lightweight_charts.py +5 -9
  36. utilities/logging.py +345 -543
  37. utilities/math.py +18 -13
  38. utilities/memory_profiler.py +11 -15
  39. utilities/more_itertools.py +200 -131
  40. utilities/operator.py +33 -29
  41. utilities/optuna.py +6 -6
  42. utilities/orjson.py +272 -137
  43. utilities/os.py +61 -4
  44. utilities/parse.py +59 -61
  45. utilities/pathlib.py +281 -40
  46. utilities/permissions.py +298 -0
  47. utilities/pickle.py +2 -2
  48. utilities/platform.py +24 -5
  49. utilities/polars.py +1214 -430
  50. utilities/polars_ols.py +1 -1
  51. utilities/postgres.py +408 -0
  52. utilities/pottery.py +113 -26
  53. utilities/pqdm.py +10 -11
  54. utilities/psutil.py +6 -57
  55. utilities/pwd.py +28 -0
  56. utilities/pydantic.py +4 -54
  57. utilities/pydantic_settings.py +240 -0
  58. utilities/pydantic_settings_sops.py +76 -0
  59. utilities/pyinstrument.py +8 -10
  60. utilities/pytest.py +227 -121
  61. utilities/pytest_plugins/__init__.py +1 -0
  62. utilities/pytest_plugins/pytest_randomly.py +23 -0
  63. utilities/pytest_plugins/pytest_regressions.py +56 -0
  64. utilities/pytest_regressions.py +26 -46
  65. utilities/random.py +13 -9
  66. utilities/re.py +58 -28
  67. utilities/redis.py +401 -550
  68. utilities/scipy.py +1 -1
  69. utilities/sentinel.py +10 -0
  70. utilities/shelve.py +4 -1
  71. utilities/shutil.py +25 -0
  72. utilities/slack_sdk.py +36 -106
  73. utilities/sqlalchemy.py +502 -473
  74. utilities/sqlalchemy_polars.py +38 -94
  75. utilities/string.py +2 -3
  76. utilities/subprocess.py +1572 -0
  77. utilities/tempfile.py +86 -4
  78. utilities/testbook.py +50 -0
  79. utilities/text.py +165 -42
  80. utilities/timer.py +37 -65
  81. utilities/traceback.py +158 -929
  82. utilities/types.py +146 -116
  83. utilities/typing.py +531 -71
  84. utilities/tzdata.py +1 -53
  85. utilities/tzlocal.py +6 -23
  86. utilities/uuid.py +43 -5
  87. utilities/version.py +27 -26
  88. utilities/whenever.py +1776 -386
  89. utilities/zoneinfo.py +84 -22
  90. dycw_utilities-0.129.10.dist-info/METADATA +0 -241
  91. dycw_utilities-0.129.10.dist-info/RECORD +0 -96
  92. dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
  93. dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
  94. utilities/datetime.py +0 -1409
  95. utilities/eventkit.py +0 -402
  96. utilities/loguru.py +0 -144
  97. utilities/luigi.py +0 -228
  98. utilities/period.py +0 -324
  99. utilities/pyrsistent.py +0 -89
  100. utilities/python_dotenv.py +0 -105
  101. utilities/streamlit.py +0 -105
  102. utilities/sys.py +0 -87
  103. utilities/tenacity.py +0 -145
utilities/math.py CHANGED
@@ -8,9 +8,10 @@ from re import Match, search
8
8
  from typing import TYPE_CHECKING, Literal, assert_never, overload, override
9
9
 
10
10
  from utilities.errors import ImpossibleCaseError
11
+ from utilities.re import ExtractGroupsError, extract_groups
11
12
 
12
13
  if TYPE_CHECKING:
13
- from utilities.types import Number, RoundMode, Sign
14
+ from utilities.types import MathRoundMode, Number, Sign
14
15
 
15
16
 
16
17
  MIN_FLOAT32, MAX_FLOAT32 = -3.4028234663852886e38, 3.4028234663852886e38
@@ -640,7 +641,10 @@ def _is_close(
640
641
  ##
641
642
 
642
643
 
643
- def number_of_decimals(x: float, /, *, max_decimals: int = 20) -> int:
644
+ MAX_DECIMALS = 10
645
+
646
+
647
+ def number_of_decimals(x: float, /, *, max_decimals: int = MAX_DECIMALS) -> int:
644
648
  """Get the number of decimals."""
645
649
  _, frac = divmod(x, 1)
646
650
  results = (
@@ -708,7 +712,7 @@ def round_(
708
712
  x: float,
709
713
  /,
710
714
  *,
711
- mode: RoundMode = "standard",
715
+ mode: MathRoundMode = "standard",
712
716
  rel_tol: float | None = None,
713
717
  abs_tol: float | None = None,
714
718
  ) -> int:
@@ -730,7 +734,7 @@ def round_(
730
734
  return 0
731
735
  case -1:
732
736
  return floor(x)
733
- case _ as never:
737
+ case never:
734
738
  assert_never(never)
735
739
  case "standard-tie-floor":
736
740
  return _round_tie_standard(x, "floor", rel_tol=rel_tol, abs_tol=abs_tol)
@@ -742,13 +746,13 @@ def round_(
742
746
  )
743
747
  case "standard-tie-away-zero":
744
748
  return _round_tie_standard(x, "away-zero", rel_tol=rel_tol, abs_tol=abs_tol)
745
- case _ as never:
749
+ case never:
746
750
  assert_never(never)
747
751
 
748
752
 
749
753
  def _round_tie_standard(
750
754
  x: float,
751
- mode: RoundMode,
755
+ mode: MathRoundMode,
752
756
  /,
753
757
  *,
754
758
  rel_tol: float | None = None,
@@ -757,9 +761,9 @@ def _round_tie_standard(
757
761
  """Round a float to an integer using the standard method."""
758
762
  frac, _ = modf(x)
759
763
  if _is_close(abs(frac), 0.5, rel_tol=rel_tol, abs_tol=abs_tol):
760
- mode_use: RoundMode = mode
764
+ mode_use: MathRoundMode = mode
761
765
  else:
762
- mode_use: RoundMode = "standard"
766
+ mode_use: MathRoundMode = "standard"
763
767
  return round_(x, mode=mode_use)
764
768
 
765
769
 
@@ -775,9 +779,9 @@ def round_float_imprecisions(
775
779
  ) -> float:
776
780
  """Round a float, removing binary representation imprecisions."""
777
781
  try:
778
- ((head, tail),) = _ROUND_FLOAT_IMPRECISIONS_PATTERN.findall(str(x))
779
- except ValueError:
780
- ((head, tail),) = _ROUND_FLOAT_IMPRECISIONS_PATTERN.findall(f"{x:.20f}")
782
+ head, tail = extract_groups(_ROUND_FLOAT_IMPRECISIONS_PATTERN, str(x))
783
+ except ExtractGroupsError:
784
+ head, tail = extract_groups(_ROUND_FLOAT_IMPRECISIONS_PATTERN, f"{x:.20f}")
781
785
  half = ceil(decimals / 2)
782
786
  pattern0 = search(rf"^([0-9]+?)(0{{{half},}})([0-9]+?)$", tail)
783
787
  pattern9 = search(rf"^(0*)([0-9]+?)(9{{{half},}})([0-9]+?)$", tail)
@@ -823,7 +827,7 @@ def round_to_float(
823
827
  y: float,
824
828
  /,
825
829
  *,
826
- mode: RoundMode = "standard",
830
+ mode: MathRoundMode = "standard",
827
831
  rel_tol: float | None = None,
828
832
  abs_tol: float | None = None,
829
833
  ) -> float:
@@ -875,7 +879,7 @@ def sign(
875
879
  if is_negative(x, rel_tol=rel_tol, abs_tol=abs_tol):
876
880
  return -1
877
881
  return 0
878
- case _ as never:
882
+ case never:
879
883
  assert_never(never)
880
884
 
881
885
 
@@ -888,6 +892,7 @@ def significant_figures(x: float, /, *, n: int = 2) -> str:
888
892
 
889
893
 
890
894
  __all__ = [
895
+ "MAX_DECIMALS",
891
896
  "MAX_FLOAT32",
892
897
  "MAX_FLOAT64",
893
898
  "MAX_INT8",
@@ -2,31 +2,19 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from functools import wraps
5
- from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
5
+ from typing import TYPE_CHECKING, Any, cast
6
6
 
7
7
  from memory_profiler import memory_usage
8
- from typing_extensions import ParamSpec
9
8
 
10
9
  if TYPE_CHECKING:
11
10
  from collections.abc import Callable
12
11
 
13
- _P = ParamSpec("_P")
14
- _T = TypeVar("_T")
15
12
 
16
-
17
- @dataclass(kw_only=True, slots=True)
18
- class Output(Generic[_T]):
19
- """A function output, and its memory usage."""
20
-
21
- value: _T
22
- memory: float
23
-
24
-
25
- def memory_profiled(func: Callable[_P, _T], /) -> Callable[_P, Output[_T]]:
13
+ def memory_profiled[**P, T](func: Callable[P, T], /) -> Callable[P, Output[T]]:
26
14
  """Call a function, but also profile its maximum memory usage."""
27
15
 
28
16
  @wraps(func)
29
- def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> Output[_T]:
17
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> Output[T]:
30
18
  memory, value = memory_usage(
31
19
  cast("Any", (func, args, kwargs)), max_usage=True, retval=True
32
20
  )
@@ -35,4 +23,12 @@ def memory_profiled(func: Callable[_P, _T], /) -> Callable[_P, Output[_T]]:
35
23
  return wrapped
36
24
 
37
25
 
26
+ @dataclass(kw_only=True, slots=True)
27
+ class Output[T]:
28
+ """A function output, and its memory usage."""
29
+
30
+ value: T
31
+ memory: float
32
+
33
+
38
34
  __all__ = ["Output", "memory_profiled"]
@@ -1,16 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import builtins
3
+ from collections.abc import Callable, Hashable
4
4
  from dataclasses import dataclass
5
5
  from itertools import islice
6
6
  from textwrap import indent
7
7
  from typing import (
8
8
  TYPE_CHECKING,
9
9
  Any,
10
- Generic,
11
10
  Literal,
12
11
  TypeGuard,
13
- TypeVar,
14
12
  assert_never,
15
13
  cast,
16
14
  overload,
@@ -23,140 +21,213 @@ from more_itertools import peekable as _peekable
23
21
  from utilities.functions import get_class_name
24
22
  from utilities.iterables import OneNonUniqueError, one
25
23
  from utilities.reprlib import get_repr
26
- from utilities.sentinel import Sentinel, sentinel
27
- from utilities.types import THashable
24
+ from utilities.sentinel import Sentinel, is_sentinel, sentinel
28
25
 
29
26
  if TYPE_CHECKING:
30
- from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
31
-
32
-
33
- _T = TypeVar("_T")
34
- _U = TypeVar("_U")
35
-
36
-
37
- ##
27
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
38
28
 
39
29
 
40
30
  @overload
41
- def bucket_mapping(
42
- iterable: Iterable[_T],
43
- func: Callable[[_T], THashable],
31
+ def bucket_mapping[T, UH: Hashable](
32
+ iterable: Iterable[T],
33
+ func: Callable[[T], UH],
44
34
  /,
45
35
  *,
46
- transform: Callable[[_T], _U],
47
- list: bool = False,
48
- unique: Literal[True],
49
- ) -> Mapping[THashable, _U]: ...
36
+ pre: None = None,
37
+ post: None = None,
38
+ ) -> Mapping[UH, Iterator[T]]: ...
50
39
  @overload
51
- def bucket_mapping(
52
- iterable: Iterable[_T],
53
- func: Callable[[_T], THashable],
40
+ def bucket_mapping[T, UH: Hashable](
41
+ iterable: Iterable[T],
42
+ func: Callable[[T], UH],
54
43
  /,
55
44
  *,
56
- transform: Callable[[_T], _U] | None = None,
57
- list: bool = False,
58
- unique: Literal[True],
59
- ) -> Mapping[THashable, _T]: ...
45
+ pre: None = None,
46
+ post: Literal["list"],
47
+ ) -> Mapping[UH, list[T]]: ...
60
48
  @overload
61
- def bucket_mapping(
62
- iterable: Iterable[_T],
63
- func: Callable[[_T], THashable],
49
+ def bucket_mapping[T, UH: Hashable](
50
+ iterable: Iterable[T],
51
+ func: Callable[[T], UH],
64
52
  /,
65
53
  *,
66
- transform: Callable[[_T], _U],
67
- list: Literal[True],
68
- ) -> Mapping[THashable, Sequence[_U]]: ...
54
+ pre: None = None,
55
+ post: Literal["tuple"],
56
+ ) -> Mapping[UH, tuple[T, ...]]: ...
69
57
  @overload
70
- def bucket_mapping(
71
- iterable: Iterable[_T],
72
- func: Callable[[_T], THashable],
58
+ def bucket_mapping[T, UH: Hashable](
59
+ iterable: Iterable[T],
60
+ func: Callable[[T], UH],
73
61
  /,
74
62
  *,
75
- transform: Callable[[_T], _U],
76
- list: bool = False,
77
- ) -> Mapping[THashable, Iterator[_U]]: ...
63
+ pre: None = None,
64
+ post: Literal["set"],
65
+ ) -> Mapping[UH, set[T]]: ...
78
66
  @overload
79
- def bucket_mapping(
80
- iterable: Iterable[_T],
81
- func: Callable[[_T], THashable],
67
+ def bucket_mapping[T, UH: Hashable](
68
+ iterable: Iterable[T],
69
+ func: Callable[[T], UH],
82
70
  /,
83
71
  *,
84
- transform: Callable[[_T], _U] | None = None,
85
- list: Literal[True],
86
- ) -> Mapping[THashable, Sequence[_T]]: ...
72
+ pre: None = None,
73
+ post: Literal["frozenset"],
74
+ ) -> Mapping[UH, frozenset[T]]: ...
87
75
  @overload
88
- def bucket_mapping(
89
- iterable: Iterable[_T],
90
- func: Callable[[_T], THashable],
76
+ def bucket_mapping[T, UH: Hashable](
77
+ iterable: Iterable[T],
78
+ func: Callable[[T], UH],
91
79
  /,
92
80
  *,
93
- transform: Callable[[_T], _U] | None = None,
94
- list: bool = False,
95
- ) -> Mapping[THashable, Iterator[_T]]: ...
81
+ pre: None = None,
82
+ post: Literal["unique"],
83
+ ) -> Mapping[UH, T]: ...
96
84
  @overload
97
- def bucket_mapping(
98
- iterable: Iterable[_T],
99
- func: Callable[[_T], THashable],
85
+ def bucket_mapping[T, U, UH: Hashable](
86
+ iterable: Iterable[T],
87
+ func: Callable[[T], UH],
100
88
  /,
101
89
  *,
102
- transform: Callable[[_T], _U] | None = None,
103
- list: bool = False,
104
- unique: bool = False,
90
+ pre: Callable[[T], U] | None = None,
91
+ post: None = None,
92
+ ) -> Mapping[UH, Iterator[U]]: ...
93
+ @overload
94
+ def bucket_mapping[T, U, UH: Hashable](
95
+ iterable: Iterable[T],
96
+ func: Callable[[T], UH],
97
+ /,
98
+ *,
99
+ pre: Callable[[T], U],
100
+ post: Literal["list"],
101
+ ) -> Mapping[UH, list[U]]: ...
102
+ @overload
103
+ def bucket_mapping[T, U, UH: Hashable](
104
+ iterable: Iterable[T],
105
+ func: Callable[[T], UH],
106
+ /,
107
+ *,
108
+ pre: Callable[[T], U],
109
+ post: Literal["tuple"],
110
+ ) -> Mapping[UH, tuple[U, ...]]: ...
111
+ @overload
112
+ def bucket_mapping[T, U, UH: Hashable](
113
+ iterable: Iterable[T],
114
+ func: Callable[[T], UH],
115
+ /,
116
+ *,
117
+ pre: Callable[[T], U],
118
+ post: Literal["set"],
119
+ ) -> Mapping[UH, set[U]]: ...
120
+ @overload
121
+ def bucket_mapping[T, U, UH: Hashable](
122
+ iterable: Iterable[T],
123
+ func: Callable[[T], UH],
124
+ /,
125
+ *,
126
+ pre: Callable[[T], U],
127
+ post: Literal["frozenset"],
128
+ ) -> Mapping[UH, frozenset[U]]: ...
129
+ @overload
130
+ def bucket_mapping[T, U, UH: Hashable](
131
+ iterable: Iterable[T],
132
+ func: Callable[[T], UH],
133
+ /,
134
+ *,
135
+ pre: Callable[[T], U] | None = None,
136
+ post: Literal["unique"],
137
+ ) -> Mapping[UH, U]: ...
138
+ @overload
139
+ def bucket_mapping[T, U, UH: Hashable](
140
+ iterable: Iterable[T],
141
+ func: Callable[[T], UH],
142
+ /,
143
+ *,
144
+ pre: Callable[[T], U] | None = None,
145
+ post: Literal["list", "tuple", "set", "frozenset", "unique"] | None = None,
105
146
  ) -> (
106
- Mapping[THashable, Iterator[_T]]
107
- | Mapping[THashable, Iterator[_U]]
108
- | Mapping[THashable, Sequence[_T]]
109
- | Mapping[THashable, Sequence[_U]]
110
- | Mapping[THashable, _T]
111
- | Mapping[THashable, _U]
147
+ Mapping[UH, Iterator[T]]
148
+ | Mapping[UH, list[T]]
149
+ | Mapping[UH, tuple[T, ...]]
150
+ | Mapping[UH, set[T]]
151
+ | Mapping[UH, frozenset[T]]
152
+ | Mapping[UH, T]
153
+ | Mapping[UH, Iterator[U]]
154
+ | Mapping[UH, list[U]]
155
+ | Mapping[UH, tuple[U, ...]]
156
+ | Mapping[UH, set[U]]
157
+ | Mapping[UH, frozenset[U]]
158
+ | Mapping[UH, U]
112
159
  ): ...
113
- def bucket_mapping(
114
- iterable: Iterable[_T],
115
- func: Callable[[_T], THashable],
160
+ def bucket_mapping[T, U, UH: Hashable](
161
+ iterable: Iterable[T],
162
+ func: Callable[[T], UH],
116
163
  /,
117
164
  *,
118
- transform: Callable[[_T], _U] | None = None,
119
- list: bool = False, # noqa: A002
120
- unique: bool = False,
165
+ pre: Callable[[T], U] | None = None,
166
+ post: Literal["list", "tuple", "set", "frozenset", "unique"] | None = None,
121
167
  ) -> (
122
- Mapping[THashable, Iterator[_T]]
123
- | Mapping[THashable, Iterator[_U]]
124
- | Mapping[THashable, Sequence[_T]]
125
- | Mapping[THashable, Sequence[_U]]
126
- | Mapping[THashable, _T]
127
- | Mapping[THashable, _U]
168
+ Mapping[UH, Iterator[T]]
169
+ | Mapping[UH, list[T]]
170
+ | Mapping[UH, tuple[T, ...]]
171
+ | Mapping[UH, set[T]]
172
+ | Mapping[UH, frozenset[T]]
173
+ | Mapping[UH, T]
174
+ | Mapping[UH, Iterator[U]]
175
+ | Mapping[UH, list[U]]
176
+ | Mapping[UH, tuple[U, ...]]
177
+ | Mapping[UH, set[U]]
178
+ | Mapping[UH, frozenset[U]]
179
+ | Mapping[UH, U]
128
180
  ):
129
181
  """Bucket the values of iterable into a mapping."""
130
- b = bucket(iterable, func)
131
- mapping = {key: b[key] for key in b}
132
- match transform, list:
133
- case None, False:
134
- ...
135
- case None, True:
136
- mapping = {k: builtins.list(v) for k, v in mapping.items()}
137
- case _, False:
138
- mapping = {k: map(transform, v) for k, v in mapping.items()}
139
- case _, True:
140
- mapping = {k: builtins.list(map(transform, v)) for k, v in mapping.items()}
141
- case _ as never:
182
+ bckt = bucket(iterable, func)
183
+ mapping = {key: bckt[key] for key in bckt}
184
+ match pre, post:
185
+ case None, None:
186
+ return mapping
187
+ case None, "list":
188
+ return {k: list(v) for k, v in mapping.items()}
189
+ case None, "tuple":
190
+ return {k: tuple(v) for k, v in mapping.items()}
191
+ case None, "set":
192
+ return {k: set(v) for k, v in mapping.items()}
193
+ case None, "frozenset":
194
+ return {k: frozenset(v) for k, v in mapping.items()}
195
+ case None, "unique":
196
+ return _bucket_mapping_unique(mapping)
197
+ case Callable(), None:
198
+ return {k: map(pre, v) for k, v in mapping.items()}
199
+ case Callable(), "list":
200
+ return {k: list(map(pre, v)) for k, v in mapping.items()}
201
+ case Callable(), "tuple":
202
+ return {k: tuple(map(pre, v)) for k, v in mapping.items()}
203
+ case Callable(), "set":
204
+ return {k: set(map(pre, v)) for k, v in mapping.items()}
205
+ case Callable(), "frozenset":
206
+ return {k: frozenset(map(pre, v)) for k, v in mapping.items()}
207
+ case Callable(), "unique":
208
+ return _bucket_mapping_unique({k: map(pre, v) for k, v in mapping.items()})
209
+ case never:
142
210
  assert_never(never)
143
- if not unique:
144
- return mapping
145
- results = {}
146
- error_no_transform: dict[THashable, tuple[_T, _T]] = {}
211
+
212
+
213
+ def _bucket_mapping_unique[K: Hashable, V](
214
+ mapping: Mapping[K, Iterable[V]], /
215
+ ) -> Mapping[K, V]:
216
+ results: dict[K, V] = {}
217
+ errors: dict[K, tuple[V, V]] = {}
147
218
  for key, value in mapping.items():
148
219
  try:
149
220
  results[key] = one(value)
150
221
  except OneNonUniqueError as error:
151
- error_no_transform[key] = (error.first, error.second)
152
- if len(error_no_transform) >= 1:
153
- raise BucketMappingError(errors=error_no_transform)
222
+ errors[key] = (error.first, error.second)
223
+ if len(errors) >= 1:
224
+ raise BucketMappingError(errors=errors)
154
225
  return results
155
226
 
156
227
 
157
228
  @dataclass(kw_only=True, slots=True)
158
- class BucketMappingError(Exception, Generic[THashable, _U]):
159
- errors: Mapping[THashable, tuple[_U, _U]]
229
+ class BucketMappingError[K: Hashable, V](Exception):
230
+ errors: Mapping[K, tuple[V, V]]
160
231
 
161
232
  @override
162
233
  def __str__(self) -> str:
@@ -171,9 +242,9 @@ class BucketMappingError(Exception, Generic[THashable, _U]):
171
242
  ##
172
243
 
173
244
 
174
- def partition_list(
175
- pred: Callable[[_T], bool], iterable: Iterable[_T], /
176
- ) -> tuple[list[_T], list[_T]]:
245
+ def partition_list[T](
246
+ pred: Callable[[T], bool], iterable: Iterable[T], /
247
+ ) -> tuple[list[T], list[T]]:
177
248
  """Partition with lists."""
178
249
  false, true = partition(pred, iterable)
179
250
  return list(false), list(true)
@@ -182,48 +253,46 @@ def partition_list(
182
253
  ##
183
254
 
184
255
 
185
- def partition_typeguard(
186
- pred: Callable[[_T], TypeGuard[_U]], iterable: Iterable[_T], /
187
- ) -> tuple[Iterator[_T], Iterator[_U]]:
256
+ def partition_typeguard[T, U](
257
+ pred: Callable[[T], TypeGuard[U]], iterable: Iterable[T], /
258
+ ) -> tuple[Iterator[T], Iterator[U]]:
188
259
  """Partition with a typeguarded function."""
189
260
  false, true = partition(pred, iterable)
190
- true = cast("Iterator[_U]", true)
261
+ true = cast("Iterator[U]", true)
191
262
  return false, true
192
263
 
193
264
 
194
265
  ##
195
266
 
196
267
 
197
- class peekable(_peekable, Generic[_T]): # noqa: N801
268
+ class peekable[T](_peekable): # noqa: N801
198
269
  """Peekable which supports dropwhile/takewhile methods."""
199
270
 
200
- def __init__(self, iterable: Iterable[_T], /) -> None:
271
+ def __init__(self, iterable: Iterable[T], /) -> None:
201
272
  super().__init__(iterable)
202
273
 
203
274
  @override
204
- def __iter__(self) -> Iterator[_T]: # pyright: ignore[reportIncompatibleMethodOverride]
275
+ def __iter__(self) -> Iterator[T]: # pyright: ignore[reportIncompatibleMethodOverride]
205
276
  while bool(self):
206
277
  yield next(self)
207
278
 
208
279
  @override
209
- def __next__(self) -> _T:
280
+ def __next__(self) -> T:
210
281
  return super().__next__()
211
282
 
212
- def dropwhile(self, predicate: Callable[[_T], bool], /) -> None:
283
+ def dropwhile(self, predicate: Callable[[T], bool], /) -> None:
213
284
  while bool(self) and predicate(self.peek()):
214
285
  _ = next(self)
215
286
 
216
287
  @overload
217
- def peek(self, *, default: Sentinel = sentinel) -> _T: ...
288
+ def peek(self, *, default: Sentinel = sentinel) -> T: ...
218
289
  @overload
219
- def peek(self, *, default: _U) -> _T | _U: ...
290
+ def peek[U](self, *, default: U) -> T | U: ...
220
291
  @override
221
292
  def peek(self, *, default: Any = sentinel) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
222
- if isinstance(default, Sentinel):
223
- return super().peek()
224
- return super().peek(default=default)
293
+ return super().peek() if is_sentinel(default) else super().peek(default=default)
225
294
 
226
- def takewhile(self, predicate: Callable[[_T], bool], /) -> Iterator[_T]:
295
+ def takewhile(self, predicate: Callable[[T], bool], /) -> Iterator[T]:
227
296
  while bool(self) and predicate(self.peek()):
228
297
  yield next(self)
229
298
 
@@ -232,11 +301,11 @@ class peekable(_peekable, Generic[_T]): # noqa: N801
232
301
 
233
302
 
234
303
  @dataclass(kw_only=True, slots=True)
235
- class Split(Generic[_T]):
304
+ class Split[T]:
236
305
  """An iterable split into head/tail."""
237
306
 
238
- head: _T
239
- tail: _T
307
+ head: T
308
+ tail: T
240
309
 
241
310
  @override
242
311
  def __repr__(self) -> str:
@@ -250,15 +319,15 @@ class Split(Generic[_T]):
250
319
  return f"{cls}(\n{joined}\n)"
251
320
 
252
321
 
253
- def yield_splits(
254
- iterable: Iterable[_T],
322
+ def yield_splits[T](
323
+ iterable: Iterable[T],
255
324
  head: int,
256
325
  tail: int,
257
326
  /,
258
327
  *,
259
328
  min_frac: float | None = None,
260
329
  freq: int | None = None,
261
- ) -> Iterator[Split[Sequence[_T]]]:
330
+ ) -> Iterator[Split[Sequence[T]]]:
262
331
  """Yield the splits of an iterable."""
263
332
  it1 = _yield_splits1(iterable, head + tail)
264
333
  it2 = _yield_splits2(it1, head, tail, min_frac=min_frac)
@@ -267,9 +336,9 @@ def yield_splits(
267
336
  return islice(it3, 0, None, freq_use)
268
337
 
269
338
 
270
- def _yield_splits1(
271
- iterable: Iterable[_T], total: int, /
272
- ) -> Iterator[tuple[Literal["head", "body"], Sequence[_T]]]:
339
+ def _yield_splits1[T](
340
+ iterable: Iterable[T], total: int, /
341
+ ) -> Iterator[tuple[Literal["head", "body"], Sequence[T]]]:
273
342
  peek = peekable(iterable)
274
343
  for i in range(1, total + 1):
275
344
  if len(result := peek[:i]) < i:
@@ -283,14 +352,14 @@ def _yield_splits1(
283
352
  break
284
353
 
285
354
 
286
- def _yield_splits2(
287
- iterable: Iterable[tuple[Literal["head", "body"], Sequence[_T]],],
355
+ def _yield_splits2[T](
356
+ iterable: Iterable[tuple[Literal["head", "body"], Sequence[T]],],
288
357
  head: int,
289
358
  tail: int,
290
359
  /,
291
360
  *,
292
361
  min_frac: float | None = None,
293
- ) -> Iterator[tuple[Iterable[_T], int, int]]:
362
+ ) -> Iterator[tuple[Iterable[T], int, int]]:
294
363
  min_length = head if min_frac is None else min_frac * head
295
364
  for kind, window in iterable:
296
365
  len_win = len(window)
@@ -303,17 +372,17 @@ def _yield_splits2(
303
372
  len_tail = max(len_win - head, 0)
304
373
  if len_tail >= 1:
305
374
  yield window, head, len_tail
306
- case _ as never:
375
+ case never:
307
376
  assert_never(never)
308
377
 
309
378
 
310
- def _yield_splits3(
311
- iterable: Iterable[tuple[Iterable[_T], int, int]], /
312
- ) -> Iterator[Split[Sequence[_T]]]:
379
+ def _yield_splits3[T](
380
+ iterable: Iterable[tuple[Iterable[T], int, int]], /
381
+ ) -> Iterator[Split[Sequence[T]]]:
313
382
  for window, len_head, len_tail in iterable:
314
383
  head_win, tail_win = split_into(window, [len_head, len_tail])
315
384
  yield cast(
316
- "Split[Sequence[_T]]", Split(head=list(head_win), tail=list(tail_win))
385
+ "Split[Sequence[T]]", Split(head=list(head_win), tail=list(tail_win))
317
386
  )
318
387
 
319
388