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.
- stranded/__init__.py +51 -0
- stranded/abc/__init__.py +21 -0
- stranded/abc/composer_.py +191 -0
- stranded/argparse/__init__.py +24 -0
- stranded/argparse/abc/__init__.py +21 -0
- stranded/argparse/abc/argument_parser_.py +539 -0
- stranded/argparse/argument_parser_.py +13 -0
- stranded/argparse/asyncio/__init__.py +24 -0
- stranded/argparse/asyncio/argument_parser_.py +50 -0
- stranded/argparse/threading/__init__.py +24 -0
- stranded/argparse/threading/argument_parser_.py +52 -0
- stranded/asyncio/__init__.py +21 -0
- stranded/asyncio/composer_.py +76 -0
- stranded/builtins/__init__.py +21 -0
- stranded/builtins/exception_.py +6 -0
- stranded/composer_.py +27 -0
- stranded/execution/__init__.py +30 -0
- stranded/execution/abc/__init__.py +21 -0
- stranded/execution/abc/scheduler_.py +59 -0
- stranded/execution/asyncio/__init__.py +21 -0
- stranded/execution/asyncio/scheduler_.py +86 -0
- stranded/execution/scheduler_.py +59 -0
- stranded/execution/threading/__init__.py +21 -0
- stranded/execution/threading/scheduler_.py +116 -0
- stranded/functools/__init__.py +51 -0
- stranded/functools/abc/__init__.py +39 -0
- stranded/functools/abc/herd_.py +96 -0
- stranded/functools/abc/lru_cache_.py +192 -0
- stranded/functools/abc/retry_.py +60 -0
- stranded/functools/abc/throttle_.py +97 -0
- stranded/functools/asyncio/__init__.py +51 -0
- stranded/functools/asyncio/herd_.py +89 -0
- stranded/functools/asyncio/lru_cache_.py +92 -0
- stranded/functools/asyncio/retry_.py +43 -0
- stranded/functools/asyncio/throttle_.py +74 -0
- stranded/functools/herd_.py +18 -0
- stranded/functools/lru_cache_.py +18 -0
- stranded/functools/retry_.py +13 -0
- stranded/functools/threading/__init__.py +51 -0
- stranded/functools/threading/herd_.py +88 -0
- stranded/functools/threading/lru_cache_.py +90 -0
- stranded/functools/threading/retry_.py +42 -0
- stranded/functools/threading/throttle_.py +75 -0
- stranded/functools/throttle_.py +13 -0
- stranded/logging/__init__.py +24 -0
- stranded/logging/abc/__init__.py +21 -0
- stranded/logging/abc/logger_.py +96 -0
- stranded/logging/asyncio/__init__.py +21 -0
- stranded/logging/asyncio/logger_.py +41 -0
- stranded/logging/logger_.py +13 -0
- stranded/logging/threading/__init__.py +21 -0
- stranded/logging/threading/logger_.py +41 -0
- stranded/py.typed +0 -0
- stranded/sqlite3/__init__.py +24 -0
- stranded/sqlite3/abc/__init__.py +21 -0
- stranded/sqlite3/abc/db_.py +149 -0
- stranded/sqlite3/asyncio/__init__.py +24 -0
- stranded/sqlite3/asyncio/db_.py +43 -0
- stranded/sqlite3/db_.py +13 -0
- stranded/sqlite3/threading/__init__.py +24 -0
- stranded/sqlite3/threading/db_.py +43 -0
- stranded/threading/__init__.py +21 -0
- stranded/threading/composer_.py +74 -0
- stranded/types/__init__.py +21 -0
- stranded/types/constraint.py +7 -0
- stranded/types/convert.py +158 -0
- stranded-0.0.1.dist-info/METADATA +173 -0
- stranded-0.0.1.dist-info/RECORD +71 -0
- stranded-0.0.1.dist-info/WHEEL +5 -0
- stranded-0.0.1.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|