kisstdlib 0.0.12__tar.gz → 0.0.14__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 (50) hide show
  1. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/PKG-INFO +4 -3
  2. kisstdlib-0.0.14/VERSION +1 -0
  3. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/__init__.py +5 -4
  4. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/argparse_ext.py +4 -7
  5. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/base.py +126 -119
  6. kisstdlib-0.0.12/kisstdlib/sorted.py → kisstdlib-0.0.14/kisstdlib/collections_ext.py +340 -268
  7. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/failure.py +37 -14
  8. kisstdlib-0.0.14/kisstdlib/fs.py +1486 -0
  9. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/io/base.py +20 -8
  10. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/io/stdio.py +11 -5
  11. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/itertools_ext.py +38 -44
  12. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/logging_ext.py +25 -4
  13. kisstdlib-0.0.14/kisstdlib/parsing.py +641 -0
  14. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/pyrepr.py +1 -2
  15. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/sqlite3_ext.py +96 -16
  16. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/string_ext.py +58 -37
  17. kisstdlib-0.0.14/kisstdlib/time.py +1268 -0
  18. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib.egg-info/PKG-INFO +4 -3
  19. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib.egg-info/SOURCES.txt +3 -2
  20. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/pyproject.toml +5 -3
  21. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/test/test_basics.py +21 -4
  22. kisstdlib-0.0.14/test/test_collections.py +200 -0
  23. kisstdlib-0.0.14/test/test_fs.py +772 -0
  24. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/test/test_parsing.py +78 -0
  25. kisstdlib-0.0.14/test/test_time.py +1079 -0
  26. kisstdlib-0.0.12/kisstdlib/fs.py +0 -1115
  27. kisstdlib-0.0.12/kisstdlib/parsing.py +0 -316
  28. kisstdlib-0.0.12/kisstdlib/time.py +0 -299
  29. kisstdlib-0.0.12/test/test_fs.py +0 -343
  30. kisstdlib-0.0.12/test/test_sorted.py +0 -153
  31. kisstdlib-0.0.12/test/test_time.py +0 -154
  32. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/LICENSE.txt +0 -0
  33. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/README.md +0 -0
  34. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/compression.py +0 -0
  35. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/getpass_ext.py +0 -0
  36. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/io/__init__.py +0 -0
  37. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/io/adapter.py +0 -0
  38. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/io/encoder.py +0 -0
  39. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/io/wrapper.py +0 -0
  40. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/py.typed +0 -0
  41. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/signal_ext.py +0 -0
  42. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib/tariter.py +0 -0
  43. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib.egg-info/dependency_links.txt +0 -0
  44. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib.egg-info/entry_points.txt +0 -0
  45. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib.egg-info/requires.txt +0 -0
  46. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib.egg-info/top_level.txt +0 -0
  47. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/kisstdlib_bin/describe_forest.py +0 -0
  48. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/setup.cfg +0 -0
  49. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/setup.py +0 -0
  50. {kisstdlib-0.0.12 → kisstdlib-0.0.14}/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.14
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.14
@@ -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,118 @@ 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:
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: ...
144
+
145
+
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: ...
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: ...
153
+
154
+
155
+ def clamp[A: (int, float, Decimal)](minv: A, maxv: A, v: A) -> A:
146
156
  return min(maxv, max(minv, v))
147
157
 
148
158
 
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)
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()
151
161
 
152
162
 
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
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.
166
+
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)
160
174
 
161
175
 
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
176
+ ### Exception handling hooks
169
177
 
170
178
 
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
179
+ def raise_one(exc: Exception, /, *_args: _t.Any, **_kwargs: _t.Any) -> _t.NoReturn:
180
+ """Raise the given `Exception`."""
181
+ raise exc
178
182
 
179
183
 
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
184
+ def raise_all(excs: list[Exception], /, *_args: _t.Any, **_kwargs: _t.Any) -> _t.NoReturn:
185
+ """Raise give exceptions as a trivial `ExceptionGroup`."""
186
+ raise ExceptionGroup("multiple failures", excs)
187
187
 
188
188
 
189
189
  ### Simple functions
190
190
 
191
191
 
192
- def identity(x: AType, /) -> AType:
193
- """Identity function."""
194
- return x
192
+ def noop(*_args: _t.Any, **_kwargs: _t.Any) -> None:
193
+ """A noop, with a bunch of ignored arguments."""
195
194
 
196
195
 
197
- def identity_(x: AType, /, *_args: _t.Any, **_kwargs: _t.Any) -> AType:
196
+ def identity[T](x: T, /, *_args: _t.Any, **_kwargs: _t.Any) -> T:
198
197
  """Identity function, with a bunch of ignored arguments."""
199
198
  return x
200
199
 
201
200
 
202
- def const(x: AType, /) -> _t.Callable[[BType], AType]:
203
- """Return a function that always returns `a`."""
204
- return lambda y: x
201
+ def const[A, B, **PS](x: A, /) -> _t.Callable[_t.Concatenate[B, PS], A]:
202
+ """Return a function that always returns `a`, with a bunch of ignored arguments."""
205
203
 
206
-
207
- def const_(x: AType, /) -> _t.Callable[_t.Concatenate[BType, BParamSpec], AType]:
208
- """Return a function that always returns `a`, and has a bunch of ignored arguments."""
209
-
210
- def sub(_y: BType, /, *_args: BParamSpec.args, **_kwargs: BParamSpec.kwargs) -> AType:
204
+ def sub(_y: B, /, *_args: PS.args, **_kwargs: PS.kwargs) -> A:
211
205
  return x
212
206
 
213
207
  return sub
214
208
 
215
209
 
216
- def fst(x: tuple[AType, BType]) -> AType:
210
+ def fst[A, B](x: tuple[A, B]) -> A:
217
211
  """Get the first element of a given 2-elemnt tuple."""
218
212
  return x[0]
219
213
 
220
214
 
221
- def snd(x: tuple[AType, BType]) -> BType:
215
+ def snd[A, B](x: tuple[A, B]) -> B:
222
216
  """Get the second element of a given 2-elemnt tuple."""
223
217
  return x[1]
224
218
 
225
219
 
226
- def first_def(*args: AType | None) -> AType:
220
+ def first_def[A](*args: A | None) -> A:
227
221
  """Return first argument that is not `None`."""
228
222
  for e in args:
229
223
  if e is not None:
@@ -231,7 +225,7 @@ def first_def(*args: AType | None) -> AType:
231
225
  raise IndexError("no matching elements")
232
226
 
233
227
 
234
- def first_ok(*args: AType) -> AType:
228
+ def first_ok[A](*args: A) -> A:
235
229
  """Return first `True`-like argument."""
236
230
  for e in args:
237
231
  if e:
@@ -239,19 +233,17 @@ def first_ok(*args: AType) -> AType:
239
233
  raise IndexError("no matching elements")
240
234
 
241
235
 
242
- def optional(b: BType, f: _t.Callable[[AType], BType], x: AType | None) -> BType:
236
+ def optional[A, B](b: B, f: _t.Callable[[A], B], x: A | None) -> B:
243
237
  """`f(x) if x is not None else b`."""
244
238
  return f(x) if x is not None else b
245
239
 
246
240
 
247
- def maybe(b: BType, f: _t.Callable[[AType], BType], x: Maybe[AType]) -> BType:
241
+ def maybe[A, B](b: B, f: _t.Callable[[A], B], x: Maybe[A]) -> B:
248
242
  """`f(x.just) if x is Just else b`."""
249
243
  return f(x.just) if isinstance(x, Just) else b
250
244
 
251
245
 
252
- def either(
253
- fa: _t.Callable[[AType], CType], fb: _t.Callable[[BType], CType], x: Either[AType, BType]
254
- ) -> CType:
246
+ def either[A, B, C](fa: _t.Callable[[A], C], fb: _t.Callable[[B], C], x: Either[A, B]) -> C:
255
247
  """`fa(x.left) if x is Left`, `fb(x.right) if x is Right`, `AssertionError` otherwise."""
256
248
  if isinstance(x, Left):
257
249
  return fa(x.left)
@@ -263,22 +255,22 @@ def either(
263
255
  ### Injectors into `list`
264
256
 
265
257
 
266
- def singleton(x: AType) -> list[AType]:
258
+ def singleton[A](x: A) -> list[A]:
267
259
  """`[x]`."""
268
260
  return [x]
269
261
 
270
262
 
271
- def optlist(p: bool, x: AType) -> list[AType]:
263
+ def optlist[A](p: bool, x: A) -> list[A]:
272
264
  """`[x] if p else []`."""
273
265
  return [x] if p else []
274
266
 
275
267
 
276
- def optslist(p: bool, xs: list[AType]) -> list[AType]:
268
+ def optslist[A](p: bool, xs: list[A]) -> list[A]:
277
269
  """`xs if p else []`."""
278
270
  return xs if p else []
279
271
 
280
272
 
281
- def optional2list(x: AType | None) -> list[AType]:
273
+ def optional2list[A](x: A | None) -> list[A]:
282
274
  """`[x] if x is not None else []`."""
283
275
  return [x] if x is not None else []
284
276
 
@@ -286,63 +278,81 @@ def optional2list(x: AType | None) -> list[AType]:
286
278
  ### Functors, i.e. applying functions to elements contained in structures
287
279
 
288
280
 
289
- def map_optional(f: _t.Callable[[AType], BType], x: AType | None) -> BType | None:
281
+ def map_optional[A, B](f: _t.Callable[[A], B], x: A | None) -> B | None:
290
282
  """`f(x) if x is not None else x`."""
291
283
  return f(x) if x is not None else x
292
284
 
293
285
 
294
- def map_optional2list(f: _t.Callable[[AType], list[BType]], x: AType | None) -> list[BType]:
286
+ def map_optional2list[A, B](f: _t.Callable[[A], list[B]], x: A | None) -> list[B]:
295
287
  """`f(x) if x is not None else []`."""
296
288
  return f(x) if x is not None else []
297
289
 
298
290
 
299
- def map_maybe(f: _t.Callable[[AType], BType], x: Maybe[AType]) -> Maybe[BType]:
291
+ def map_maybe[A, B](f: _t.Callable[[A], B], x: Maybe[A]) -> Maybe[B]:
300
292
  """`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)
293
+ return Just(f(x.just)) if isinstance(x, Just) else _t.cast(Maybe[B], x)
302
294
 
303
295
 
304
- def map_maybe2list(f: _t.Callable[[AType], list[BType]], x: Maybe[AType]) -> list[BType]:
296
+ def map_maybe2list[A, B](f: _t.Callable[[A], list[B]], x: Maybe[A]) -> list[B]:
305
297
  """`f(x.just) if x is Just else []`."""
306
298
  return f(x.just) if isinstance(x, Just) else []
307
299
 
308
300
 
309
- def map_left(f: _t.Callable[[AType], CType], x: Either[AType, BType]) -> Either[CType, BType]:
301
+ def map_left[A, B, C](f: _t.Callable[[A], C], x: Either[A, B]) -> Either[C, B]:
310
302
  """`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)
303
+ return Left(f(x.left)) if isinstance(x, Left) else _t.cast(Either[C, B], x)
312
304
 
313
305
 
314
- def map_right(f: _t.Callable[[BType], CType], x: Either[AType, BType]) -> Either[AType, CType]:
306
+ def map_right[A, B, C](f: _t.Callable[[B], C], x: Either[A, B]) -> Either[A, C]:
315
307
  """`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)
308
+ return Right(f(x.right)) if isinstance(x, Right) else _t.cast(Either[A, C], x)
317
309
 
318
310
 
319
311
  ### Function composition
320
312
 
321
313
 
322
- def compose(
323
- f: _t.Callable[[BType], CType], g: _t.Callable[[AType], BType]
324
- ) -> _t.Callable[[AType], CType]:
325
- return lambda x: f(g(x))
314
+ def compose[A, B, C, **PS](
315
+ f: _t.Callable[[B], C], g: _t.Callable[_t.Concatenate[A, PS], B]
316
+ ) -> _t.Callable[_t.Concatenate[A, PS], C]:
317
+ """Compose two functions `f` and `g` while passing all the additional `PS` arguments to `g`."""
326
318
 
319
+ def sub(x: A, /, *args: PS.args, **kwargs: PS.kwargs) -> C:
320
+ return f(g(x, *args, **kwargs))
327
321
 
328
- def compose_pipe(
329
- pipe: list[_t.Callable[_t.Concatenate[AType, AParamSpec], AType]], /
330
- ) -> _t.Callable[_t.Concatenate[AType, AParamSpec], AType]:
331
- """Compose a list of functions, similarly to how a shell pipe ("|")
332
- exression does it. I.e. compose them in reverse order.
322
+ return sub
333
323
 
334
- The mutating value gets passed as the first argument, all other
335
- arguments get passed unchanged.
336
324
 
337
- `compose_pipe([])` is `identity_`.
325
+ def compose_wide[A, B, C, **PS](
326
+ f: _t.Callable[_t.Concatenate[B, PS], C], g: _t.Callable[_t.Concatenate[A, PS], B]
327
+ ) -> _t.Callable[_t.Concatenate[A, PS], C]:
328
+ """Compose two functions `f` and `g` while passing all the additional `PS` arguments to both `f`
329
+ and `g`.
330
+ """
331
+
332
+ def sub(x: A, /, *args: PS.args, **kwargs: PS.kwargs) -> C:
333
+ return f(g(x, *args, **kwargs), *args, **kwargs)
334
+
335
+ return sub
336
+
337
+
338
+ def compose_pipe[A, **PS](
339
+ pipe: list[_t.Callable[_t.Concatenate[A, PS], A]], /
340
+ ) -> _t.Callable[_t.Concatenate[A, PS], A]:
341
+ """Compose a list of functions, similarly to how a shell pipe ("|") exression does it. I.e.
342
+ compose them in reverse order.
343
+
344
+ The mutating value gets passed as the first argument, all other arguments get passed unchanged
345
+ like with `compose_wide`.
346
+
347
+ `compose_pipe([])` is `identity`.
338
348
  `compose_pipe([x])` is `x`.
339
349
  """
340
350
  if len(pipe) == 0:
341
- return identity_
351
+ return identity
342
352
  if len(pipe) == 1:
343
353
  return pipe[0]
344
354
 
345
- def sub(x: AType, /, *args: AParamSpec.args, **kwargs: AParamSpec.kwargs) -> AType:
355
+ def sub(x: A, /, *args: PS.args, **kwargs: PS.kwargs) -> A:
346
356
  for func in pipe:
347
357
  x = func(x, *args, **kwargs)
348
358
  return x
@@ -374,17 +384,14 @@ def get_traceback(exc: BaseException, /, lines: int = 128) -> str:
374
384
  return fobj.getvalue()
375
385
 
376
386
 
377
- _BaseExeceptionType = _t.TypeVar("_BaseExeceptionType", bound=BaseException)
378
-
379
-
380
- def flat_exceptions(
381
- es: BaseExceptionGroup[_BaseExeceptionType], /
382
- ) -> _t.Iterator[_BaseExeceptionType]:
387
+ def flat_exceptions[ExceptionType: BaseException](
388
+ es: BaseExceptionGroup[ExceptionType], /
389
+ ) -> _t.Iterator[ExceptionType]:
383
390
  """For simplifying some `try ... except*` expressions."""
384
391
 
385
392
  def sub(
386
- excs: _BaseExeceptionType | BaseExceptionGroup[_BaseExeceptionType],
387
- ) -> _t.Iterator[_BaseExeceptionType]:
393
+ excs: ExceptionType | BaseExceptionGroup[ExceptionType],
394
+ ) -> _t.Iterator[ExceptionType]:
388
395
  if isinstance(excs, BaseExceptionGroup):
389
396
  for e in excs.exceptions:
390
397
  yield from sub(e)