stranded 0.0.1__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 (71) hide show
  1. stranded/__init__.py +51 -0
  2. stranded/abc/__init__.py +21 -0
  3. stranded/abc/composer_.py +191 -0
  4. stranded/argparse/__init__.py +24 -0
  5. stranded/argparse/abc/__init__.py +21 -0
  6. stranded/argparse/abc/argument_parser_.py +539 -0
  7. stranded/argparse/argument_parser_.py +13 -0
  8. stranded/argparse/asyncio/__init__.py +24 -0
  9. stranded/argparse/asyncio/argument_parser_.py +50 -0
  10. stranded/argparse/threading/__init__.py +24 -0
  11. stranded/argparse/threading/argument_parser_.py +52 -0
  12. stranded/asyncio/__init__.py +21 -0
  13. stranded/asyncio/composer_.py +76 -0
  14. stranded/builtins/__init__.py +21 -0
  15. stranded/builtins/exception_.py +6 -0
  16. stranded/composer_.py +27 -0
  17. stranded/execution/__init__.py +30 -0
  18. stranded/execution/abc/__init__.py +21 -0
  19. stranded/execution/abc/scheduler_.py +59 -0
  20. stranded/execution/asyncio/__init__.py +21 -0
  21. stranded/execution/asyncio/scheduler_.py +86 -0
  22. stranded/execution/scheduler_.py +59 -0
  23. stranded/execution/threading/__init__.py +21 -0
  24. stranded/execution/threading/scheduler_.py +116 -0
  25. stranded/functools/__init__.py +51 -0
  26. stranded/functools/abc/__init__.py +39 -0
  27. stranded/functools/abc/herd_.py +96 -0
  28. stranded/functools/abc/lru_cache_.py +192 -0
  29. stranded/functools/abc/retry_.py +60 -0
  30. stranded/functools/abc/throttle_.py +97 -0
  31. stranded/functools/asyncio/__init__.py +51 -0
  32. stranded/functools/asyncio/herd_.py +89 -0
  33. stranded/functools/asyncio/lru_cache_.py +92 -0
  34. stranded/functools/asyncio/retry_.py +43 -0
  35. stranded/functools/asyncio/throttle_.py +74 -0
  36. stranded/functools/herd_.py +18 -0
  37. stranded/functools/lru_cache_.py +18 -0
  38. stranded/functools/retry_.py +13 -0
  39. stranded/functools/threading/__init__.py +51 -0
  40. stranded/functools/threading/herd_.py +88 -0
  41. stranded/functools/threading/lru_cache_.py +90 -0
  42. stranded/functools/threading/retry_.py +42 -0
  43. stranded/functools/threading/throttle_.py +75 -0
  44. stranded/functools/throttle_.py +13 -0
  45. stranded/logging/__init__.py +24 -0
  46. stranded/logging/abc/__init__.py +21 -0
  47. stranded/logging/abc/logger_.py +96 -0
  48. stranded/logging/asyncio/__init__.py +21 -0
  49. stranded/logging/asyncio/logger_.py +41 -0
  50. stranded/logging/logger_.py +13 -0
  51. stranded/logging/threading/__init__.py +21 -0
  52. stranded/logging/threading/logger_.py +41 -0
  53. stranded/py.typed +0 -0
  54. stranded/sqlite3/__init__.py +24 -0
  55. stranded/sqlite3/abc/__init__.py +21 -0
  56. stranded/sqlite3/abc/db_.py +149 -0
  57. stranded/sqlite3/asyncio/__init__.py +24 -0
  58. stranded/sqlite3/asyncio/db_.py +43 -0
  59. stranded/sqlite3/db_.py +13 -0
  60. stranded/sqlite3/threading/__init__.py +24 -0
  61. stranded/sqlite3/threading/db_.py +43 -0
  62. stranded/threading/__init__.py +21 -0
  63. stranded/threading/composer_.py +74 -0
  64. stranded/types/__init__.py +21 -0
  65. stranded/types/constraint.py +7 -0
  66. stranded/types/convert.py +158 -0
  67. stranded-0.0.1.dist-info/METADATA +173 -0
  68. stranded-0.0.1.dist-info/RECORD +71 -0
  69. stranded-0.0.1.dist-info/WHEEL +5 -0
  70. stranded-0.0.1.dist-info/licenses/LICENSE +21 -0
  71. stranded-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,539 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import dataclasses
5
+ import enum
6
+ import inspect
7
+ import typing
8
+
9
+ from stranded import types
10
+
11
+ from stranded.abc import composer_
12
+ from stranded.builtins import exception_
13
+
14
+
15
+ @dataclasses.dataclass(frozen=True, kw_only=True)
16
+ class _Annotation[T](abc.ABC):
17
+ t: T
18
+ comment: str = ""
19
+
20
+ @abc.abstractmethod
21
+ def to_short_str(self) -> str: ...
22
+
23
+ @abc.abstractmethod
24
+ def to_long_str(self) -> str: ...
25
+
26
+ def to_type_str(self) -> str:
27
+ if typing.get_origin(self.t) is typing.Literal:
28
+ return f'literal: {{{', '.join(map(str, typing.get_args(self.t)))}}}'
29
+ if isinstance(self.t, type) and issubclass(self.t, enum.Enum):
30
+ return f'enum: {{{', '.join(self.t._member_names_)}}}'
31
+ return f'type: {self.t.__name__}'
32
+
33
+
34
+ @dataclasses.dataclass(frozen=True, kw_only=True)
35
+ class _Parameter[T](_Annotation[T], abc.ABC):
36
+ name: str
37
+
38
+ def __call__(self, arg: str) -> T:
39
+ return types.Convert(t=self.t)(arg)
40
+
41
+ def to_long_str(self) -> str:
42
+ return '\n'.join((
43
+ f' {self.to_short_str()}',
44
+ f' {self.to_type_str()}',
45
+ *((f' {self.comment}',) if self.comment else ()),
46
+ ))
47
+
48
+
49
+ @dataclasses.dataclass(frozen=True, kw_only=True)
50
+ class _RequiredParameter[T](_Parameter[T], abc.ABC):
51
+
52
+ @classmethod
53
+ def of_parameter(cls, parameter: inspect.Parameter, /) -> typing.Self:
54
+ match parameter.annotation, typing.get_origin(parameter.annotation), typing.get_args(parameter.annotation):
55
+ case t, None, ():
56
+ return cls(name=parameter.name, t=t)
57
+ case _, typing.Annotated, (t, *_, comment):
58
+ return cls(name=parameter.name, t=t, comment=comment)
59
+ case _, t, ts:
60
+ return cls(name=parameter.name, t=t[*ts])
61
+ case _:
62
+ raise RuntimeError()
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True, kw_only=True)
66
+ class _OptionalParameter[T](_Parameter[T], abc.ABC):
67
+ default: T
68
+
69
+ @classmethod
70
+ def of_parameter(cls, parameter: inspect.Parameter, /) -> typing.Self:
71
+ match parameter.annotation, typing.get_origin(parameter.annotation), typing.get_args(parameter.annotation):
72
+ case t, None, ():
73
+ return cls(name=parameter.name, t=t, default=parameter.default)
74
+ case _, typing.Annotated, (t, *_, comment):
75
+ return cls(name=parameter.name, t=t, comment=comment, default=parameter.default)
76
+ case _, t, ts:
77
+ return cls(name=parameter.name, t=t[*ts], default=parameter.default)
78
+ case _:
79
+ raise RuntimeError()
80
+
81
+
82
+ @dataclasses.dataclass(frozen=True, kw_only=True)
83
+ class _VariadicParameter[T](_Parameter[T], abc.ABC):
84
+
85
+ @classmethod
86
+ def of_parameter(cls, parameter: inspect.Parameter, /) -> typing.Self:
87
+ match parameter.annotation, typing.get_origin(parameter.annotation), typing.get_args(parameter.annotation):
88
+ case t, None, ():
89
+ return cls(name=parameter.name, t=t)
90
+ case _, typing.Annotated, (t, *_, comment):
91
+ return cls(name=parameter.name, t=t, comment=comment)
92
+ case _, t, ts:
93
+ return cls(name=parameter.name, t=t[*ts])
94
+ case _:
95
+ raise RuntimeError()
96
+
97
+
98
+ @dataclasses.dataclass(frozen=True, kw_only=True)
99
+ class _StackedParameter[T](_Parameter[T], abc.ABC): ...
100
+
101
+
102
+ @dataclasses.dataclass(frozen=True, kw_only=True)
103
+ class _RequiredPositionalParameter[T](_RequiredParameter[T], _StackedParameter[T]):
104
+
105
+ def to_short_str(self) -> str:
106
+ return f'<{self.name}>'
107
+
108
+
109
+ @dataclasses.dataclass(frozen=True, kw_only=True)
110
+ class _OptionalPositionalParameter[T](_OptionalParameter[T], _StackedParameter[T]):
111
+
112
+ def to_short_str(self) -> str:
113
+ return f'[<{self.name}({self.default})>]'
114
+
115
+
116
+ @dataclasses.dataclass(frozen=True, kw_only=True)
117
+ class _VariadicPositionalParameter[T](_VariadicParameter[T], _StackedParameter[T]):
118
+
119
+ def to_short_str(self) -> str:
120
+ return f'[<{self.name}>]...'
121
+
122
+
123
+ @dataclasses.dataclass(frozen=True, kw_only=True)
124
+ class _KeywordParameter[T](_Parameter[T], abc.ABC): ...
125
+
126
+
127
+ @dataclasses.dataclass(frozen=True, kw_only=True)
128
+ class _RequiredKeywordParameter[T](_RequiredParameter[T], _KeywordParameter[T]):
129
+
130
+ def to_short_str(self) -> str:
131
+ return f'--{self.name} <{self.name}>'
132
+
133
+
134
+ @dataclasses.dataclass(frozen=True, kw_only=True)
135
+ class _OptionalKeywordParameter[T](_OptionalParameter[T], _KeywordParameter[T]):
136
+
137
+ def to_short_str(self) -> str:
138
+ return f'[-{self.name}]' if (self.t == bool and self.default == False) else (
139
+ f'[--{self.name} <{self.name}({self.default})>]'
140
+ )
141
+
142
+
143
+ @dataclasses.dataclass(frozen=True, kw_only=True)
144
+ class _VariadicKeywordParameter[T](_VariadicParameter[T], _KeywordParameter[T]):
145
+
146
+ def to_short_str(self) -> str:
147
+ return f'[--{self.name} <{self.name}>]...'
148
+
149
+
150
+ @dataclasses.dataclass(frozen=True, kw_only=True)
151
+ class _ReturnAnnotation[T](_Annotation[T]):
152
+
153
+ @staticmethod
154
+ def of_annotation(annotation: type) -> _ReturnAnnotation[typing.Any]:
155
+ match annotation, typing.get_origin(annotation), typing.get_args(annotation):
156
+ case t, None, ():
157
+ return _ReturnAnnotation(t=t)
158
+ case _, typing.Annotated, (t, *_, comment):
159
+ return _ReturnAnnotation(t=t, comment=comment)
160
+ case _, t, ts:
161
+ return _ReturnAnnotation(t=t[*ts]) # type: ignore[index]
162
+ case _:
163
+ raise RuntimeError(f'{annotation=}. Cannot create return annotation.')
164
+
165
+ def to_short_str(self) -> str:
166
+ return ''
167
+
168
+ def to_long_str(self) -> str:
169
+ return '\n'.join((
170
+ f' return: {self.t}',
171
+ *((f' {self.comment}',) if self.comment else ()),
172
+ ))
173
+
174
+
175
+ @dataclasses.dataclass(frozen=True, kw_only=True)
176
+ class _Signature:
177
+ return_annotation: _ReturnAnnotation[typing.Any]
178
+
179
+ # Parameters that are detected by position.
180
+ optional_positional_parameter_by_name: dict[str, _OptionalPositionalParameter[typing.Any]]
181
+ required_positional_parameter_by_name: dict[str, _RequiredPositionalParameter[typing.Any]]
182
+ variadic_positional_parameter: _VariadicPositionalParameter[typing.Any] | None
183
+
184
+ # Flags that are detected by two leading dashes.
185
+ optional_keyword_parameter_by_name: dict[str, _OptionalKeywordParameter[typing.Any]]
186
+ required_keyword_parameter_by_name: dict[str, _RequiredKeywordParameter[typing.Any]]
187
+ variadic_keyword_parameter: _VariadicKeywordParameter[typing.Any] | None
188
+
189
+ ReturnAnnotation: typing.ClassVar = _ReturnAnnotation
190
+
191
+ RequiredPositionalParameter: typing.ClassVar = _RequiredPositionalParameter
192
+ OptionalPositionalParameter: typing.ClassVar = _OptionalPositionalParameter
193
+ VariadicPositionalParameter: typing.ClassVar = _VariadicPositionalParameter
194
+
195
+ RequiredKeywordParameter: typing.ClassVar = _RequiredKeywordParameter
196
+ OptionalKeywordParameter: typing.ClassVar = _OptionalKeywordParameter
197
+ VariadicKeywordParameter: typing.ClassVar = _VariadicKeywordParameter
198
+
199
+ @property
200
+ def variadic_parameter_by_name(self) -> dict[str, _VariadicParameter[typing.Any]]:
201
+ return {
202
+ parameter.name: parameter
203
+ for parameter in (self.variadic_positional_parameter, self.variadic_keyword_parameter)
204
+ if parameter is not None
205
+ }
206
+
207
+ @property
208
+ def keyword_parameter_by_name(self) -> dict[str, _KeywordParameter[typing.Any]]:
209
+ return self.optional_keyword_parameter_by_name | self.required_keyword_parameter_by_name # type: ignore[no-any-return, operator]
210
+
211
+ @property
212
+ def positional_parameter_by_name(self) -> dict[str, _StackedParameter[typing.Any]]:
213
+ return self.optional_positional_parameter_by_name | self.required_positional_parameter_by_name # type: ignore[no-any-return, operator]
214
+
215
+ @property
216
+ def parameter_by_name(self) -> dict[str, _Parameter[typing.Any]]:
217
+ return self.variadic_parameter_by_name | self.positional_parameter_by_name | self.keyword_parameter_by_name # type: ignore[no-any-return, operator]
218
+
219
+ @property
220
+ def annotations(self) -> tuple[_Annotation[typing.Any], ...]:
221
+ return self.return_annotation, *self.parameter_by_name.values()
222
+
223
+ def __call__(self, *argv: str) -> _BoundSignature:
224
+ args = list(reversed(argv))
225
+
226
+ required_positional_parameter_by_name = dict(self.required_positional_parameter_by_name)
227
+ optional_positional_parameter_by_name = dict(self.optional_positional_parameter_by_name)
228
+
229
+ required_keyword_parameter_by_name = dict(self.required_keyword_parameter_by_name)
230
+ optional_keyword_parameter_by_name = dict(self.optional_keyword_parameter_by_name)
231
+
232
+ positional_value_by_name = dict()
233
+ variadic_positional_values = []
234
+
235
+ keyword_value_by_name = dict()
236
+ variadic_keyword_value_by_name = dict()
237
+
238
+ while args:
239
+ match args[-1].split('-'):
240
+
241
+ case ('', '', name) if parameter := required_keyword_parameter_by_name.pop(name, None):
242
+ args.pop()
243
+ keyword_value_by_name[name] = parameter(args.pop())
244
+ case ('', '', name) if parameter := optional_keyword_parameter_by_name.pop(name, None): # type: ignore[assignment]
245
+ args.pop()
246
+ keyword_value_by_name[name] = parameter(args.pop())
247
+ case ('', '', name) if parameter := self.variadic_keyword_parameter: # type: ignore[assignment]
248
+ args.pop()
249
+ variadic_keyword_value_by_name[name] = parameter(args.pop())
250
+ case ('', '', _) if parameter := self.variadic_positional_parameter: # type: ignore[assignment]
251
+ variadic_positional_values += [args.pop(), parameter(args.pop())]
252
+
253
+ # Special cases of keyword arguments where type is bool.
254
+ case ('', name) if (
255
+ (parameter := required_keyword_parameter_by_name.pop(name, None))
256
+ and (parameter.t == bool)
257
+ ):
258
+ args.pop()
259
+ keyword_value_by_name[name] = True
260
+ case ('', name) if (
261
+ (parameter := optional_keyword_parameter_by_name.pop(name, None)) # type: ignore[assignment]
262
+ and (parameter.t == bool)
263
+ and (parameter.default == False)
264
+ ):
265
+ args.pop()
266
+ keyword_value_by_name[name] = True
267
+ case ('', name) if self.variadic_keyword_parameter and (self.variadic_keyword_parameter.t == bool):
268
+ args.pop()
269
+ variadic_keyword_value_by_name[name] = True
270
+ case ('', _) if self.variadic_positional_parameter and (self.variadic_positional_parameter.t == bool):
271
+ args.pop()
272
+ variadic_positional_values.append(True)
273
+
274
+ case (_arg,):
275
+ break
276
+
277
+ while args:
278
+ match args[-1]:
279
+ case arg if required_positional_parameter_by_name:
280
+ name, parameter = required_positional_parameter_by_name.popitem() # type: ignore[assignment]
281
+ positional_value_by_name[name] = parameter(args.pop()) # type: ignore[misc]
282
+ case arg if optional_positional_parameter_by_name:
283
+ name, parameter = optional_positional_parameter_by_name.popitem() # type: ignore[assignment]
284
+ positional_value_by_name[name] = parameter(args.pop()) # type: ignore[misc]
285
+ case arg if parameter := self.variadic_positional_parameter: # type: ignore[assignment]
286
+ variadic_positional_values.append(parameter(args.pop()))
287
+
288
+ case _:
289
+ raise RuntimeError(f'Could not parse {arg=}.')
290
+
291
+ return _BoundSignature(
292
+ return_annotation=self.return_annotation,
293
+
294
+ required_positional_parameter_by_name=self.required_positional_parameter_by_name,
295
+ optional_positional_parameter_by_name=self.optional_positional_parameter_by_name,
296
+
297
+ required_keyword_parameter_by_name=self.required_keyword_parameter_by_name,
298
+ optional_keyword_parameter_by_name=self.optional_keyword_parameter_by_name,
299
+
300
+ variadic_positional_parameter=self.variadic_positional_parameter,
301
+ variadic_keyword_parameter=self.variadic_keyword_parameter,
302
+
303
+ positional_value_by_name=positional_value_by_name,
304
+ keyword_value_by_name=keyword_value_by_name,
305
+
306
+ variadic_positional_values=variadic_positional_values,
307
+ variadic_keyword_value_by_name=variadic_keyword_value_by_name,
308
+ )
309
+
310
+ @staticmethod
311
+ def of_signature(signature: inspect.Signature, /) -> _Signature:
312
+ variadic_keyword_parameter: _VariadicKeywordParameter[typing.Any] | None = None
313
+ variadic_positional_parameter: _VariadicPositionalParameter[typing.Any] | None = None
314
+ optional_keyword_parameter_by_name: dict[str, _OptionalKeywordParameter[typing.Any]] = {}
315
+ required_keyword_parameter_by_name: dict[str, _RequiredKeywordParameter[typing.Any]] = {}
316
+ optional_positional_parameter_by_name: dict[str, _OptionalPositionalParameter[typing.Any]] = {}
317
+ required_positional_parameter_by_name: dict[str, _RequiredPositionalParameter[typing.Any]] = {}
318
+
319
+ for parameter in reversed(signature.parameters.values()): # type: ignore[call-overload]
320
+ match parameter:
321
+ case inspect.Parameter(name=name, kind=parameter.POSITIONAL_ONLY, default=parameter.empty):
322
+ required_positional_parameter_by_name[name] = _RequiredPositionalParameter.of_parameter(parameter)
323
+ case inspect.Parameter(name=name, kind=parameter.POSITIONAL_ONLY):
324
+ optional_positional_parameter_by_name[name] = _OptionalPositionalParameter.of_parameter(parameter)
325
+
326
+ case inspect.Parameter(name=name, kind=parameter.POSITIONAL_OR_KEYWORD, default=parameter.empty):
327
+ required_positional_parameter_by_name[name] = _RequiredPositionalParameter.of_parameter(parameter)
328
+ case inspect.Parameter(name=name, kind=parameter.POSITIONAL_OR_KEYWORD):
329
+ optional_keyword_parameter_by_name[name] = _OptionalKeywordParameter.of_parameter(parameter)
330
+
331
+ case inspect.Parameter(name=name, kind=parameter.KEYWORD_ONLY, default=parameter.empty):
332
+ required_keyword_parameter_by_name[name] = _RequiredKeywordParameter.of_parameter(parameter)
333
+ case inspect.Parameter(name=name, kind=parameter.KEYWORD_ONLY):
334
+ optional_keyword_parameter_by_name[name] = _OptionalKeywordParameter.of_parameter(parameter)
335
+
336
+ case inspect.Parameter(kind=parameter.VAR_KEYWORD):
337
+ variadic_keyword_parameter = _VariadicKeywordParameter.of_parameter(parameter)
338
+ case inspect.Parameter(kind=parameter.VAR_POSITIONAL):
339
+ variadic_positional_parameter = _VariadicPositionalParameter.of_parameter(parameter)
340
+
341
+ return _Signature(
342
+ return_annotation=_ReturnAnnotation[typing.Any].of_annotation(signature.return_annotation),
343
+ optional_positional_parameter_by_name=optional_positional_parameter_by_name,
344
+ required_positional_parameter_by_name=required_positional_parameter_by_name,
345
+ optional_keyword_parameter_by_name=optional_keyword_parameter_by_name,
346
+ required_keyword_parameter_by_name=required_keyword_parameter_by_name,
347
+ variadic_keyword_parameter=variadic_keyword_parameter,
348
+ variadic_positional_parameter=variadic_positional_parameter,
349
+ )
350
+
351
+ @staticmethod
352
+ def of_signatures(left_signature: _Signature, /, *signatures: _Signature) -> _Signature:
353
+ for right_signature in signatures:
354
+ for name in (
355
+ (left_parameter_by_name := left_signature.parameter_by_name).keys()
356
+ & (right_parameter_by_name := right_signature.parameter_by_name).keys()
357
+ ):
358
+ assert (
359
+ left_parameter := left_parameter_by_name[name]
360
+ ) == (
361
+ right_parameter := right_parameter_by_name[name]
362
+ ), (
363
+ f'Cannot merge conflicting CLI signatures with {left_parameter=} and {right_parameter=}.'
364
+ )
365
+
366
+ left_signature = _Signature(
367
+ required_positional_parameter_by_name=(
368
+ right_signature.required_positional_parameter_by_name
369
+ | left_signature.required_positional_parameter_by_name
370
+ ),
371
+ optional_positional_parameter_by_name=(
372
+ right_signature.optional_positional_parameter_by_name
373
+ | left_signature.optional_positional_parameter_by_name
374
+ ),
375
+ required_keyword_parameter_by_name=(
376
+ right_signature.required_keyword_parameter_by_name
377
+ | left_signature.required_keyword_parameter_by_name
378
+ ),
379
+ optional_keyword_parameter_by_name=(
380
+ right_signature.optional_keyword_parameter_by_name
381
+ | left_signature.optional_keyword_parameter_by_name
382
+ ),
383
+ variadic_positional_parameter=(
384
+ right_signature.variadic_positional_parameter
385
+ or left_signature.variadic_positional_parameter
386
+ ),
387
+ variadic_keyword_parameter=(
388
+ right_signature.variadic_keyword_parameter
389
+ or left_signature.variadic_keyword_parameter
390
+ ),
391
+ return_annotation=right_signature.return_annotation,
392
+ )
393
+
394
+ return left_signature
395
+
396
+ def to_short_str(self) -> str:
397
+ return ' '.join((value.to_short_str() for value in reversed(self.annotations))).rstrip()
398
+
399
+ def to_long_str(self) -> str:
400
+ return '\n'.join((value.to_long_str() for value in reversed(self.annotations))).rstrip()
401
+
402
+
403
+ @dataclasses.dataclass(frozen=True, kw_only=True)
404
+ class _BoundSignature(_Signature):
405
+ positional_value_by_name: dict[str, object]
406
+ keyword_value_by_name: dict[str, object]
407
+
408
+ variadic_positional_values: list[object]
409
+ variadic_keyword_value_by_name: dict[str, object]
410
+
411
+ def __call__(self, composeds: typing.Iterable[Composed[..., typing.Any]]) -> typing.Iterable[typing.Callable[..., typing.Any]]: # type: ignore[override]
412
+ for composed in composeds:
413
+ composee_signature = inspect.signature(composed.composee)
414
+ yield lambda: composed.composee(
415
+ *(
416
+ value for name, value in self.positional_value_by_name.items()
417
+ if name in composee_signature.parameters
418
+ and isinstance(self.parameter_by_name[name], _StackedParameter)
419
+ ),
420
+ *(
421
+ value for value in self.variadic_positional_values
422
+ if self.variadic_positional_parameter.name in composee_signature.parameters # type: ignore[union-attr]
423
+ ),
424
+ **{
425
+ name: value for name, value in self.keyword_value_by_name.items()
426
+ if name in composee_signature.parameters
427
+ and isinstance(self.parameter_by_name[name], _KeywordParameter)
428
+ },
429
+ **{
430
+ name: value for name, value in self.variadic_keyword_value_by_name.items()
431
+ if self.variadic_keyword_parameter.name in composee_signature.parameters # type: ignore[union-attr]
432
+ },
433
+ )
434
+
435
+
436
+ class Exception(exception_.Exception): ... # noqa
437
+
438
+
439
+ @typing.runtime_checkable
440
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol): ...
441
+
442
+
443
+ @dataclasses.dataclass(frozen=True, kw_only=True)
444
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC): ...
445
+
446
+
447
+ @dataclasses.dataclass(frozen=True, kw_only=True)
448
+ class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], abc.ABC): ...
449
+
450
+
451
+ @dataclasses.dataclass(frozen=True, kw_only=True)
452
+ class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], abc.ABC): ...
453
+
454
+
455
+ @dataclasses.dataclass(frozen=True, kw_only=True)
456
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC):
457
+ composee: Composee[ParamT, RetT]
458
+ children: tuple[typing.Self, typing.Self] | None = None
459
+
460
+ @property
461
+ def composeds(self) -> tuple[typing.Self, ...]:
462
+ match self.children:
463
+ case None:
464
+ return self,
465
+ case left, right:
466
+ return *left.composeds, *right.composeds
467
+ case _:
468
+ raise RuntimeError(f'{self.children=}. Expected {self.__annotations__['children']}')
469
+
470
+ def __call__(self, *argv: str) -> typing.Iterable[typing.Callable[..., typing.Any]]:
471
+ yield from self.to_signature()(*argv)(self.composeds)
472
+
473
+
474
+ def __or__[Composed2T: Composed[..., typing.Any]](self, other: Composed2T) -> Composed2T:
475
+ return dataclasses.replace(other, children=(self, other))
476
+
477
+ def __xor__[Composed2T: Composed[..., typing.Any]](self, other: Composed2T) -> Composed2T: # type: ignore[empty-body]
478
+ # TODO make this mark flags in the two CLI's as mutually exclusive.
479
+ ...
480
+
481
+ def __str__(self) -> str:
482
+ return '\n'.join([
483
+ f'{self.__doc__}',
484
+ '',
485
+ f'{self.__signature__}',
486
+ ])
487
+
488
+ def to_long_str(self) -> str:
489
+ return '\n\n'.join([
490
+ self.to_signature().to_short_str(),
491
+ f'{self.__doc__}',
492
+ self.to_signature().to_long_str(),
493
+ ])
494
+
495
+ def to_signature(self) -> _Signature:
496
+ return _Signature.of_signature(inspect.signature(self.composee, eval_str=True)) if self.children is None else (
497
+ _Signature.of_signatures(self.children[0].to_signature(), self.children[1].to_signature())
498
+ )
499
+
500
+
501
+ @dataclasses.dataclass(frozen=True, kw_only=True)
502
+ class ArgumentParser[**ParamT, RetT](composer_.Composer[ParamT, RetT], abc.ABC):
503
+ type HT = typing.Annotated[bool, 'Print help and exit.']
504
+ type LogLevelT = typing.Annotated[
505
+ typing.Literal['INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'],
506
+ 'Log level to set.',
507
+ ]
508
+
509
+ type ComposeeT = Composee[ParamT, RetT]
510
+ type ConnectT = Connect[ParamT, RetT]
511
+ type ExitT = Exit[ParamT, RetT]
512
+ type EnterT = Enter[ParamT, RetT]
513
+ type ComposedT = Composed[ParamT, RetT]
514
+ type ComposerT = ArgumentParser[ParamT, RetT]
515
+
516
+ Signature: typing.ClassVar = _Signature
517
+ Composee: typing.ClassVar[type[Composee[typing.Any, typing.Any]]]
518
+ Connect: typing.ClassVar[type[Connect[typing.Any, typing.Any]]]
519
+ Exit: typing.ClassVar[type[Exit[typing.Any, typing.Any]]]
520
+ Enter: typing.ClassVar[type[Enter[typing.Any, typing.Any]]]
521
+ Composed: typing.ClassVar[type[Composed[typing.Any, typing.Any]]]
522
+ Composer: typing.ClassVar[type[ArgumentParser[typing.Any, typing.Any]]]
523
+
524
+ def __call__(self, composee: ComposeeT, /) -> ComposedT:
525
+ return self.Composed( # type: ignore[return-value]
526
+ __doc__=str(composee.__doc__),
527
+ __module__=str(composee.__module__),
528
+ __name__=str(composee.__name__),
529
+ __qualname__=str(composee.__qualname__),
530
+ __signature__=inspect.signature(composee).replace(
531
+ parameters=(inspect.Parameter('argv', inspect.Parameter.VAR_POSITIONAL, annotation=str),),
532
+ ),
533
+ composee=composee,
534
+ composer=self,
535
+ stack=(self.Enter(composer=self, composee=composee),),
536
+ )
537
+
538
+
539
+ Composer = ArgumentParser
@@ -0,0 +1,13 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from .abc import argument_parser_
5
+ from .. import composer_
6
+
7
+
8
+ @dataclasses.dataclass(frozen=True, kw_only=True)
9
+ class ArgumentParser(composer_.Composer[..., typing.Any], argument_parser_.ArgumentParser[..., typing.Any]): ... # type: ignore[misc]
10
+
11
+
12
+ Composer = ArgumentParser
13
+ argument_parser = ArgumentParser()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib as _importlib
4
+ import typing as _typing
5
+
6
+ if _typing.TYPE_CHECKING:
7
+ from . import argument_parser_
8
+ from .argument_parser_ import ArgumentParser
9
+ from .argument_parser_ import argument_parser
10
+
11
+
12
+ def __getattr__(name: str) -> _typing.Any:
13
+ match name:
14
+ case 'argument_parser_': return _importlib.import_module('.argument_parser_', __name__)
15
+ case 'ArgumentParser': return _importlib.import_module('.argument_parser_', __name__).ArgumentParser
16
+ case 'argument_parser': return _importlib.import_module('.argument_parser_', __name__).argument_parser
17
+ case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
18
+
19
+
20
+ __all__ = (
21
+ 'argument_parser_',
22
+ 'ArgumentParser',
23
+ 'argument_parser',
24
+ )
@@ -0,0 +1,50 @@
1
+ import dataclasses
2
+ import inspect
3
+ import typing
4
+
5
+ from ...asyncio import composer_
6
+ from ..abc import argument_parser_
7
+
8
+ @typing.runtime_checkable
9
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], argument_parser_.Composee[ParamT, RetT], typing.Protocol): ...
10
+
11
+
12
+ @typing.final
13
+ @dataclasses.dataclass(frozen=True, kw_only=True)
14
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], argument_parser_.Connect[ParamT, RetT]): ...
15
+
16
+
17
+ @typing.final
18
+ @dataclasses.dataclass(frozen=True, kw_only=True)
19
+ class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], argument_parser_.Exit[ParamT, RetT]): ...
20
+
21
+
22
+ @typing.final
23
+ @dataclasses.dataclass(frozen=True, kw_only=True)
24
+ class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], argument_parser_.Enter[ParamT, RetT]): ...
25
+
26
+
27
+ @typing.final
28
+ @dataclasses.dataclass(frozen=True, kw_only=True)
29
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], argument_parser_.Composed[ParamT, RetT]):
30
+ async def __call__(self, *argv: str) -> RetT: # type: ignore[override]
31
+ ret = None
32
+ for call in super(composer_.Composed, self).__call__(*argv):
33
+ ret = call()
34
+ if inspect.iscoroutine(ret):
35
+ ret = await ret
36
+ return ret # type: ignore[return-value]
37
+
38
+
39
+ @typing.final
40
+ @dataclasses.dataclass(frozen=True, kw_only=True)
41
+ class ArgumentParser[**ParamT = ..., RetT = typing.Any](composer_.Composer[ParamT, RetT], argument_parser_.Composer[ParamT, RetT]):
42
+ Composee: typing.ClassVar = Composee
43
+ Connect: typing.ClassVar = Connect
44
+ Exit: typing.ClassVar = Exit
45
+ Enter: typing.ClassVar = Enter
46
+ Composed: typing.ClassVar = Composed
47
+
48
+
49
+ Composer = ArgumentParser
50
+ argument_parser = ArgumentParser()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib as _importlib
4
+ import typing as _typing
5
+
6
+ if _typing.TYPE_CHECKING:
7
+ from . import argument_parser_
8
+ from .argument_parser_ import ArgumentParser
9
+ from .argument_parser_ import argument_parser
10
+
11
+
12
+ def __getattr__(name: str) -> _typing.Any:
13
+ match name:
14
+ case 'argument_parser_': return _importlib.import_module('.argument_parser_', __name__)
15
+ case 'ArgumentParser': return _importlib.import_module('.argument_parser_', __name__).ArgumentParser
16
+ case 'argument_parser': return _importlib.import_module('.argument_parser_', __name__).argument_parser
17
+ case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
18
+
19
+
20
+ __all__ = (
21
+ 'argument_parser_',
22
+ 'ArgumentParser',
23
+ 'argument_parser',
24
+ )