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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugantic
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Simplified extendable composition with pydantic
5
5
  Author-email: Martin Kunze <martin@martinkunze.com>
6
6
  Project-URL: Homepage, https://github.com/martinkunze/plugantic
@@ -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.3"
2
+ version = "0.2.4"
@@ -1,4 +1,4 @@
1
- from typing_extensions import ClassVar, Type, Self, Literal, Any, TypeVar, Set, Self, get_type_hints, get_origin, get_args, TYPE_CHECKING
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._get_declared_type() # inject the default discriminator value if not provided
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
- cls._create_annotation(cls.__plugantic_varname_type__, Literal[value])
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
- declared_type = cls._get_declared_type()
138
- if not declared_type:
135
+ declared_types = cls._get_declared_types()
136
+ if not declared_types:
139
137
  return
140
- cls._create_field_default(cls.__plugantic_varname_type__, declared_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 _get_declared_type(cls) -> str|None:
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)[0]
159
+ return get_args(field)
162
160
 
163
- return None
161
+ return []
164
162
 
165
163
  @classmethod
166
164
  def _is_valid_subclass(cls) -> bool:
167
- if cls._get_declared_type():
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._mark_schame_created()
186
+ subcls._mark_schema_created()
189
187
  return handler(subcls)
190
188
 
191
189
  for subcls in subclasses:
192
- subcls._mark_schame_created()
190
+ subcls._mark_schema_created()
193
191
 
194
192
  choices = dict[str, Type[Self]]()
195
193
 
196
194
  for subcls in subclasses:
197
- type_ = subcls._get_declared_type()
198
- existing = choices.get(type_, None)
199
- if existing:
200
- subcls = existing.__plugantic_order__(subcls)
201
- choices[type_] = subcls
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._mark_schame_created()
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 _mark_schame_created(cls) -> None:
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|type[PluginModel]"):
260
+ def __init__(self, *args: "PluganticCombinedModel|Type[PluginModel]"):
263
261
  self.items = args
264
262
 
265
- def _get_valid_subclasses(self) -> Set[type[PluginModel]]: ...
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 super().__and__(other)
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 super().__rand__(other)
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 super().__or__(other)
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 super().__ror__(other)
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._mark_schame_created()
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._mark_schame_created()
294
+ subcls._mark_schema_created()
297
295
  varname = subcls.__plugantic_varname_type__
298
- type_ = subcls._get_declared_type()
299
- existing = choices.setdefault(varname, {}).get(type_, None)
300
- if existing:
301
- subcls = existing.__plugantic_order__(subcls)
302
- choices[varname][type_] = subcls
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugantic
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Simplified extendable composition with pydantic
5
5
  Author-email: Martin Kunze <martin@martinkunze.com>
6
6
  Project-URL: Homepage, https://github.com/martinkunze/plugantic
@@ -3,7 +3,6 @@ README.md
3
3
  pyproject.toml
4
4
  src/plugantic/__init__.py
5
5
  src/plugantic/_consts.py
6
- src/plugantic/_helpers.py
7
6
  src/plugantic/_types.py
8
7
  src/plugantic/plugin.py
9
8
  src/plugantic.egg-info/PKG-INFO
@@ -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