enumetyped 0.3.2__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.
- enumetyped-0.3.2/LICENSE +21 -0
- enumetyped-0.3.2/PKG-INFO +220 -0
- enumetyped-0.3.2/README.md +192 -0
- enumetyped-0.3.2/enumetyped/__init__.py +11 -0
- enumetyped-0.3.2/enumetyped/core.py +144 -0
- enumetyped-0.3.2/enumetyped/py.typed +0 -0
- enumetyped-0.3.2/enumetyped/pydantic/__init__.py +18 -0
- enumetyped-0.3.2/enumetyped/pydantic/core.py +158 -0
- enumetyped-0.3.2/enumetyped/pydantic/serialization/__init__.py +9 -0
- enumetyped-0.3.2/enumetyped/pydantic/serialization/adjacently.py +116 -0
- enumetyped-0.3.2/enumetyped/pydantic/serialization/externally.py +110 -0
- enumetyped-0.3.2/enumetyped/pydantic/serialization/internally.py +140 -0
- enumetyped-0.3.2/enumetyped/pydantic/serialization/tagged.py +43 -0
- enumetyped-0.3.2/pyproject.toml +50 -0
enumetyped-0.3.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: enumetyped
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Type-containing enumeration
|
|
5
|
+
Home-page: https://github.com/rjotanm/enumetyped
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Rinat Balbekov
|
|
8
|
+
Author-email: me@rjotanm.dev
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Pydantic
|
|
12
|
+
Classifier: Framework :: Pydantic :: 2
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Provides-Extra: pydantic
|
|
24
|
+
Requires-Dist: pydantic (>=2.9.0) ; extra == "pydantic"
|
|
25
|
+
Requires-Dist: typing-extensions (>=4.0.0)
|
|
26
|
+
Project-URL: Repository, https://github.com/rjotanm/enumetyped
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Enumetyped
|
|
30
|
+
|
|
31
|
+
This package provide a way to create typed enumerations.
|
|
32
|
+
|
|
33
|
+
# Install
|
|
34
|
+
|
|
35
|
+
- `pip install enumetyped`
|
|
36
|
+
- `pip install enumetyped[pydantic]` - install with pydantic `>=2.9`
|
|
37
|
+
|
|
38
|
+
# Quickstart
|
|
39
|
+
|
|
40
|
+
#### Without pydantic
|
|
41
|
+
```python
|
|
42
|
+
from enumetyped import TypEnum, TypEnumContent
|
|
43
|
+
|
|
44
|
+
class SimpleEnum(TypEnum[TypEnumContent]):
|
|
45
|
+
A: type["SimpleEnum[NoValue]"]
|
|
46
|
+
Int: type["SimpleEnum[int]"]
|
|
47
|
+
|
|
48
|
+
# isinstance checking
|
|
49
|
+
assert isinstance(SimpleEnum.A(...), SimpleEnum)
|
|
50
|
+
assert isinstance(SimpleEnum.Int(123), SimpleEnum.Int)
|
|
51
|
+
assert not isinstance(SimpleEnum.Int(123), SimpleEnum.A)
|
|
52
|
+
|
|
53
|
+
# fully pattern-matching
|
|
54
|
+
match SimpleEnum.Int(1):
|
|
55
|
+
case SimpleEnum.Int(2):
|
|
56
|
+
a = False
|
|
57
|
+
case SimpleEnum.Int(1):
|
|
58
|
+
a = True
|
|
59
|
+
case SimpleEnum.Int():
|
|
60
|
+
a = True
|
|
61
|
+
case _:
|
|
62
|
+
a = False
|
|
63
|
+
|
|
64
|
+
assert a
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### With pydantic
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import typing
|
|
71
|
+
from dataclasses import dataclass
|
|
72
|
+
|
|
73
|
+
from pydantic import BaseModel
|
|
74
|
+
|
|
75
|
+
from enumetyped import TypEnum, TypEnumContent, NoValue
|
|
76
|
+
from enumetyped.pydantic import TypEnumPydantic, FieldMetadata, Rename
|
|
77
|
+
from typing_extensions import Annotated, TypedDict
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Enum(TypEnum[NoValue]):
|
|
81
|
+
V1: type["Enum"]
|
|
82
|
+
V2: type["Enum"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class TestDataClass:
|
|
87
|
+
a: int
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestModel(BaseModel):
|
|
91
|
+
b: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestTypedDict(TypedDict):
|
|
95
|
+
tm: TestModel
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SimpleEnum(TypEnumPydantic[NoValue]):
|
|
99
|
+
V1: type["SimpleEnum"]
|
|
100
|
+
V2: type["SimpleEnum"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OtherEnum(TypEnumPydantic[TypEnumContent]):
|
|
104
|
+
Int: type["OtherEnum[int]"]
|
|
105
|
+
Int: type["OtherEnum[str]"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# class MyEnum(TypEnumPydantic[TypEnumContent], variant="key", content="value"): <- adjacently
|
|
109
|
+
# class MyEnum(TypEnumPydantic[TypEnumContent], variant="key"): <- internally
|
|
110
|
+
class MyEnum(TypEnumPydantic[TypEnumContent]): # <- externally, default
|
|
111
|
+
# MyEnum.Int(123)
|
|
112
|
+
Int: type["MyEnum[int]"]
|
|
113
|
+
|
|
114
|
+
# MyEnum.Str(123)
|
|
115
|
+
Str: type["MyEnum[str]"]
|
|
116
|
+
|
|
117
|
+
# MyEnum.Str(OtherEnum.Int(1))
|
|
118
|
+
Other: type["MyEnum[OtherEnum[Any]]"] # any from OtherEnum variants
|
|
119
|
+
|
|
120
|
+
# MyEnum.Str(MyEnum.Int(1)) | MyEnum.Str(MyEnum.Str(1))
|
|
121
|
+
Self: type["MyEnum[MyEnum[Any]]"] # any from self variants
|
|
122
|
+
|
|
123
|
+
# MyEnum.OnlySelf(...) - any parameters skipped, serialized just by name
|
|
124
|
+
NoValue: type["MyEnum[NoValue]"]
|
|
125
|
+
|
|
126
|
+
# MyEnum.OnlySelf2(None)
|
|
127
|
+
Optional: type["MyEnum[Optional[bool]]"]
|
|
128
|
+
|
|
129
|
+
# MyEnum.List(["1", "2", "3"])
|
|
130
|
+
List: type["MyEnum[list[str]]"]
|
|
131
|
+
|
|
132
|
+
# MyEnum.Dict({"key": "value"})
|
|
133
|
+
Dict: type["MyEnum[dict[str, str]]"]
|
|
134
|
+
# TypedDict: type["MyEnum[{"b": str}]"]
|
|
135
|
+
TypedDict: type["MyEnum[TestD]"] # python doesn`t have inline TypedDict now
|
|
136
|
+
|
|
137
|
+
# MyEnum.DC(TestDataClass(a=1))
|
|
138
|
+
DataClass: type["MyEnum[TestDataClass]"]
|
|
139
|
+
|
|
140
|
+
# MyEnum.Model(TestModel(b="2"))
|
|
141
|
+
Model: type["MyEnum[TestModel]"]
|
|
142
|
+
|
|
143
|
+
# MyEnum.StrTuple(("1", "2")))
|
|
144
|
+
StringTuple: Annotated[type["MyEnum[tuple[str, str]]"], FieldMetadata(rename="just_str_tuple")]
|
|
145
|
+
# or use enumetyped.pydantic.Rename
|
|
146
|
+
# StrTuple: Annotated[type["MyEnum[tuple[str, str]]"], Rename("some_other_name")]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FinModel(BaseModel):
|
|
150
|
+
enum: MyEnum
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def dump_and_load(e: MyEnum):
|
|
154
|
+
model = FinModel(enum=e)
|
|
155
|
+
json_ = model.model_dump_json()
|
|
156
|
+
print(json_)
|
|
157
|
+
restored = FinModel.model_validate_json(json_)
|
|
158
|
+
assert model == restored
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# externally -> {"enum":{"Int":1}}
|
|
162
|
+
# adjacently -> {"enum":{"key":"Int","value":1}}
|
|
163
|
+
# internally -> not supported
|
|
164
|
+
dump_and_load(MyEnum.Int(1))
|
|
165
|
+
|
|
166
|
+
# externally -> {"enum":{"Str":"str"}}
|
|
167
|
+
# adjacently -> {"enum":{"key":"Str","value":"str"}}
|
|
168
|
+
# internally -> not supported
|
|
169
|
+
dump_and_load(MyEnum.Str("str"))
|
|
170
|
+
|
|
171
|
+
# externally -> {"enum":{"List":["list"]}}
|
|
172
|
+
# adjacently -> {"enum":{"key":"List","value":["list"]}}
|
|
173
|
+
# internally -> not supported
|
|
174
|
+
dump_and_load(MyEnum.List(["list"]))
|
|
175
|
+
|
|
176
|
+
# externally -> {"enum":{"just_str_tuple":["str","str2"]}}
|
|
177
|
+
# adjacently -> {"enum":{"key":"just_str_tuple","value":["str","str2"]}}
|
|
178
|
+
# internally -> not supported
|
|
179
|
+
dump_and_load(MyEnum.StringTuple(("str", "str2")))
|
|
180
|
+
|
|
181
|
+
# externally -> {"enum":{"Self":{"Int":1}}}
|
|
182
|
+
# adjacently -> {"enum":{"key":"Self","value":{"key":"Int","value":1}}}
|
|
183
|
+
# internally -> not supported
|
|
184
|
+
dump_and_load(MyEnum.Self(MyEnum.Int(1)))
|
|
185
|
+
|
|
186
|
+
# externally -> {"enum":{"DC":{"a":1}}}
|
|
187
|
+
# adjacently -> {"enum":{"key":"DC","value":{"a":1}}}
|
|
188
|
+
# internally -> {"enum":{"key":"DC","a":1}}}
|
|
189
|
+
dump_and_load(MyEnum.DataClass(TestDataClass(a=1)))
|
|
190
|
+
|
|
191
|
+
# externally -> {"enum":{"Model":{"b":"test_model"}}}
|
|
192
|
+
# adjacently -> {"enum":{"key":"Model","value":{"b":"test_model"}}}
|
|
193
|
+
# internally -> {"enum":{"key":"Model", "b":"test_model"}}
|
|
194
|
+
dump_and_load(MyEnum.Model(TestModel(b="test_model")))
|
|
195
|
+
|
|
196
|
+
# externally -> {"enum":{"TypedDict":{"tm":{"b":"test_model"}}}}
|
|
197
|
+
# adjacently -> {"enum":{"key":"TypedDict","value":{"tm":{"b":"test_model"}}}}
|
|
198
|
+
# internally -> {"enum":{"key":"TypedDict","tm":{"b":"test_model"}}}
|
|
199
|
+
dump_and_load(MyEnum.TypedDict(TestTypedDict(tm=TestModel(b="test_model"))))
|
|
200
|
+
|
|
201
|
+
# externally -> {"enum":{"Dict":{"a":"1","b":"2"}}}
|
|
202
|
+
# adjacently -> {"enum":{"key":"Dict","value":{"a":"1","b":"2"}}}
|
|
203
|
+
# internally -> not supported
|
|
204
|
+
dump_and_load(MyEnum.Dict({"a": "1", "b": "2"}))
|
|
205
|
+
|
|
206
|
+
# externally -> {"enum":"NoValue"}
|
|
207
|
+
# adjacently -> {"enum":{"key":"NoValue"}}
|
|
208
|
+
# internally -> {"enum":{"key":"NoValue"}}
|
|
209
|
+
dump_and_load(MyEnum.NoValue(...))
|
|
210
|
+
|
|
211
|
+
# externally -> {"enum":{"Optional":null}}
|
|
212
|
+
# adjacently -> {"enum":{"key":"Optional","value":null}}
|
|
213
|
+
# internally -> not supported
|
|
214
|
+
dump_and_load(MyEnum.Optional(None))
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Other
|
|
218
|
+
|
|
219
|
+
- [Compatibility](docs/compatibility.md)
|
|
220
|
+
- [Limitations](docs/limitations.md)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Enumetyped
|
|
2
|
+
|
|
3
|
+
This package provide a way to create typed enumerations.
|
|
4
|
+
|
|
5
|
+
# Install
|
|
6
|
+
|
|
7
|
+
- `pip install enumetyped`
|
|
8
|
+
- `pip install enumetyped[pydantic]` - install with pydantic `>=2.9`
|
|
9
|
+
|
|
10
|
+
# Quickstart
|
|
11
|
+
|
|
12
|
+
#### Without pydantic
|
|
13
|
+
```python
|
|
14
|
+
from enumetyped import TypEnum, TypEnumContent
|
|
15
|
+
|
|
16
|
+
class SimpleEnum(TypEnum[TypEnumContent]):
|
|
17
|
+
A: type["SimpleEnum[NoValue]"]
|
|
18
|
+
Int: type["SimpleEnum[int]"]
|
|
19
|
+
|
|
20
|
+
# isinstance checking
|
|
21
|
+
assert isinstance(SimpleEnum.A(...), SimpleEnum)
|
|
22
|
+
assert isinstance(SimpleEnum.Int(123), SimpleEnum.Int)
|
|
23
|
+
assert not isinstance(SimpleEnum.Int(123), SimpleEnum.A)
|
|
24
|
+
|
|
25
|
+
# fully pattern-matching
|
|
26
|
+
match SimpleEnum.Int(1):
|
|
27
|
+
case SimpleEnum.Int(2):
|
|
28
|
+
a = False
|
|
29
|
+
case SimpleEnum.Int(1):
|
|
30
|
+
a = True
|
|
31
|
+
case SimpleEnum.Int():
|
|
32
|
+
a = True
|
|
33
|
+
case _:
|
|
34
|
+
a = False
|
|
35
|
+
|
|
36
|
+
assert a
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### With pydantic
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import typing
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
|
|
45
|
+
from pydantic import BaseModel
|
|
46
|
+
|
|
47
|
+
from enumetyped import TypEnum, TypEnumContent, NoValue
|
|
48
|
+
from enumetyped.pydantic import TypEnumPydantic, FieldMetadata, Rename
|
|
49
|
+
from typing_extensions import Annotated, TypedDict
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Enum(TypEnum[NoValue]):
|
|
53
|
+
V1: type["Enum"]
|
|
54
|
+
V2: type["Enum"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class TestDataClass:
|
|
59
|
+
a: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestModel(BaseModel):
|
|
63
|
+
b: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestTypedDict(TypedDict):
|
|
67
|
+
tm: TestModel
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SimpleEnum(TypEnumPydantic[NoValue]):
|
|
71
|
+
V1: type["SimpleEnum"]
|
|
72
|
+
V2: type["SimpleEnum"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OtherEnum(TypEnumPydantic[TypEnumContent]):
|
|
76
|
+
Int: type["OtherEnum[int]"]
|
|
77
|
+
Int: type["OtherEnum[str]"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# class MyEnum(TypEnumPydantic[TypEnumContent], variant="key", content="value"): <- adjacently
|
|
81
|
+
# class MyEnum(TypEnumPydantic[TypEnumContent], variant="key"): <- internally
|
|
82
|
+
class MyEnum(TypEnumPydantic[TypEnumContent]): # <- externally, default
|
|
83
|
+
# MyEnum.Int(123)
|
|
84
|
+
Int: type["MyEnum[int]"]
|
|
85
|
+
|
|
86
|
+
# MyEnum.Str(123)
|
|
87
|
+
Str: type["MyEnum[str]"]
|
|
88
|
+
|
|
89
|
+
# MyEnum.Str(OtherEnum.Int(1))
|
|
90
|
+
Other: type["MyEnum[OtherEnum[Any]]"] # any from OtherEnum variants
|
|
91
|
+
|
|
92
|
+
# MyEnum.Str(MyEnum.Int(1)) | MyEnum.Str(MyEnum.Str(1))
|
|
93
|
+
Self: type["MyEnum[MyEnum[Any]]"] # any from self variants
|
|
94
|
+
|
|
95
|
+
# MyEnum.OnlySelf(...) - any parameters skipped, serialized just by name
|
|
96
|
+
NoValue: type["MyEnum[NoValue]"]
|
|
97
|
+
|
|
98
|
+
# MyEnum.OnlySelf2(None)
|
|
99
|
+
Optional: type["MyEnum[Optional[bool]]"]
|
|
100
|
+
|
|
101
|
+
# MyEnum.List(["1", "2", "3"])
|
|
102
|
+
List: type["MyEnum[list[str]]"]
|
|
103
|
+
|
|
104
|
+
# MyEnum.Dict({"key": "value"})
|
|
105
|
+
Dict: type["MyEnum[dict[str, str]]"]
|
|
106
|
+
# TypedDict: type["MyEnum[{"b": str}]"]
|
|
107
|
+
TypedDict: type["MyEnum[TestD]"] # python doesn`t have inline TypedDict now
|
|
108
|
+
|
|
109
|
+
# MyEnum.DC(TestDataClass(a=1))
|
|
110
|
+
DataClass: type["MyEnum[TestDataClass]"]
|
|
111
|
+
|
|
112
|
+
# MyEnum.Model(TestModel(b="2"))
|
|
113
|
+
Model: type["MyEnum[TestModel]"]
|
|
114
|
+
|
|
115
|
+
# MyEnum.StrTuple(("1", "2")))
|
|
116
|
+
StringTuple: Annotated[type["MyEnum[tuple[str, str]]"], FieldMetadata(rename="just_str_tuple")]
|
|
117
|
+
# or use enumetyped.pydantic.Rename
|
|
118
|
+
# StrTuple: Annotated[type["MyEnum[tuple[str, str]]"], Rename("some_other_name")]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class FinModel(BaseModel):
|
|
122
|
+
enum: MyEnum
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def dump_and_load(e: MyEnum):
|
|
126
|
+
model = FinModel(enum=e)
|
|
127
|
+
json_ = model.model_dump_json()
|
|
128
|
+
print(json_)
|
|
129
|
+
restored = FinModel.model_validate_json(json_)
|
|
130
|
+
assert model == restored
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# externally -> {"enum":{"Int":1}}
|
|
134
|
+
# adjacently -> {"enum":{"key":"Int","value":1}}
|
|
135
|
+
# internally -> not supported
|
|
136
|
+
dump_and_load(MyEnum.Int(1))
|
|
137
|
+
|
|
138
|
+
# externally -> {"enum":{"Str":"str"}}
|
|
139
|
+
# adjacently -> {"enum":{"key":"Str","value":"str"}}
|
|
140
|
+
# internally -> not supported
|
|
141
|
+
dump_and_load(MyEnum.Str("str"))
|
|
142
|
+
|
|
143
|
+
# externally -> {"enum":{"List":["list"]}}
|
|
144
|
+
# adjacently -> {"enum":{"key":"List","value":["list"]}}
|
|
145
|
+
# internally -> not supported
|
|
146
|
+
dump_and_load(MyEnum.List(["list"]))
|
|
147
|
+
|
|
148
|
+
# externally -> {"enum":{"just_str_tuple":["str","str2"]}}
|
|
149
|
+
# adjacently -> {"enum":{"key":"just_str_tuple","value":["str","str2"]}}
|
|
150
|
+
# internally -> not supported
|
|
151
|
+
dump_and_load(MyEnum.StringTuple(("str", "str2")))
|
|
152
|
+
|
|
153
|
+
# externally -> {"enum":{"Self":{"Int":1}}}
|
|
154
|
+
# adjacently -> {"enum":{"key":"Self","value":{"key":"Int","value":1}}}
|
|
155
|
+
# internally -> not supported
|
|
156
|
+
dump_and_load(MyEnum.Self(MyEnum.Int(1)))
|
|
157
|
+
|
|
158
|
+
# externally -> {"enum":{"DC":{"a":1}}}
|
|
159
|
+
# adjacently -> {"enum":{"key":"DC","value":{"a":1}}}
|
|
160
|
+
# internally -> {"enum":{"key":"DC","a":1}}}
|
|
161
|
+
dump_and_load(MyEnum.DataClass(TestDataClass(a=1)))
|
|
162
|
+
|
|
163
|
+
# externally -> {"enum":{"Model":{"b":"test_model"}}}
|
|
164
|
+
# adjacently -> {"enum":{"key":"Model","value":{"b":"test_model"}}}
|
|
165
|
+
# internally -> {"enum":{"key":"Model", "b":"test_model"}}
|
|
166
|
+
dump_and_load(MyEnum.Model(TestModel(b="test_model")))
|
|
167
|
+
|
|
168
|
+
# externally -> {"enum":{"TypedDict":{"tm":{"b":"test_model"}}}}
|
|
169
|
+
# adjacently -> {"enum":{"key":"TypedDict","value":{"tm":{"b":"test_model"}}}}
|
|
170
|
+
# internally -> {"enum":{"key":"TypedDict","tm":{"b":"test_model"}}}
|
|
171
|
+
dump_and_load(MyEnum.TypedDict(TestTypedDict(tm=TestModel(b="test_model"))))
|
|
172
|
+
|
|
173
|
+
# externally -> {"enum":{"Dict":{"a":"1","b":"2"}}}
|
|
174
|
+
# adjacently -> {"enum":{"key":"Dict","value":{"a":"1","b":"2"}}}
|
|
175
|
+
# internally -> not supported
|
|
176
|
+
dump_and_load(MyEnum.Dict({"a": "1", "b": "2"}))
|
|
177
|
+
|
|
178
|
+
# externally -> {"enum":"NoValue"}
|
|
179
|
+
# adjacently -> {"enum":{"key":"NoValue"}}
|
|
180
|
+
# internally -> {"enum":{"key":"NoValue"}}
|
|
181
|
+
dump_and_load(MyEnum.NoValue(...))
|
|
182
|
+
|
|
183
|
+
# externally -> {"enum":{"Optional":null}}
|
|
184
|
+
# adjacently -> {"enum":{"key":"Optional","value":null}}
|
|
185
|
+
# internally -> not supported
|
|
186
|
+
dump_and_load(MyEnum.Optional(None))
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Other
|
|
190
|
+
|
|
191
|
+
- [Compatibility](docs/compatibility.md)
|
|
192
|
+
- [Limitations](docs/limitations.md)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import types
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"NoValue",
|
|
6
|
+
"TypEnum",
|
|
7
|
+
"TypEnumContent",
|
|
8
|
+
"TypEnumMeta",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
import typing_extensions
|
|
12
|
+
|
|
13
|
+
from annotated_types import BaseMetadata
|
|
14
|
+
from typing_extensions import Annotated
|
|
15
|
+
|
|
16
|
+
TypEnumContent = typing.TypeVar("TypEnumContent")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
NoValue = types.EllipsisType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TypEnumMeta(type):
|
|
23
|
+
__full_variant_name__: str
|
|
24
|
+
__variant_name__: str
|
|
25
|
+
|
|
26
|
+
__content_type__: typing.Union[str, type[typing.Any]]
|
|
27
|
+
|
|
28
|
+
__variants__: dict[type['_TypEnum[typing.Any]'], str]
|
|
29
|
+
|
|
30
|
+
__is_variant__: bool = False
|
|
31
|
+
|
|
32
|
+
def __new__(
|
|
33
|
+
cls,
|
|
34
|
+
cls_name: str,
|
|
35
|
+
bases: tuple[typing.Any],
|
|
36
|
+
class_dict: dict[str, typing.Any],
|
|
37
|
+
) -> typing.Any:
|
|
38
|
+
enum_class = super().__new__(cls, cls_name, bases, class_dict)
|
|
39
|
+
if enum_class.__annotations__.get("__abstract__"):
|
|
40
|
+
return enum_class
|
|
41
|
+
|
|
42
|
+
if enum_class.__is_variant__:
|
|
43
|
+
return enum_class
|
|
44
|
+
else:
|
|
45
|
+
enum_class.__variants__ = dict()
|
|
46
|
+
|
|
47
|
+
enum_class.__full_variant_name__ = cls_name
|
|
48
|
+
enum_class.__variant_name__ = cls_name
|
|
49
|
+
|
|
50
|
+
annotation: typing.Union[type[Annotated[typing.Any, BaseMetadata]], type]
|
|
51
|
+
for attr, annotation in enum_class.__annotations__.items():
|
|
52
|
+
if not hasattr(annotation, "__args__"):
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if (__origin__ := getattr(annotation, "__origin__", None)) and annotation.__name__ == "Annotated":
|
|
56
|
+
origin = typing.get_args(__origin__)[0]
|
|
57
|
+
else:
|
|
58
|
+
is_type = isinstance(annotation, types.GenericAlias) and annotation.__name__ == "type"
|
|
59
|
+
if not is_type:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
origin = typing.get_args(annotation)[0]
|
|
63
|
+
|
|
64
|
+
split = origin[:-1].split("[", maxsplit=1)
|
|
65
|
+
|
|
66
|
+
content_type: str | type[typing.Any]
|
|
67
|
+
if len(split) == 1:
|
|
68
|
+
content_type = NoValue
|
|
69
|
+
else:
|
|
70
|
+
left, right = split
|
|
71
|
+
if left != enum_class.__name__:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if right.split("[", maxsplit=1)[0] == enum_class.__name__:
|
|
75
|
+
content_type = enum_class
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
content_type = eval(right)
|
|
79
|
+
except NameError:
|
|
80
|
+
content_type = right
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
variant_base = enum_class[content_type] # type: ignore
|
|
84
|
+
except TypeError:
|
|
85
|
+
# When enum is non-generic, like this
|
|
86
|
+
#
|
|
87
|
+
# class SimpleEnum(TypEnum):
|
|
88
|
+
# V: type["SimpleEnum"]
|
|
89
|
+
#
|
|
90
|
+
variant_base = enum_class
|
|
91
|
+
|
|
92
|
+
class _EnumVariant(variant_base): # type: ignore
|
|
93
|
+
__is_variant__ = True
|
|
94
|
+
|
|
95
|
+
_EnumVariant.__name__ = _EnumVariant.__full_variant_name__ = f"{enum_class.__name__}.{attr}"
|
|
96
|
+
_EnumVariant.__variant_name__ = attr
|
|
97
|
+
_EnumVariant.__content_type__ = content_type
|
|
98
|
+
|
|
99
|
+
enum_class.__variants__[_EnumVariant] = attr
|
|
100
|
+
|
|
101
|
+
setattr(enum_class, attr, _EnumVariant)
|
|
102
|
+
|
|
103
|
+
return enum_class
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
return getattr(self, "__full_variant_name__", self.__class__.__name__)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _TypEnum(typing.Generic[TypEnumContent], metaclass=TypEnumMeta):
|
|
110
|
+
__match_args__ = ("value",)
|
|
111
|
+
|
|
112
|
+
__full_variant_name__: typing.ClassVar[str]
|
|
113
|
+
__variant_name__: typing.ClassVar[str]
|
|
114
|
+
|
|
115
|
+
__content_type__: typing.ClassVar[typing.Union[str, type[typing.Any]]]
|
|
116
|
+
|
|
117
|
+
__variants__: typing.ClassVar[dict[type['_TypEnum[typing.Any]'], str]]
|
|
118
|
+
|
|
119
|
+
__is_variant__: typing.ClassVar[bool] = False
|
|
120
|
+
|
|
121
|
+
__abstract__: typing_extensions.Never
|
|
122
|
+
|
|
123
|
+
value: typing.Optional[TypEnumContent]
|
|
124
|
+
|
|
125
|
+
def __init__(self, value: TypEnumContent):
|
|
126
|
+
if self.__content_type__ is NoValue:
|
|
127
|
+
self.value = None
|
|
128
|
+
else:
|
|
129
|
+
self.value = value
|
|
130
|
+
|
|
131
|
+
def __repr__(self) -> str:
|
|
132
|
+
if self.__content_type__ is NoValue:
|
|
133
|
+
return f"{self.__full_variant_name__}()"
|
|
134
|
+
return f"{self.__full_variant_name__}({self.value.__repr__()})"
|
|
135
|
+
|
|
136
|
+
def __eq__(self, other: object) -> bool:
|
|
137
|
+
if not isinstance(other, _TypEnum):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
return self.__class__ == other.__class__ and self.value == other.value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TypEnum(_TypEnum[TypEnumContent]):
|
|
144
|
+
__abstract__: typing_extensions.Never
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pydantic
|
|
2
|
+
|
|
3
|
+
if tuple(map(int, pydantic.version.VERSION.split('.'))) < (2, 9, 0):
|
|
4
|
+
raise ValueError("Pydantic version must be >=2.9.0")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from .core import (
|
|
8
|
+
Rename,
|
|
9
|
+
TypEnumPydantic,
|
|
10
|
+
FieldMetadata,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FieldMetadata",
|
|
16
|
+
"Rename",
|
|
17
|
+
"TypEnumPydantic",
|
|
18
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
import pydantic as pydantic_
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import typing_extensions
|
|
9
|
+
from annotated_types import GroupedMetadata, BaseMetadata
|
|
10
|
+
from pydantic_core import core_schema
|
|
11
|
+
from pydantic_core.core_schema import ValidationInfo, SerializerFunctionWrapHandler
|
|
12
|
+
|
|
13
|
+
from enumetyped.core import TypEnumMeta, _TypEnum, TypEnumContent
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Rename",
|
|
17
|
+
"FieldMetadata",
|
|
18
|
+
"TypEnumPydantic",
|
|
19
|
+
"TypEnumPydanticMeta",
|
|
20
|
+
"eval_content_type",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
from enumetyped.pydantic.serialization import AdjacentlyTagged, InternallyTagged, ExternallyTagged
|
|
24
|
+
from enumetyped.pydantic.serialization.tagged import TaggedSerialization
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class Rename(BaseMetadata):
|
|
29
|
+
value: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FieldMetadata(GroupedMetadata):
|
|
34
|
+
rename: typing.Optional[str] = None
|
|
35
|
+
|
|
36
|
+
def __iter__(self) -> typing.Iterator[BaseMetadata]:
|
|
37
|
+
if self.rename is not None:
|
|
38
|
+
yield Rename(self.rename)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def eval_content_type(cls: type['TypEnumPydantic[TypEnumContent]']) -> type:
|
|
42
|
+
# Eval annotation into real object
|
|
43
|
+
base = cls.__orig_bases__[0] # type: ignore
|
|
44
|
+
module = importlib.import_module(base.__module__)
|
|
45
|
+
return eval(cls.__content_type__, module.__dict__) # type: ignore
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TypEnumPydanticMeta(TypEnumMeta):
|
|
49
|
+
__serialization__: TaggedSerialization
|
|
50
|
+
|
|
51
|
+
def __new__(
|
|
52
|
+
cls,
|
|
53
|
+
cls_name: str,
|
|
54
|
+
bases: tuple[typing.Any],
|
|
55
|
+
class_dict: dict[str, typing.Any],
|
|
56
|
+
variant: typing.Optional[str] = None,
|
|
57
|
+
content: typing.Optional[str] = None,
|
|
58
|
+
) -> typing.Any:
|
|
59
|
+
enum_class = super().__new__(cls, cls_name, bases, class_dict)
|
|
60
|
+
if enum_class.__annotations__.get("__abstract__"):
|
|
61
|
+
return enum_class
|
|
62
|
+
|
|
63
|
+
enum_class.__full_variant_name__ = cls_name
|
|
64
|
+
enum_class.__variant_name__ = cls_name
|
|
65
|
+
|
|
66
|
+
if enum_class.__is_variant__:
|
|
67
|
+
return enum_class
|
|
68
|
+
|
|
69
|
+
enum_class.__names_serialization__ = dict()
|
|
70
|
+
enum_class.__names_deserialization__ = dict()
|
|
71
|
+
|
|
72
|
+
if variant is not None and content is not None:
|
|
73
|
+
enum_class.__serialization__ = AdjacentlyTagged(variant, content)
|
|
74
|
+
elif variant is not None:
|
|
75
|
+
enum_class.__serialization__ = InternallyTagged(variant)
|
|
76
|
+
else:
|
|
77
|
+
enum_class.__serialization__ = ExternallyTagged()
|
|
78
|
+
|
|
79
|
+
annotation: typing.Union[type[typing_extensions.Annotated[typing.Any, BaseMetadata]], type]
|
|
80
|
+
for attr, annotation in enum_class.__annotations__.items():
|
|
81
|
+
if not hasattr(annotation, "__args__"):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
enum_variant = getattr(enum_class, attr)
|
|
85
|
+
if isinstance(enum_variant.__content_type__, str):
|
|
86
|
+
try:
|
|
87
|
+
enum_variant.__content_type__ = eval_content_type(enum_variant)
|
|
88
|
+
except NameError:
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
if isinstance(annotation, typing._AnnotatedAlias): # type: ignore
|
|
92
|
+
metadata: list[typing.Union[BaseMetadata, GroupedMetadata]] = []
|
|
93
|
+
for v in annotation.__metadata__:
|
|
94
|
+
if isinstance(v, FieldMetadata):
|
|
95
|
+
metadata.extend(v)
|
|
96
|
+
else:
|
|
97
|
+
metadata.append(v)
|
|
98
|
+
|
|
99
|
+
for __meta__ in metadata:
|
|
100
|
+
if isinstance(__meta__, Rename):
|
|
101
|
+
if __meta__.value in enum_class.__names_deserialization__:
|
|
102
|
+
raise ValueError(f"{cls_name}: Two or many field renamed to `{__meta__.value}`")
|
|
103
|
+
|
|
104
|
+
enum_class.__names_serialization__[attr] = __meta__.value
|
|
105
|
+
enum_class.__names_deserialization__[__meta__.value] = attr
|
|
106
|
+
|
|
107
|
+
return enum_class
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TypEnumPydantic(_TypEnum[TypEnumContent], metaclass=TypEnumPydanticMeta):
|
|
111
|
+
__abstract__: typing_extensions.Never
|
|
112
|
+
|
|
113
|
+
__names_serialization__: typing.ClassVar[dict[str, str]]
|
|
114
|
+
__names_deserialization__: typing.ClassVar[dict[str, str]]
|
|
115
|
+
|
|
116
|
+
__serialization__: typing.ClassVar[TaggedSerialization]
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def content_type(cls) -> type:
|
|
120
|
+
# Resolve types when __content_type__ declare after cls declaration
|
|
121
|
+
if isinstance(cls.__content_type__, str):
|
|
122
|
+
cls.__content_type__ = eval_content_type(cls)
|
|
123
|
+
return cls.__content_type__
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def __variant_constructor__(
|
|
127
|
+
cls: type["TypEnumPydantic[TypEnumContent]"],
|
|
128
|
+
value: typing.Any,
|
|
129
|
+
info: ValidationInfo,
|
|
130
|
+
) -> "TypEnumPydantic[TypEnumContent]":
|
|
131
|
+
if inspect.isclass(cls.content_type()) and issubclass(cls.content_type(), TypEnumPydantic):
|
|
132
|
+
value = cls.__python_value_restore__(value, info)
|
|
133
|
+
|
|
134
|
+
return cls(value)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def __get_pydantic_core_schema__(
|
|
138
|
+
cls: type["TypEnumPydantic[TypEnumContent]"],
|
|
139
|
+
source_type: typing.Any,
|
|
140
|
+
handler: pydantic_.GetCoreSchemaHandler,
|
|
141
|
+
) -> core_schema.CoreSchema:
|
|
142
|
+
return cls.__serialization__.__get_pydantic_core_schema__(cls, source_type, handler)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def __python_value_restore__(
|
|
146
|
+
cls: type["TypEnumPydantic[TypEnumContent]"],
|
|
147
|
+
input_value: typing.Any,
|
|
148
|
+
info: ValidationInfo,
|
|
149
|
+
) -> typing.Any:
|
|
150
|
+
return cls.__serialization__.__python_value_restore__(cls, input_value, info)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def __pydantic_serialization__(
|
|
154
|
+
cls: type["TypEnumPydantic[TypEnumContent]"],
|
|
155
|
+
model: typing.Any,
|
|
156
|
+
serializer: SerializerFunctionWrapHandler,
|
|
157
|
+
) -> typing.Any:
|
|
158
|
+
return cls.__serialization__.__pydantic_serialization__(cls, model, serializer)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import pydantic as pydantic_
|
|
5
|
+
from pydantic_core import CoreSchema, core_schema
|
|
6
|
+
from pydantic_core.core_schema import SerializerFunctionWrapHandler, ValidationInfo
|
|
7
|
+
|
|
8
|
+
from enumetyped.core import TypEnumContent, NoValue
|
|
9
|
+
from enumetyped.pydantic.serialization.tagged import TaggedSerialization
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
from ..core import TypEnumPydantic # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AdjacentlyTagged",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AdjacentlyTagged(TaggedSerialization):
|
|
21
|
+
__variant_tag__: str
|
|
22
|
+
__content_tag__: str
|
|
23
|
+
|
|
24
|
+
def __init__(self, variant: str, content: str):
|
|
25
|
+
self.__variant_tag__ = variant
|
|
26
|
+
self.__content_tag__ = content
|
|
27
|
+
|
|
28
|
+
def __get_pydantic_core_schema__(
|
|
29
|
+
self,
|
|
30
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
31
|
+
_source_type: typing.Any,
|
|
32
|
+
handler: pydantic_.GetCoreSchemaHandler,
|
|
33
|
+
) -> CoreSchema:
|
|
34
|
+
from enumetyped.pydantic.core import TypEnumPydantic
|
|
35
|
+
|
|
36
|
+
json_schemas: list[core_schema.CoreSchema] = []
|
|
37
|
+
for attr in kls.__variants__.values():
|
|
38
|
+
enum_variant: type[TypEnumPydantic[TypEnumContent]] = getattr(kls, attr)
|
|
39
|
+
attr = kls.__names_serialization__.get(attr, attr)
|
|
40
|
+
variant_schema = core_schema.typed_dict_field(core_schema.str_schema(pattern=attr))
|
|
41
|
+
is_enumetyped_variant = (
|
|
42
|
+
inspect.isclass(enum_variant.__content_type__) and
|
|
43
|
+
issubclass(enum_variant.__content_type__, TypEnumPydantic)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
schema = {
|
|
47
|
+
self.__variant_tag__: variant_schema,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if is_enumetyped_variant or enum_variant.__content_type__ is NoValue:
|
|
51
|
+
if is_enumetyped_variant:
|
|
52
|
+
kls_: type = enum_variant.__content_type__ # type: ignore
|
|
53
|
+
schema_definition = core_schema.definition_reference_schema(f"{kls_.__name__}:{id(kls_)}")
|
|
54
|
+
value_schema = core_schema.typed_dict_field(core_schema.definitions_schema(
|
|
55
|
+
schema=schema_definition,
|
|
56
|
+
definitions=[
|
|
57
|
+
core_schema.any_schema(ref=f"{kls_.__name__}:{id(kls_)}")
|
|
58
|
+
],
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
schema[self.__content_tag__] = value_schema
|
|
62
|
+
else:
|
|
63
|
+
value_schema = core_schema.typed_dict_field(handler.generate_schema(enum_variant.__content_type__))
|
|
64
|
+
schema[self.__content_tag__] = value_schema
|
|
65
|
+
|
|
66
|
+
json_schemas.append(core_schema.typed_dict_schema(schema))
|
|
67
|
+
|
|
68
|
+
return core_schema.json_or_python_schema(
|
|
69
|
+
json_schema=core_schema.with_info_after_validator_function(
|
|
70
|
+
kls.__python_value_restore__,
|
|
71
|
+
core_schema.union_schema([*json_schemas]),
|
|
72
|
+
),
|
|
73
|
+
python_schema=core_schema.with_info_after_validator_function(
|
|
74
|
+
kls.__python_value_restore__,
|
|
75
|
+
core_schema.union_schema([*json_schemas, core_schema.any_schema()]),
|
|
76
|
+
),
|
|
77
|
+
serialization=core_schema.wrap_serializer_function_ser_schema(
|
|
78
|
+
kls.__pydantic_serialization__
|
|
79
|
+
),
|
|
80
|
+
ref=f"{kls.__name__}:{id(kls)}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __python_value_restore__(
|
|
84
|
+
self,
|
|
85
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
86
|
+
input_value: typing.Any,
|
|
87
|
+
info: ValidationInfo,
|
|
88
|
+
) -> typing.Any:
|
|
89
|
+
from enumetyped.pydantic.core import TypEnumPydantic
|
|
90
|
+
|
|
91
|
+
if isinstance(input_value, TypEnumPydantic):
|
|
92
|
+
return input_value
|
|
93
|
+
|
|
94
|
+
type_key = input_value[self.__variant_tag__]
|
|
95
|
+
value = input_value.get(self.__content_tag__, None)
|
|
96
|
+
attr = kls.__names_deserialization__.get(type_key, type_key)
|
|
97
|
+
return getattr(kls, attr).__variant_constructor__(value, info)
|
|
98
|
+
|
|
99
|
+
def __pydantic_serialization__(
|
|
100
|
+
self,
|
|
101
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
102
|
+
model: typing.Any,
|
|
103
|
+
serializer: SerializerFunctionWrapHandler,
|
|
104
|
+
) -> typing.Any:
|
|
105
|
+
attr = model.__variant_name__
|
|
106
|
+
attr = kls.__names_serialization__.get(attr, attr)
|
|
107
|
+
|
|
108
|
+
result = {self.__variant_tag__: attr}
|
|
109
|
+
if model.__content_type__ is NoValue:
|
|
110
|
+
pass
|
|
111
|
+
elif isinstance(model.value, kls):
|
|
112
|
+
result[self.__content_tag__] = kls.__pydantic_serialization__(model.value, serializer)
|
|
113
|
+
else:
|
|
114
|
+
result[self.__content_tag__] = serializer(model.value)
|
|
115
|
+
|
|
116
|
+
return result
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import pydantic as pydantic_
|
|
5
|
+
from pydantic_core import CoreSchema, core_schema
|
|
6
|
+
from pydantic_core.core_schema import SerializerFunctionWrapHandler, ValidationInfo
|
|
7
|
+
|
|
8
|
+
from enumetyped.core import TypEnumContent, NoValue
|
|
9
|
+
from enumetyped.pydantic.serialization.tagged import TaggedSerialization
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
from ..core import TypEnumPydantic # type: ignore
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ExternallyTagged",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExternallyTagged(TaggedSerialization):
|
|
20
|
+
def __get_pydantic_core_schema__(
|
|
21
|
+
self,
|
|
22
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
23
|
+
_source_type: typing.Any,
|
|
24
|
+
handler: pydantic_.GetCoreSchemaHandler,
|
|
25
|
+
) -> CoreSchema:
|
|
26
|
+
from enumetyped.pydantic.core import TypEnumPydantic
|
|
27
|
+
|
|
28
|
+
json_schema_attrs = {}
|
|
29
|
+
other_schemas = []
|
|
30
|
+
for attr in kls.__variants__.values():
|
|
31
|
+
enum_variant: type[TypEnumPydantic[TypEnumContent]] = getattr(kls, attr)
|
|
32
|
+
attr = kls.__names_serialization__.get(attr, attr)
|
|
33
|
+
|
|
34
|
+
is_enumetyped_variant = (
|
|
35
|
+
inspect.isclass(enum_variant.__content_type__) and
|
|
36
|
+
issubclass(enum_variant.__content_type__, TypEnumPydantic)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
item_schema: core_schema.CoreSchema
|
|
40
|
+
if is_enumetyped_variant or enum_variant.__content_type__ is NoValue:
|
|
41
|
+
if enum_variant.__content_type__ is NoValue:
|
|
42
|
+
other_schemas.append(core_schema.str_schema(pattern=attr))
|
|
43
|
+
continue
|
|
44
|
+
else:
|
|
45
|
+
kls_: type = enum_variant.__content_type__ # type: ignore
|
|
46
|
+
schema_definition = core_schema.definition_reference_schema(f"{kls_.__name__}:{id(kls_)}")
|
|
47
|
+
item_schema = core_schema.definitions_schema(
|
|
48
|
+
schema=schema_definition,
|
|
49
|
+
definitions=[
|
|
50
|
+
core_schema.any_schema(ref=f"{kls_.__name__}:{id(kls_)}")
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
item_schema = handler.generate_schema(enum_variant.__content_type__)
|
|
56
|
+
|
|
57
|
+
json_schema_attrs[attr] = core_schema.typed_dict_field(item_schema, required=False)
|
|
58
|
+
|
|
59
|
+
schemas = [core_schema.typed_dict_schema(json_schema_attrs), *other_schemas]
|
|
60
|
+
|
|
61
|
+
return core_schema.json_or_python_schema(
|
|
62
|
+
json_schema=core_schema.with_info_after_validator_function(
|
|
63
|
+
kls.__python_value_restore__,
|
|
64
|
+
core_schema.union_schema([*schemas]),
|
|
65
|
+
),
|
|
66
|
+
python_schema=core_schema.with_info_after_validator_function(
|
|
67
|
+
kls.__python_value_restore__,
|
|
68
|
+
core_schema.union_schema([*schemas, core_schema.any_schema()]),
|
|
69
|
+
),
|
|
70
|
+
serialization=core_schema.wrap_serializer_function_ser_schema(
|
|
71
|
+
kls.__pydantic_serialization__
|
|
72
|
+
),
|
|
73
|
+
ref=f"{kls.__name__}:{id(kls)}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __python_value_restore__(
|
|
77
|
+
self,
|
|
78
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
79
|
+
input_value: typing.Any,
|
|
80
|
+
info: ValidationInfo,
|
|
81
|
+
) -> typing.Any:
|
|
82
|
+
from enumetyped.pydantic.core import TypEnumPydantic
|
|
83
|
+
|
|
84
|
+
if isinstance(input_value, TypEnumPydantic):
|
|
85
|
+
return input_value
|
|
86
|
+
|
|
87
|
+
if isinstance(input_value, str):
|
|
88
|
+
input_value = {input_value: None}
|
|
89
|
+
|
|
90
|
+
for attr, value in input_value.items():
|
|
91
|
+
attr = kls.__names_deserialization__.get(attr, attr)
|
|
92
|
+
return getattr(kls, attr).__variant_constructor__(value, info)
|
|
93
|
+
|
|
94
|
+
def __pydantic_serialization__(
|
|
95
|
+
self,
|
|
96
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
97
|
+
model: typing.Any,
|
|
98
|
+
serializer: SerializerFunctionWrapHandler,
|
|
99
|
+
) -> typing.Any:
|
|
100
|
+
attr = model.__variant_name__
|
|
101
|
+
attr = kls.__names_serialization__.get(attr, attr)
|
|
102
|
+
|
|
103
|
+
if model.__content_type__ is NoValue:
|
|
104
|
+
return attr
|
|
105
|
+
elif isinstance(model.value, kls):
|
|
106
|
+
content = kls.__pydantic_serialization__(model.value, serializer)
|
|
107
|
+
else:
|
|
108
|
+
content = serializer(model.value)
|
|
109
|
+
|
|
110
|
+
return {attr: content}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import pydantic as pydantic_
|
|
4
|
+
from pydantic_core import CoreSchema, core_schema, SchemaValidator
|
|
5
|
+
from pydantic_core.core_schema import SerializerFunctionWrapHandler, ValidationInfo
|
|
6
|
+
|
|
7
|
+
from enumetyped.core import NoValue, TypEnumContent
|
|
8
|
+
from enumetyped.pydantic.serialization.tagged import TaggedSerialization
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from ..core import TypEnumPydantic # type: ignore
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"InternallyTagged",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InternallyTagged(TaggedSerialization):
|
|
19
|
+
__variant_tag__: str
|
|
20
|
+
__ext_tagged_schema_validator__: SchemaValidator
|
|
21
|
+
|
|
22
|
+
def __init__(self, variant: str):
|
|
23
|
+
self.__variant_tag__ = variant
|
|
24
|
+
|
|
25
|
+
def __get_pydantic_core_schema__(
|
|
26
|
+
self,
|
|
27
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
28
|
+
source_type: typing.Any,
|
|
29
|
+
handler: pydantic_.GetCoreSchemaHandler,
|
|
30
|
+
) -> CoreSchema:
|
|
31
|
+
from enumetyped.pydantic.core import TypEnumPydantic
|
|
32
|
+
|
|
33
|
+
json_schemas: dict[str, core_schema.CoreSchema] = {}
|
|
34
|
+
real_schema_attrs = {}
|
|
35
|
+
real_schemas: list[core_schema.CoreSchema] = []
|
|
36
|
+
|
|
37
|
+
for attr in kls.__variants__.values():
|
|
38
|
+
enum_variant: type[TypEnumPydantic[TypEnumContent]] = getattr(kls, attr)
|
|
39
|
+
attr = kls.__names_serialization__.get(attr, attr)
|
|
40
|
+
variant_schema = core_schema.typed_dict_field(core_schema.str_schema(pattern=attr))
|
|
41
|
+
|
|
42
|
+
schema = {
|
|
43
|
+
self.__variant_tag__: variant_schema,
|
|
44
|
+
}
|
|
45
|
+
if enum_variant.__content_type__ is NoValue:
|
|
46
|
+
real_schemas.append(core_schema.str_schema(pattern=attr))
|
|
47
|
+
else:
|
|
48
|
+
item_schema = handler.generate_schema(enum_variant.__content_type__)
|
|
49
|
+
resolved = handler.resolve_ref_schema(item_schema)
|
|
50
|
+
|
|
51
|
+
real_schema_attrs[attr] = core_schema.typed_dict_field(resolved, required=False)
|
|
52
|
+
real_schemas.append(resolved)
|
|
53
|
+
|
|
54
|
+
match resolved:
|
|
55
|
+
case {"type": "dataclass", "schema": {"fields": fields}}:
|
|
56
|
+
fields = {
|
|
57
|
+
field["name"]: {
|
|
58
|
+
"type": "typed-dict-field",
|
|
59
|
+
"schema": field["schema"]
|
|
60
|
+
} for field in fields
|
|
61
|
+
}
|
|
62
|
+
schema.update(**fields)
|
|
63
|
+
case {"type": "model", "schema": {"fields": fields}}:
|
|
64
|
+
fields = {
|
|
65
|
+
k: {
|
|
66
|
+
"type": "typed-dict-field",
|
|
67
|
+
"schema": v["schema"]
|
|
68
|
+
} for k, v in fields.items()
|
|
69
|
+
}
|
|
70
|
+
schema.update(**fields)
|
|
71
|
+
case {"type": "typed-dict", "fields": fields}:
|
|
72
|
+
schema.update(**fields)
|
|
73
|
+
case _:
|
|
74
|
+
raise TypeError(
|
|
75
|
+
"Type of content must be a TypedDict, dataclass or BaseModel subclass"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
json_schemas[attr] = core_schema.typed_dict_schema(schema)
|
|
79
|
+
|
|
80
|
+
# Store nested schema for deserializing
|
|
81
|
+
self.__ext_tagged_schema_validator__ = SchemaValidator(core_schema.union_schema([
|
|
82
|
+
core_schema.typed_dict_schema(real_schema_attrs),
|
|
83
|
+
*real_schemas,
|
|
84
|
+
]))
|
|
85
|
+
|
|
86
|
+
json_schema = core_schema.tagged_union_schema(
|
|
87
|
+
choices=json_schemas,
|
|
88
|
+
discriminator=self.__variant_tag__,
|
|
89
|
+
)
|
|
90
|
+
return core_schema.json_or_python_schema(
|
|
91
|
+
json_schema=core_schema.with_info_after_validator_function(
|
|
92
|
+
kls.__python_value_restore__,
|
|
93
|
+
json_schema,
|
|
94
|
+
),
|
|
95
|
+
python_schema=core_schema.with_info_after_validator_function(
|
|
96
|
+
kls.__python_value_restore__,
|
|
97
|
+
core_schema.any_schema(),
|
|
98
|
+
),
|
|
99
|
+
serialization=core_schema.wrap_serializer_function_ser_schema(
|
|
100
|
+
kls.__pydantic_serialization__
|
|
101
|
+
),
|
|
102
|
+
ref=f"{kls.__name__}:{id(kls)}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def __python_value_restore__(
|
|
106
|
+
self,
|
|
107
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
108
|
+
input_value: typing.Any,
|
|
109
|
+
info: ValidationInfo,
|
|
110
|
+
) -> typing.Any:
|
|
111
|
+
if isinstance(input_value, kls):
|
|
112
|
+
return input_value
|
|
113
|
+
|
|
114
|
+
type_key = input_value.pop(self.__variant_tag__)
|
|
115
|
+
attr = kls.__names_deserialization__.get(type_key, type_key)
|
|
116
|
+
|
|
117
|
+
if input_value:
|
|
118
|
+
value = self.__ext_tagged_schema_validator__.validate_python({type_key: input_value})
|
|
119
|
+
return getattr(kls, attr).__variant_constructor__(value[type_key], info)
|
|
120
|
+
else:
|
|
121
|
+
return getattr(kls, attr).__variant_constructor__(None, info)
|
|
122
|
+
|
|
123
|
+
def __pydantic_serialization__(
|
|
124
|
+
self,
|
|
125
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
126
|
+
model: typing.Any,
|
|
127
|
+
serializer: SerializerFunctionWrapHandler,
|
|
128
|
+
) -> typing.Any:
|
|
129
|
+
attr = model.__variant_name__
|
|
130
|
+
attr = kls.__names_serialization__.get(attr, attr)
|
|
131
|
+
|
|
132
|
+
result = {self.__variant_tag__: attr}
|
|
133
|
+
if model.__content_type__ is NoValue:
|
|
134
|
+
pass
|
|
135
|
+
elif isinstance(model.value, kls):
|
|
136
|
+
result.update(**kls.__pydantic_serialization__(model.value, serializer))
|
|
137
|
+
else:
|
|
138
|
+
result.update(**serializer(model.value))
|
|
139
|
+
|
|
140
|
+
return result
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
import pydantic as pydantic_
|
|
5
|
+
from pydantic_core import CoreSchema
|
|
6
|
+
from pydantic_core.core_schema import SerializerFunctionWrapHandler, ValidationInfo
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from ...core import TypEnumContent # type: ignore
|
|
10
|
+
from ..core import TypEnumPydantic # type: ignore
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"TaggedSerialization",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaggedSerialization(ABC):
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def __get_pydantic_core_schema__(
|
|
20
|
+
self,
|
|
21
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
22
|
+
_source_type: typing.Any,
|
|
23
|
+
handler: pydantic_.GetCoreSchemaHandler,
|
|
24
|
+
) -> CoreSchema:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def __python_value_restore__(
|
|
29
|
+
self,
|
|
30
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
31
|
+
input_value: typing.Any,
|
|
32
|
+
info: ValidationInfo,
|
|
33
|
+
) -> typing.Any:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def __pydantic_serialization__(
|
|
38
|
+
self,
|
|
39
|
+
kls: type["TypEnumPydantic[TypEnumContent]"],
|
|
40
|
+
model: typing.Any,
|
|
41
|
+
serializer: SerializerFunctionWrapHandler,
|
|
42
|
+
) -> typing.Any:
|
|
43
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
package-mode = true
|
|
3
|
+
name = "enumetyped"
|
|
4
|
+
version = "0.3.2"
|
|
5
|
+
|
|
6
|
+
authors = [
|
|
7
|
+
"Rinat Balbekov <me@rjotanm.dev>",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
description = "Type-containing enumeration"
|
|
11
|
+
repository = "https://github.com/rjotanm/enumetyped"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Framework :: Pydantic",
|
|
18
|
+
"Framework :: Pydantic :: 2",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
packages = [
|
|
32
|
+
{ include = "enumetyped" },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.poetry.dependencies]
|
|
36
|
+
python = "^3.10"
|
|
37
|
+
typing-extensions = ">=4.0.0"
|
|
38
|
+
|
|
39
|
+
pydantic = { version = ">=2.9.0", optional = true }
|
|
40
|
+
|
|
41
|
+
[tool.poetry.group.dev.dependencies]
|
|
42
|
+
mypy = "^1.13.0"
|
|
43
|
+
|
|
44
|
+
[tool.poetry.extras]
|
|
45
|
+
pydantic = ["pydantic"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["poetry-core"]
|
|
50
|
+
build-backend = "poetry.core.masonry.api"
|