kisstdlib 0.0.12__tar.gz → 0.0.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/PKG-INFO +4 -3
  2. kisstdlib-0.0.13/VERSION +1 -0
  3. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/__init__.py +5 -4
  4. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/argparse_ext.py +4 -7
  5. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/base.py +94 -106
  6. kisstdlib-0.0.12/kisstdlib/sorted.py → kisstdlib-0.0.13/kisstdlib/collections_ext.py +340 -268
  7. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/failure.py +37 -14
  8. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/fs.py +192 -132
  9. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/io/base.py +20 -8
  10. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/io/stdio.py +11 -5
  11. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/itertools_ext.py +38 -44
  12. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/logging_ext.py +7 -3
  13. kisstdlib-0.0.13/kisstdlib/parsing.py +641 -0
  14. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/pyrepr.py +1 -2
  15. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/sqlite3_ext.py +96 -16
  16. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/string_ext.py +58 -37
  17. kisstdlib-0.0.13/kisstdlib/time.py +1252 -0
  18. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib.egg-info/PKG-INFO +4 -3
  19. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib.egg-info/SOURCES.txt +3 -2
  20. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/pyproject.toml +5 -3
  21. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/test/test_basics.py +21 -4
  22. kisstdlib-0.0.13/test/test_collections.py +200 -0
  23. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/test/test_fs.py +9 -9
  24. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/test/test_parsing.py +78 -0
  25. kisstdlib-0.0.13/test/test_time.py +1000 -0
  26. kisstdlib-0.0.12/kisstdlib/parsing.py +0 -316
  27. kisstdlib-0.0.12/kisstdlib/time.py +0 -299
  28. kisstdlib-0.0.12/test/test_sorted.py +0 -153
  29. kisstdlib-0.0.12/test/test_time.py +0 -154
  30. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/LICENSE.txt +0 -0
  31. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/README.md +0 -0
  32. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/compression.py +0 -0
  33. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/getpass_ext.py +0 -0
  34. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/io/__init__.py +0 -0
  35. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/io/adapter.py +0 -0
  36. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/io/encoder.py +0 -0
  37. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/io/wrapper.py +0 -0
  38. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/py.typed +0 -0
  39. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/signal_ext.py +0 -0
  40. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib/tariter.py +0 -0
  41. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib.egg-info/dependency_links.txt +0 -0
  42. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib.egg-info/entry_points.txt +0 -0
  43. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib.egg-info/requires.txt +0 -0
  44. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib.egg-info/top_level.txt +0 -0
  45. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/kisstdlib_bin/describe_forest.py +0 -0
  46. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/setup.cfg +0 -0
  47. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/setup.py +0 -0
  48. {kisstdlib-0.0.12 → kisstdlib-0.0.13}/test/test_io.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: kisstdlib
3
- Version: 0.0.12
3
+ Version: 0.0.13
4
4
  Summary: System programming with conceptually and algebraically simple primitives.
5
5
  Author-email: Jan Malakhovski <oxij@oxij.org>
6
6
  Project-URL: Homepage, https://oxij.org/software/kisstdlib/
@@ -12,10 +12,11 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: License :: OSI Approved :: Python Software Foundation License
14
14
  Classifier: License :: OSI Approved :: BSD License
15
- Requires-Python: >=3.11
15
+ Requires-Python: >=3.13
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE.txt
18
18
  Requires-Dist: sortedcontainers==2.4.*
19
+ Dynamic: license-file
19
20
 
20
21
  # Table of Contents
21
22
  <details><summary>(Click me to see it.)</summary>
@@ -0,0 +1 @@
1
+ 0.0.13
@@ -32,6 +32,7 @@ from .base import *
32
32
  from .failure import *
33
33
  from .string_ext import *
34
34
  from .itertools_ext import *
35
+ from .collections_ext import *
35
36
  from .io.stdio import *
36
37
  from .logging_ext import *
37
38
  from .signal_ext import *
@@ -63,12 +64,12 @@ def setup_kisstdlib(
63
64
  return res
64
65
 
65
66
 
66
- def run_kisstdlib_main(
67
+ def run_kisstdlib_main[**PS](
67
68
  setup_logging_result: tuple[LogCounter, ANSILogHandler],
68
- func: _t.Callable[AParamSpec, None],
69
+ func: _t.Callable[PS, None],
69
70
  /,
70
- *args: AParamSpec.args,
71
- **kwargs: AParamSpec.kwargs,
71
+ *args: PS.args,
72
+ **kwargs: PS.kwargs,
72
73
  ) -> None:
73
74
  """Run given function with given arguments under `with no_signals()` block while
74
75
  properly handling all exceptions in `ExceptionGroup`s,
@@ -343,15 +343,12 @@ class BetterArgumentParser(ArgumentParser):
343
343
  return res
344
344
 
345
345
 
346
- _AParamSpec = _t.ParamSpec("_AParamSpec")
347
-
348
-
349
- def make_argparser_and_run(
346
+ def make_argparser_and_run[**PS](
350
347
  make_argparser: _t.Callable[[bool], BetterArgumentParser],
351
- func: _t.Callable[_t.Concatenate[Namespace, _AParamSpec], None],
348
+ func: _t.Callable[_t.Concatenate[Namespace, PS], None],
352
349
  /,
353
- *args: _AParamSpec.args,
354
- **kwargs: _AParamSpec.kwargs,
350
+ *args: PS.args,
351
+ **kwargs: PS.kwargs,
355
352
  ) -> None:
356
353
  """Given `make_argparser` function that produces "real" and "fake"
357
354
  `BetterArgumentParser`s, run `parse_args` with on real one, but then, if
@@ -35,23 +35,9 @@ from functools import partial, partialmethod # pylint: disable=unused-import
35
35
 
36
36
  ### Primitive types
37
37
 
38
- # Placeholders
39
- AType = _t.TypeVar("AType")
40
- BType = _t.TypeVar("BType")
41
- CType = _t.TypeVar("CType")
42
- DType = _t.TypeVar("DType")
43
- EType = _t.TypeVar("EType")
44
- AParamSpec = _t.ParamSpec("AParamSpec")
45
- BParamSpec = _t.ParamSpec("BParamSpec")
46
-
47
38
  # Bytes-like object
48
39
  BytesLike = bytes | bytearray | memoryview
49
40
 
50
- # Numbers
51
- Number = Decimal | float | int
52
- AnyNumber = _t.TypeVar("AnyNumber", bound=Number)
53
-
54
-
55
41
  ### Common constants
56
42
 
57
43
  # OS detection
@@ -60,16 +46,23 @@ WINDOWS = _os.name == "nt"
60
46
 
61
47
  # Time lengths, in seconds
62
48
  MINUTE = 60
63
- HOUR = 60 * MINUTE
64
- DAY = 24 * HOUR
65
- AVG_MONTH = 30 * DAY
66
- AVG_YEAR = 365 * DAY
49
+ HOUR = 3600
50
+ DAY = 86400
51
+ WEEK = 604800
52
+
53
+ # Common metric prefixes
54
+ MILLI = Decimal((0, (1,), -3))
55
+ MICRO = Decimal((0, (1,), -6))
56
+ NANO = Decimal((0, (1,), -9))
67
57
 
68
58
  # Common byte sizes
69
- KiB = 1024
70
- MiB = 1024 * KiB
71
- GiB = 1024 * MiB
59
+ KB = 1000
60
+ MB = 1000000
61
+ GB = 1000000000
72
62
 
63
+ KiB = 1024
64
+ MiB = 1048576
65
+ GiB = 1073741824
73
66
 
74
67
  ### Placeholder values
75
68
 
@@ -104,8 +97,8 @@ MISSING = Missing()
104
97
  ### Structured unions
105
98
 
106
99
 
107
- class Maybe(_t.Generic[AType]):
108
- """Similar to `Optional[AType]`, i.e. `AType | None`, but allows
100
+ class Maybe[T]:
101
+ """Similar to `Optional[T]`, i.e. `T | None`, but allows
109
102
  arbitrary nesting.
110
103
 
111
104
  E.g. `Maybe[Maybe[int]]` does not get flattened into `Maybe[int]`.
@@ -113,117 +106,119 @@ class Maybe(_t.Generic[AType]):
113
106
 
114
107
 
115
108
  @_dc.dataclass(order=True)
116
- class Nothing(Maybe[AType], _t.Generic[AType]):
109
+ class Nothing[T](Maybe[T]):
117
110
  pass
118
111
 
119
112
 
120
113
  @_dc.dataclass(order=True, slots=True)
121
- class Just(Maybe[AType], _t.Generic[AType]):
122
- just: AType
114
+ class Just[T](Maybe[T]):
115
+ just: T
123
116
 
124
117
 
125
- class Either(_t.Generic[AType, BType]):
126
- """Either a value of `AType` or a value `BType`, without flattening.
118
+ class Either[A, B]():
119
+ """Either a value of type `A` or a value of type `B`, without flattening.
127
120
 
128
121
  E.g., `Either[int, int]` is not just `int`.
129
122
  """
130
123
 
131
124
 
132
125
  @_dc.dataclass(order=True, slots=True)
133
- class Left(Either[AType, BType], _t.Generic[AType, BType]):
134
- left: AType
126
+ class Left[A, B](Either[A, B]):
127
+ left: A
135
128
 
136
129
 
137
130
  @_dc.dataclass(order=True, slots=True)
138
- class Right(Either[AType, BType], _t.Generic[AType, BType]):
139
- right: BType
131
+ class Right[A, B](Either[A, B]):
132
+ right: B
140
133
 
141
134
 
142
135
  ### Operations on Numbers
143
136
 
144
137
 
145
- def clamp(minv: AnyNumber, maxv: AnyNumber, v: AnyNumber) -> AnyNumber:
146
- return min(maxv, max(minv, v))
138
+ class SupportsCmp(_t.Protocol):
139
+ # `__eq__` and `__ne__` belong to `object` and are always available
140
+ def __lt__(self, x: int | _t.Self) -> bool: ...
141
+ def __le__(self, x: int | _t.Self) -> bool: ...
142
+ def __gt__(self, x: int | _t.Self) -> bool: ...
143
+ def __ge__(self, x: int | _t.Self) -> bool: ...
147
144
 
148
145
 
149
- def is_infinite(x: Number) -> bool:
150
- return hasattr(x, "is_infinite") and x.is_infinite() or isinstance(x, float) and _math.isinf(x)
146
+ class SupportsCounting(SupportsCmp, _t.Protocol):
147
+ def __add__(self, x: int | _t.Self) -> _t.Self: ...
148
+ def __sub__(self, x: int | _t.Self) -> _t.Self: ...
151
149
 
150
+ # because this can be implemented using the above
151
+ # note that from the result, only `SupportsCmp` is required
152
+ def __abs__(self) -> SupportsCmp: ...
152
153
 
153
- def add_(x: Number, y: Number) -> Number:
154
- """`x + y` that always works."""
155
- if isinstance(x, float) and isinstance(y, Decimal):
156
- return Decimal(x) + y
157
- if isinstance(x, Decimal) and isinstance(y, float):
158
- return x + Decimal(y)
159
- return x + y # type: ignore
160
154
 
155
+ def clamp[A: (int, float, Decimal)](minv: A, maxv: A, v: A) -> A:
156
+ return min(maxv, max(minv, v))
161
157
 
162
- def sub_(x: Number, y: Number) -> Number:
163
- """`x - y` that always works."""
164
- if isinstance(x, float) and isinstance(y, Decimal):
165
- return Decimal(x) - y
166
- if isinstance(x, Decimal) and isinstance(y, float):
167
- return x - Decimal(y)
168
- return x - y # type: ignore
169
158
 
159
+ def is_infinite(x: _t.Any) -> bool:
160
+ return isinstance(x, float) and _math.isinf(x) or hasattr(x, "is_infinite") and x.is_infinite()
170
161
 
171
- def mul_(x: Number, y: Number) -> Number:
172
- """`x * y` that always works."""
173
- if isinstance(x, float) and isinstance(y, Decimal):
174
- return Decimal(x) * y
175
- if isinstance(x, Decimal) and isinstance(y, float):
176
- return x * Decimal(y)
177
- return x * y # type: ignore
178
162
 
163
+ def is_closer[A: SupportsCounting](ideal: A, a: A, b: A) -> bool:
164
+ """Check whether `a` is closer to `ideal` than `b` is. That is, essentially, `abs(ideal - a) <
165
+ abs(ideal - b)`, but with infinite `ideal` cases handled properly.
179
166
 
180
- def truediv_(x: Number, y: Number) -> Number:
181
- """`x / y` that always works."""
182
- if isinstance(x, float) and isinstance(y, Decimal):
183
- return Decimal(x) / y
184
- if isinstance(x, Decimal) and isinstance(y, float):
185
- return x / Decimal(y)
186
- return x / y # type: ignore
167
+ Note that the above implies that this function always returns `False` when `a == b`.
168
+ """
169
+ if is_infinite(ideal):
170
+ if ideal < 0:
171
+ return a < b
172
+ return b < a
173
+ return abs(ideal - a) < abs(ideal - b)
187
174
 
188
175
 
189
176
  ### Simple functions
190
177
 
191
178
 
192
- def identity(x: AType, /) -> AType:
179
+ def noop() -> None:
180
+ """A noop."""
181
+
182
+
183
+ def noop_(*_args: _t.Any, **_kwargs: _t.Any) -> None:
184
+ """A noop, with a bunch of ignored arguments."""
185
+
186
+
187
+ def identity[T](x: T, /) -> T:
193
188
  """Identity function."""
194
189
  return x
195
190
 
196
191
 
197
- def identity_(x: AType, /, *_args: _t.Any, **_kwargs: _t.Any) -> AType:
192
+ def identity_[T](x: T, /, *_args: _t.Any, **_kwargs: _t.Any) -> T:
198
193
  """Identity function, with a bunch of ignored arguments."""
199
194
  return x
200
195
 
201
196
 
202
- def const(x: AType, /) -> _t.Callable[[BType], AType]:
197
+ def const[A, B](x: A, /) -> _t.Callable[[B], A]:
203
198
  """Return a function that always returns `a`."""
204
199
  return lambda y: x
205
200
 
206
201
 
207
- def const_(x: AType, /) -> _t.Callable[_t.Concatenate[BType, BParamSpec], AType]:
202
+ def const_[A, B, **PS](x: A, /) -> _t.Callable[_t.Concatenate[B, PS], A]:
208
203
  """Return a function that always returns `a`, and has a bunch of ignored arguments."""
209
204
 
210
- def sub(_y: BType, /, *_args: BParamSpec.args, **_kwargs: BParamSpec.kwargs) -> AType:
205
+ def sub(_y: B, /, *_args: PS.args, **_kwargs: PS.kwargs) -> A:
211
206
  return x
212
207
 
213
208
  return sub
214
209
 
215
210
 
216
- def fst(x: tuple[AType, BType]) -> AType:
211
+ def fst[A, B](x: tuple[A, B]) -> A:
217
212
  """Get the first element of a given 2-elemnt tuple."""
218
213
  return x[0]
219
214
 
220
215
 
221
- def snd(x: tuple[AType, BType]) -> BType:
216
+ def snd[A, B](x: tuple[A, B]) -> B:
222
217
  """Get the second element of a given 2-elemnt tuple."""
223
218
  return x[1]
224
219
 
225
220
 
226
- def first_def(*args: AType | None) -> AType:
221
+ def first_def[A](*args: A | None) -> A:
227
222
  """Return first argument that is not `None`."""
228
223
  for e in args:
229
224
  if e is not None:
@@ -231,7 +226,7 @@ def first_def(*args: AType | None) -> AType:
231
226
  raise IndexError("no matching elements")
232
227
 
233
228
 
234
- def first_ok(*args: AType) -> AType:
229
+ def first_ok[A](*args: A) -> A:
235
230
  """Return first `True`-like argument."""
236
231
  for e in args:
237
232
  if e:
@@ -239,19 +234,17 @@ def first_ok(*args: AType) -> AType:
239
234
  raise IndexError("no matching elements")
240
235
 
241
236
 
242
- def optional(b: BType, f: _t.Callable[[AType], BType], x: AType | None) -> BType:
237
+ def optional[A, B](b: B, f: _t.Callable[[A], B], x: A | None) -> B:
243
238
  """`f(x) if x is not None else b`."""
244
239
  return f(x) if x is not None else b
245
240
 
246
241
 
247
- def maybe(b: BType, f: _t.Callable[[AType], BType], x: Maybe[AType]) -> BType:
242
+ def maybe[A, B](b: B, f: _t.Callable[[A], B], x: Maybe[A]) -> B:
248
243
  """`f(x.just) if x is Just else b`."""
249
244
  return f(x.just) if isinstance(x, Just) else b
250
245
 
251
246
 
252
- def either(
253
- fa: _t.Callable[[AType], CType], fb: _t.Callable[[BType], CType], x: Either[AType, BType]
254
- ) -> CType:
247
+ def either[A, B, C](fa: _t.Callable[[A], C], fb: _t.Callable[[B], C], x: Either[A, B]) -> C:
255
248
  """`fa(x.left) if x is Left`, `fb(x.right) if x is Right`, `AssertionError` otherwise."""
256
249
  if isinstance(x, Left):
257
250
  return fa(x.left)
@@ -263,22 +256,22 @@ def either(
263
256
  ### Injectors into `list`
264
257
 
265
258
 
266
- def singleton(x: AType) -> list[AType]:
259
+ def singleton[A](x: A) -> list[A]:
267
260
  """`[x]`."""
268
261
  return [x]
269
262
 
270
263
 
271
- def optlist(p: bool, x: AType) -> list[AType]:
264
+ def optlist[A](p: bool, x: A) -> list[A]:
272
265
  """`[x] if p else []`."""
273
266
  return [x] if p else []
274
267
 
275
268
 
276
- def optslist(p: bool, xs: list[AType]) -> list[AType]:
269
+ def optslist[A](p: bool, xs: list[A]) -> list[A]:
277
270
  """`xs if p else []`."""
278
271
  return xs if p else []
279
272
 
280
273
 
281
- def optional2list(x: AType | None) -> list[AType]:
274
+ def optional2list[A](x: A | None) -> list[A]:
282
275
  """`[x] if x is not None else []`."""
283
276
  return [x] if x is not None else []
284
277
 
@@ -286,48 +279,46 @@ def optional2list(x: AType | None) -> list[AType]:
286
279
  ### Functors, i.e. applying functions to elements contained in structures
287
280
 
288
281
 
289
- def map_optional(f: _t.Callable[[AType], BType], x: AType | None) -> BType | None:
282
+ def map_optional[A, B](f: _t.Callable[[A], B], x: A | None) -> B | None:
290
283
  """`f(x) if x is not None else x`."""
291
284
  return f(x) if x is not None else x
292
285
 
293
286
 
294
- def map_optional2list(f: _t.Callable[[AType], list[BType]], x: AType | None) -> list[BType]:
287
+ def map_optional2list[A, B](f: _t.Callable[[A], list[B]], x: A | None) -> list[B]:
295
288
  """`f(x) if x is not None else []`."""
296
289
  return f(x) if x is not None else []
297
290
 
298
291
 
299
- def map_maybe(f: _t.Callable[[AType], BType], x: Maybe[AType]) -> Maybe[BType]:
292
+ def map_maybe[A, B](f: _t.Callable[[A], B], x: Maybe[A]) -> Maybe[B]:
300
293
  """`Just(f(x.just)) if x is Just else x`."""
301
- return Just(f(x.just)) if isinstance(x, Just) else _t.cast(Maybe[BType], x)
294
+ return Just(f(x.just)) if isinstance(x, Just) else _t.cast(Maybe[B], x)
302
295
 
303
296
 
304
- def map_maybe2list(f: _t.Callable[[AType], list[BType]], x: Maybe[AType]) -> list[BType]:
297
+ def map_maybe2list[A, B](f: _t.Callable[[A], list[B]], x: Maybe[A]) -> list[B]:
305
298
  """`f(x.just) if x is Just else []`."""
306
299
  return f(x.just) if isinstance(x, Just) else []
307
300
 
308
301
 
309
- def map_left(f: _t.Callable[[AType], CType], x: Either[AType, BType]) -> Either[CType, BType]:
302
+ def map_left[A, B, C](f: _t.Callable[[A], C], x: Either[A, B]) -> Either[C, B]:
310
303
  """`Right(f(x.left)) if x is Left else x`."""
311
- return Left(f(x.left)) if isinstance(x, Left) else _t.cast(Either[CType, BType], x)
304
+ return Left(f(x.left)) if isinstance(x, Left) else _t.cast(Either[C, B], x)
312
305
 
313
306
 
314
- def map_right(f: _t.Callable[[BType], CType], x: Either[AType, BType]) -> Either[AType, CType]:
307
+ def map_right[A, B, C](f: _t.Callable[[B], C], x: Either[A, B]) -> Either[A, C]:
315
308
  """`Right(f(x.right)) if x is Right else x`."""
316
- return Right(f(x.right)) if isinstance(x, Right) else _t.cast(Either[AType, CType], x)
309
+ return Right(f(x.right)) if isinstance(x, Right) else _t.cast(Either[A, C], x)
317
310
 
318
311
 
319
312
  ### Function composition
320
313
 
321
314
 
322
- def compose(
323
- f: _t.Callable[[BType], CType], g: _t.Callable[[AType], BType]
324
- ) -> _t.Callable[[AType], CType]:
315
+ def compose[A, B, C](f: _t.Callable[[B], C], g: _t.Callable[[A], B]) -> _t.Callable[[A], C]:
325
316
  return lambda x: f(g(x))
326
317
 
327
318
 
328
- def compose_pipe(
329
- pipe: list[_t.Callable[_t.Concatenate[AType, AParamSpec], AType]], /
330
- ) -> _t.Callable[_t.Concatenate[AType, AParamSpec], AType]:
319
+ def compose_pipe[A, **PS](
320
+ pipe: list[_t.Callable[_t.Concatenate[A, PS], A]], /
321
+ ) -> _t.Callable[_t.Concatenate[A, PS], A]:
331
322
  """Compose a list of functions, similarly to how a shell pipe ("|")
332
323
  exression does it. I.e. compose them in reverse order.
333
324
 
@@ -342,7 +333,7 @@ def compose_pipe(
342
333
  if len(pipe) == 1:
343
334
  return pipe[0]
344
335
 
345
- def sub(x: AType, /, *args: AParamSpec.args, **kwargs: AParamSpec.kwargs) -> AType:
336
+ def sub(x: A, /, *args: PS.args, **kwargs: PS.kwargs) -> A:
346
337
  for func in pipe:
347
338
  x = func(x, *args, **kwargs)
348
339
  return x
@@ -374,17 +365,14 @@ def get_traceback(exc: BaseException, /, lines: int = 128) -> str:
374
365
  return fobj.getvalue()
375
366
 
376
367
 
377
- _BaseExeceptionType = _t.TypeVar("_BaseExeceptionType", bound=BaseException)
378
-
379
-
380
- def flat_exceptions(
381
- es: BaseExceptionGroup[_BaseExeceptionType], /
382
- ) -> _t.Iterator[_BaseExeceptionType]:
368
+ def flat_exceptions[ExceptionType: BaseException](
369
+ es: BaseExceptionGroup[ExceptionType], /
370
+ ) -> _t.Iterator[ExceptionType]:
383
371
  """For simplifying some `try ... except*` expressions."""
384
372
 
385
373
  def sub(
386
- excs: _BaseExeceptionType | BaseExceptionGroup[_BaseExeceptionType],
387
- ) -> _t.Iterator[_BaseExeceptionType]:
374
+ excs: ExceptionType | BaseExceptionGroup[ExceptionType],
375
+ ) -> _t.Iterator[ExceptionType]:
388
376
  if isinstance(excs, BaseExceptionGroup):
389
377
  for e in excs.exceptions:
390
378
  yield from sub(e)