plugantic 0.2.3__tar.gz → 0.2.4__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.
- {plugantic-0.2.3/src/plugantic.egg-info → plugantic-0.2.4}/PKG-INFO +1 -1
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic/_consts.py +1 -1
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic/plugin.py +55 -54
- {plugantic-0.2.3 → plugantic-0.2.4/src/plugantic.egg-info}/PKG-INFO +1 -1
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic.egg-info/SOURCES.txt +0 -1
- {plugantic-0.2.3 → plugantic-0.2.4}/test/test_auto_downcast.py +3 -3
- {plugantic-0.2.3 → plugantic-0.2.4}/test/test_basic_usage.py +63 -8
- plugantic-0.2.3/src/plugantic/_helpers.py +0 -42
- {plugantic-0.2.3 → plugantic-0.2.4}/LICENSE +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/README.md +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/pyproject.toml +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/setup.cfg +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic/__init__.py +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic/_types.py +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic.egg-info/dependency_links.txt +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic.egg-info/requires.txt +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/src/plugantic.egg-info/top_level.txt +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/test/test_combined_usage.py +0 -0
- {plugantic-0.2.3 → plugantic-0.2.4}/test/test_schema_error.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Single source of truth for information needed at runtime and build-time (i.e. version)
|
|
2
|
-
version = "0.2.
|
|
2
|
+
version = "0.2.4"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing_extensions import ClassVar, Type, Self, Literal, Any, TypeVar, Set,
|
|
1
|
+
from typing_extensions import ClassVar, Type, Self, Literal, Any, TypeVar, Set, Collection, Sequence, get_type_hints, get_origin, get_args, TYPE_CHECKING
|
|
2
2
|
from pydantic import BaseModel, GetCoreSchemaHandler, Field, ConfigDict, model_validator
|
|
3
3
|
from pydantic.fields import FieldInfo
|
|
4
4
|
from pydantic_core.core_schema import tagged_union_schema, union_schema
|
|
@@ -6,7 +6,7 @@ from pydantic_core.core_schema import tagged_union_schema, union_schema
|
|
|
6
6
|
|
|
7
7
|
class PluganticConfigDict(ConfigDict, total=False):
|
|
8
8
|
varname_type: str
|
|
9
|
-
value: str
|
|
9
|
+
value: str|Collection[str]
|
|
10
10
|
auto_downcast: bool
|
|
11
11
|
downcast_order: int
|
|
12
12
|
|
|
@@ -15,28 +15,24 @@ _T2 = TypeVar("_T2")
|
|
|
15
15
|
|
|
16
16
|
class PluganticModelMeta(type(BaseModel)):
|
|
17
17
|
def __and__(cls: Type[_T1], other: Type[_T2]) -> Type[_T1|_T2]:
|
|
18
|
-
if issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel):
|
|
19
|
-
return PluganticCombinedAnd(cls, other)
|
|
20
|
-
|
|
21
|
-
return super().__and__(other)
|
|
18
|
+
if issubclass(cls, PluginModel) and (issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel)):
|
|
19
|
+
return PluganticCombinedAnd(cls, other) # pyright: ignore[reportReturnType]
|
|
20
|
+
return NotImplemented
|
|
22
21
|
|
|
23
22
|
def __rand__(cls: Type[_T1], other: Type[_T2]) -> Type[_T1|_T2]:
|
|
24
|
-
if issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel):
|
|
25
|
-
return PluganticCombinedAnd(other, cls)
|
|
26
|
-
|
|
27
|
-
return super().__rand__(other)
|
|
23
|
+
if issubclass(cls, PluginModel) and (issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel)):
|
|
24
|
+
return PluganticCombinedAnd(other, cls) # pyright: ignore[reportReturnType]
|
|
25
|
+
return NotImplemented
|
|
28
26
|
|
|
29
27
|
def __or__(cls: Type[_T1], other: Type[_T2]) -> Type[_T1|_T2]:
|
|
30
|
-
if issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel):
|
|
31
|
-
return PluganticCombinedOr(cls, other)
|
|
32
|
-
|
|
33
|
-
return super().__or__(other)
|
|
28
|
+
if issubclass(cls, PluginModel) and (issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel)):
|
|
29
|
+
return PluganticCombinedOr(cls, other) # pyright: ignore[reportReturnType]
|
|
30
|
+
return NotImplemented
|
|
34
31
|
|
|
35
32
|
def __ror__(cls: Type[_T1], other: Type[_T2]) -> Type[_T1|_T2]:
|
|
36
|
-
if issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel):
|
|
37
|
-
return PluganticCombinedOr(other, cls)
|
|
38
|
-
|
|
39
|
-
return super().__ror__(other)
|
|
33
|
+
if issubclass(cls, PluginModel) and (issubclass(other, PluginModel) or isinstance(other, PluganticCombinedModel)):
|
|
34
|
+
return PluganticCombinedOr(other, cls) # pyright: ignore[reportReturnType]
|
|
35
|
+
return NotImplemented
|
|
40
36
|
|
|
41
37
|
if TYPE_CHECKING:
|
|
42
38
|
_PluginModelMeta = type(BaseModel)
|
|
@@ -50,11 +46,11 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
50
46
|
__plugantic_was_schema_created__: ClassVar[bool] = False
|
|
51
47
|
__plugantic_check_schema_usage__: ClassVar[bool] = True
|
|
52
48
|
|
|
53
|
-
model_config: ClassVar[PluganticConfigDict] = PluganticConfigDict()
|
|
49
|
+
model_config: ClassVar[ConfigDict|PluganticConfigDict] = PluganticConfigDict(defer_build=True)
|
|
54
50
|
|
|
55
51
|
if not TYPE_CHECKING:
|
|
56
52
|
def __init__(self, *args, **kwargs):
|
|
57
|
-
declared_type = self.
|
|
53
|
+
declared_type = self._get_declared_types()[0] # inject the default discriminator value if not provided
|
|
58
54
|
if declared_type:
|
|
59
55
|
kwargs = {
|
|
60
56
|
self.__plugantic_varname_type__: declared_type,
|
|
@@ -64,7 +60,7 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
64
60
|
|
|
65
61
|
def __init_subclass__(cls, *,
|
|
66
62
|
varname_type: str|None=None,
|
|
67
|
-
value: str|None=None,
|
|
63
|
+
value: str|Collection[str]|None=None,
|
|
68
64
|
auto_downcast: bool|None=None,
|
|
69
65
|
downcast_order: int|None=None,
|
|
70
66
|
**kwargs):
|
|
@@ -89,7 +85,9 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
89
85
|
cls.__plugantic_varname_type__ = varname_type
|
|
90
86
|
|
|
91
87
|
if value is not None:
|
|
92
|
-
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
value = [value]
|
|
90
|
+
cls._create_annotation(cls.__plugantic_varname_type__, Literal[*value])
|
|
93
91
|
|
|
94
92
|
cls._ensure_varname_default()
|
|
95
93
|
|
|
@@ -134,10 +132,10 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
134
132
|
SomeConfig(type="something") # works
|
|
135
133
|
SomeConfig(type="else") # fails
|
|
136
134
|
"""
|
|
137
|
-
|
|
138
|
-
if not
|
|
135
|
+
declared_types = cls._get_declared_types()
|
|
136
|
+
if not declared_types:
|
|
139
137
|
return
|
|
140
|
-
cls._create_field_default(cls.__plugantic_varname_type__,
|
|
138
|
+
cls._create_field_default(cls.__plugantic_varname_type__, declared_types[0])
|
|
141
139
|
|
|
142
140
|
@classmethod
|
|
143
141
|
def _get_declared_annotation(cls, name: str):
|
|
@@ -153,18 +151,18 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
153
151
|
return annotation
|
|
154
152
|
|
|
155
153
|
@classmethod
|
|
156
|
-
def
|
|
154
|
+
def _get_declared_types(cls) -> Sequence[str]:
|
|
157
155
|
"""Get the value declared for the discriminator name (e.g. `type: Literal["something"]` -> "something")"""
|
|
158
156
|
field = cls._get_declared_annotation(cls.__plugantic_varname_type__)
|
|
159
157
|
|
|
160
158
|
if get_origin(field) is Literal:
|
|
161
|
-
return get_args(field)
|
|
159
|
+
return get_args(field)
|
|
162
160
|
|
|
163
|
-
return
|
|
161
|
+
return []
|
|
164
162
|
|
|
165
163
|
@classmethod
|
|
166
164
|
def _is_valid_subclass(cls) -> bool:
|
|
167
|
-
if cls.
|
|
165
|
+
if cls._get_declared_types():
|
|
168
166
|
return True
|
|
169
167
|
return False
|
|
170
168
|
|
|
@@ -185,20 +183,21 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
185
183
|
subclasses = set(cls._get_valid_subclasses())
|
|
186
184
|
if len(subclasses) == 1:
|
|
187
185
|
subcls = subclasses.pop()
|
|
188
|
-
subcls.
|
|
186
|
+
subcls._mark_schema_created()
|
|
189
187
|
return handler(subcls)
|
|
190
188
|
|
|
191
189
|
for subcls in subclasses:
|
|
192
|
-
subcls.
|
|
190
|
+
subcls._mark_schema_created()
|
|
193
191
|
|
|
194
192
|
choices = dict[str, Type[Self]]()
|
|
195
193
|
|
|
196
194
|
for subcls in subclasses:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
types = subcls._get_declared_types()
|
|
196
|
+
for type_ in types:
|
|
197
|
+
existing = choices.get(type_, None)
|
|
198
|
+
if existing:
|
|
199
|
+
subcls = existing.__plugantic_order__(subcls)
|
|
200
|
+
choices[type_] = subcls
|
|
202
201
|
|
|
203
202
|
choices = {
|
|
204
203
|
type_: handler(subcls)
|
|
@@ -209,7 +208,7 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
209
208
|
|
|
210
209
|
@classmethod
|
|
211
210
|
def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler):
|
|
212
|
-
cls.
|
|
211
|
+
cls._mark_schema_created()
|
|
213
212
|
return cls._as_tagged_union(handler)
|
|
214
213
|
|
|
215
214
|
@classmethod
|
|
@@ -227,7 +226,7 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
227
226
|
return cls
|
|
228
227
|
|
|
229
228
|
@classmethod
|
|
230
|
-
def
|
|
229
|
+
def _mark_schema_created(cls) -> None:
|
|
231
230
|
cls.__plugantic_was_schema_created__ = True
|
|
232
231
|
|
|
233
232
|
@classmethod
|
|
@@ -246,6 +245,7 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
246
245
|
return False
|
|
247
246
|
|
|
248
247
|
@model_validator(mode="wrap")
|
|
248
|
+
@classmethod
|
|
249
249
|
def _try_downcast(cls, data, handler):
|
|
250
250
|
if isinstance(data, cls):
|
|
251
251
|
pass
|
|
@@ -255,58 +255,59 @@ class PluginModel(BaseModel, metaclass=_PluginModelMeta):
|
|
|
255
255
|
except Exception as e:
|
|
256
256
|
raise ValueError(f"Failed to downcast given {repr(data)} to required {cls.__name__}; please provide the required config directly") from e
|
|
257
257
|
return handler(data)
|
|
258
|
-
|
|
259
|
-
model_config = {"defer_build": True}
|
|
260
258
|
|
|
261
259
|
class PluganticCombinedModel:
|
|
262
|
-
def __init__(self, *args: "PluganticCombinedModel|
|
|
260
|
+
def __init__(self, *args: "PluganticCombinedModel|Type[PluginModel]"):
|
|
263
261
|
self.items = args
|
|
264
262
|
|
|
265
|
-
def _get_valid_subclasses(self) -> Set[
|
|
263
|
+
def _get_valid_subclasses(self) -> Set[Type[PluginModel]]: ...
|
|
266
264
|
|
|
267
265
|
def __and__(self, other: Type):
|
|
268
266
|
if isinstance(other, PluganticCombinedModel) or issubclass(other, PluginModel):
|
|
269
267
|
return PluganticCombinedAnd(self, other)
|
|
270
|
-
return
|
|
268
|
+
return NotImplemented
|
|
271
269
|
|
|
272
270
|
def __rand__(self, other):
|
|
273
271
|
if isinstance(other, PluganticCombinedModel) or issubclass(other, PluginModel):
|
|
274
272
|
return PluganticCombinedAnd(other, self)
|
|
275
|
-
return
|
|
273
|
+
return NotImplemented
|
|
276
274
|
|
|
277
275
|
def __or__(self, other):
|
|
278
276
|
if isinstance(other, PluganticCombinedModel) or issubclass(other, PluginModel):
|
|
279
277
|
return PluganticCombinedOr(self, other)
|
|
280
|
-
return
|
|
278
|
+
return NotImplemented
|
|
281
279
|
|
|
282
280
|
def __ror__(self, other):
|
|
283
281
|
if isinstance(other, PluganticCombinedModel) or issubclass(other, PluginModel):
|
|
284
282
|
return PluganticCombinedOr(other, self)
|
|
285
|
-
return
|
|
283
|
+
return NotImplemented
|
|
286
284
|
|
|
287
285
|
def __get_pydantic_core_schema__(self, source, handler: GetCoreSchemaHandler):
|
|
288
286
|
subclasses = set(self._get_valid_subclasses())
|
|
289
287
|
if len(subclasses) == 1:
|
|
290
288
|
subcls = subclasses.pop()
|
|
291
|
-
subcls.
|
|
289
|
+
subcls._mark_schema_created()
|
|
292
290
|
return handler(subcls)
|
|
293
291
|
|
|
294
292
|
choices = dict[str, dict[str, Type[PluginModel]]]()
|
|
295
293
|
for subcls in subclasses:
|
|
296
|
-
subcls.
|
|
294
|
+
subcls._mark_schema_created()
|
|
297
295
|
varname = subcls.__plugantic_varname_type__
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
if varname is None:
|
|
297
|
+
continue
|
|
298
|
+
types = subcls._get_declared_types()
|
|
299
|
+
for type_ in types:
|
|
300
|
+
existing = choices.setdefault(varname, {}).get(type_, None)
|
|
301
|
+
if existing:
|
|
302
|
+
subcls = existing.__plugantic_order__(subcls)
|
|
303
|
+
choices[varname][type_] = subcls
|
|
303
304
|
|
|
304
305
|
choices = {
|
|
305
306
|
varname: {type_: handler(subcls) for type_, subcls in types.items()}
|
|
306
307
|
for varname, types in choices.items()
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
unions = [
|
|
310
|
+
unions: list = [
|
|
310
311
|
tagged_union_schema(c, discriminator=d) for d, c in choices.items()
|
|
311
312
|
]
|
|
312
313
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
|
-
from plugantic import PluginModel
|
|
2
|
+
from plugantic import PluginModel, Field
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
def test_auto_downcast():
|
|
@@ -10,11 +10,11 @@ def test_auto_downcast():
|
|
|
10
10
|
pass
|
|
11
11
|
|
|
12
12
|
class Impl1(Base):
|
|
13
|
-
type: Literal["impl1"]
|
|
13
|
+
type: Literal["impl1"] = Field(default=...)
|
|
14
14
|
value: str|None
|
|
15
15
|
|
|
16
16
|
class Impl2(Impl1, Feature1):
|
|
17
|
-
value: str
|
|
17
|
+
value: str # pyright: ignore[reportIncompatibleVariableOverride]
|
|
18
18
|
|
|
19
19
|
class Impl3(Impl1):
|
|
20
20
|
pass
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing_extensions import Literal
|
|
2
|
-
from plugantic import PluginModel
|
|
2
|
+
from plugantic import PluginModel, Field
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
def test_basic_usage_subclass_args():
|
|
@@ -13,7 +13,7 @@ def test_basic_usage_subclass_args():
|
|
|
13
13
|
number: int|None = None
|
|
14
14
|
|
|
15
15
|
class TestImplNumberStrict(TestImplNumber, value="number-strict"):
|
|
16
|
-
number: int = 0
|
|
16
|
+
number: int = 0 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
17
17
|
|
|
18
18
|
class OtherConfig(BaseModel):
|
|
19
19
|
config: TestBase
|
|
@@ -50,7 +50,7 @@ def test_basic_usage_subclass_annotated():
|
|
|
50
50
|
value: str
|
|
51
51
|
|
|
52
52
|
class TestImplText(TestBase):
|
|
53
|
-
type: Literal["text"]
|
|
53
|
+
type: Literal["text"] = Field(default=...)
|
|
54
54
|
text: str
|
|
55
55
|
|
|
56
56
|
class TestImplNumber(TestBase):
|
|
@@ -58,15 +58,15 @@ def test_basic_usage_subclass_annotated():
|
|
|
58
58
|
number: int|None = None
|
|
59
59
|
|
|
60
60
|
class TestImplNumberStrict(TestImplNumber):
|
|
61
|
-
type: Literal["number-strict"]
|
|
62
|
-
number: int = 0
|
|
61
|
+
type: Literal["number-strict"] # pyright: ignore[reportIncompatibleVariableOverride]
|
|
62
|
+
number: int = 0 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
63
63
|
|
|
64
64
|
class OtherConfig(BaseModel):
|
|
65
65
|
config: TestBase
|
|
66
66
|
|
|
67
67
|
OtherConfig(config=TestImplText(value="some text", text="other text"))
|
|
68
|
-
OtherConfig(config=TestImplNumber(value="some number"))
|
|
69
|
-
OtherConfig(config=TestImplNumberStrict(value="strict number", number=3))
|
|
68
|
+
OtherConfig(config=TestImplNumber(value="some number")) # pyright: ignore[reportCallIssue]
|
|
69
|
+
OtherConfig(config=TestImplNumberStrict(value="strict number", number=3)) # pyright: ignore[reportCallIssue]
|
|
70
70
|
|
|
71
71
|
c1 = OtherConfig.model_validate({"config": {
|
|
72
72
|
"type": "text",
|
|
@@ -104,7 +104,7 @@ def test_basic_usage_subclass_config():
|
|
|
104
104
|
model_config = {"value": "number"}
|
|
105
105
|
|
|
106
106
|
class TestImplNumberStrict(TestImplNumber):
|
|
107
|
-
number: int = 0
|
|
107
|
+
number: int = 0 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
108
108
|
model_config = {"value": "number-strict"}
|
|
109
109
|
|
|
110
110
|
class OtherConfig(BaseModel):
|
|
@@ -137,3 +137,58 @@ def test_basic_usage_subclass_config():
|
|
|
137
137
|
assert not isinstance(c2.config, TestImplNumberStrict)
|
|
138
138
|
assert isinstance(c3.config, TestImplNumberStrict)
|
|
139
139
|
|
|
140
|
+
def test_basic_usage_multiple_values():
|
|
141
|
+
class TestBase(PluginModel, varname_type="type"):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
class TestImplText(TestBase, value=["text", "str"]):
|
|
145
|
+
text: str
|
|
146
|
+
|
|
147
|
+
class TestImplNumber(TestBase):
|
|
148
|
+
number: int
|
|
149
|
+
model_config = {"value": ["number", "num"]}
|
|
150
|
+
|
|
151
|
+
class TestImplEmpty(TestBase):
|
|
152
|
+
type: Literal["empty", "none"] = Field(default=...)
|
|
153
|
+
|
|
154
|
+
class OtherConfig(BaseModel):
|
|
155
|
+
config: TestBase
|
|
156
|
+
|
|
157
|
+
OtherConfig(config=TestImplText(text="other text"))
|
|
158
|
+
OtherConfig(config=TestImplNumber(number=3))
|
|
159
|
+
OtherConfig(config=TestImplEmpty())
|
|
160
|
+
|
|
161
|
+
c1 = OtherConfig.model_validate({"config": {
|
|
162
|
+
"type": "text",
|
|
163
|
+
"text": "some text",
|
|
164
|
+
}})
|
|
165
|
+
|
|
166
|
+
c2 = OtherConfig.model_validate({"config": {
|
|
167
|
+
"type": "str",
|
|
168
|
+
"text": "other text",
|
|
169
|
+
}})
|
|
170
|
+
|
|
171
|
+
c3 = OtherConfig.model_validate({"config": {
|
|
172
|
+
"type": "number",
|
|
173
|
+
"number": 3,
|
|
174
|
+
}})
|
|
175
|
+
|
|
176
|
+
c4 = OtherConfig.model_validate({"config": {
|
|
177
|
+
"type": "num",
|
|
178
|
+
"number": 3,
|
|
179
|
+
}})
|
|
180
|
+
|
|
181
|
+
c5 = OtherConfig.model_validate({"config": {
|
|
182
|
+
"type": "empty",
|
|
183
|
+
}})
|
|
184
|
+
|
|
185
|
+
c6 = OtherConfig.model_validate({"config": {
|
|
186
|
+
"type": "none",
|
|
187
|
+
}})
|
|
188
|
+
|
|
189
|
+
assert isinstance(c1.config, TestImplText)
|
|
190
|
+
assert isinstance(c2.config, TestImplText)
|
|
191
|
+
assert isinstance(c3.config, TestImplNumber)
|
|
192
|
+
assert isinstance(c4.config, TestImplNumber)
|
|
193
|
+
assert isinstance(c5.config, TestImplEmpty)
|
|
194
|
+
assert isinstance(c6.config, TestImplEmpty)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing_extensions import TypeVar, Iterable, TypeGuard, TypeAliasType, Callable
|
|
4
|
-
from itertools import combinations, product
|
|
5
|
-
|
|
6
|
-
T = TypeVar("T")
|
|
7
|
-
RecursiveList = TypeAliasType("RecursiveList", Iterable["RecursiveListItem[T]"], type_params=(T,))
|
|
8
|
-
RecursiveListItem = TypeAliasType("RecursiveListItem", T | RecursiveList[T], type_params=(T,))
|
|
9
|
-
|
|
10
|
-
def recursive_linear(items: RecursiveList[T], typeguard: Callable[[RecursiveListItem[T]], TypeGuard[T]], join: Callable[[Iterable[T]], T]) -> Iterable[T]:
|
|
11
|
-
result = []
|
|
12
|
-
for item in items:
|
|
13
|
-
if typeguard(item):
|
|
14
|
-
result.append(item)
|
|
15
|
-
else:
|
|
16
|
-
result.extend(recursive_powerset(item, typeguard, join))
|
|
17
|
-
return result
|
|
18
|
-
|
|
19
|
-
def recursive_powerset(items: RecursiveList[T], typeguard: Callable[[RecursiveListItem[T]], TypeGuard[T]], join: Callable[[Iterable[T]], T]) -> Iterable[T]:
|
|
20
|
-
arbitrary_subset = []
|
|
21
|
-
subsets = []
|
|
22
|
-
for downcast in items:
|
|
23
|
-
if typeguard(downcast):
|
|
24
|
-
arbitrary_subset.append(downcast)
|
|
25
|
-
else:
|
|
26
|
-
linear_subset = recursive_linear(downcast, typeguard, join)
|
|
27
|
-
linear_subset = ((), *((d,) for d in linear_subset))
|
|
28
|
-
subsets.append(linear_subset)
|
|
29
|
-
|
|
30
|
-
arbitrary_powerset = [()]
|
|
31
|
-
for l in range(1, len(arbitrary_subset) + 1):
|
|
32
|
-
arbitrary_powerset.extend(combinations(arbitrary_subset, l))
|
|
33
|
-
subsets.append(arbitrary_powerset)
|
|
34
|
-
|
|
35
|
-
powerset = []
|
|
36
|
-
for subset in product(*subsets):
|
|
37
|
-
callbacks = [c for s in subset for c in s]
|
|
38
|
-
if not callbacks:
|
|
39
|
-
continue
|
|
40
|
-
powerset.append(join(callbacks))
|
|
41
|
-
|
|
42
|
-
return powerset
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|