enumetyped 0.3.2__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.
enumetyped/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from enumetyped.core import TypEnum, TypEnumContent, NoValue
2
+
3
+ __all__ = [
4
+ "NoValue",
5
+ "TypEnum",
6
+ "TypEnumContent",
7
+ ]
8
+
9
+ __package_name__ = "enumetyped"
10
+ __version__ = "0.3.2"
11
+ __description__ = "Type-containing enumeration"
enumetyped/core.py ADDED
@@ -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
enumetyped/py.typed ADDED
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,9 @@
1
+ from .externally import ExternallyTagged
2
+ from .adjacently import AdjacentlyTagged
3
+ from .internally import InternallyTagged
4
+
5
+ __all__ = [
6
+ "ExternallyTagged",
7
+ "AdjacentlyTagged",
8
+ "InternallyTagged",
9
+ ]
@@ -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,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,14 @@
1
+ enumetyped/__init__.py,sha256=GMWHTsiLZvstlKznvbWpjjnrGl9P201hpuHenGaApPU,231
2
+ enumetyped/core.py,sha256=lF1wDX6ISZZMUQcyhxCOxfYw8zwhIO3AM2Ey9Jgre7I,4381
3
+ enumetyped/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ enumetyped/pydantic/__init__.py,sha256=6BEk1WIR4CpPqemzEvYZuz2-eD5U4pGQsiHgW4MRNP0,293
5
+ enumetyped/pydantic/core.py,sha256=HaCbRswPJEZ-12VKqGAlDRzW8jPSyaelysDGY-IQTBo,5679
6
+ enumetyped/pydantic/serialization/__init__.py,sha256=FUwxRUX4p-5kXceRn1DXDk24US81AqZdtg83FgMwru4,210
7
+ enumetyped/pydantic/serialization/adjacently.py,sha256=8TbPg1nR9Mvf86bZQz34m2B3Xi64FZFyVdZSZTSI690,4494
8
+ enumetyped/pydantic/serialization/externally.py,sha256=svJbncxfZHJSUba_CjNwmN6ueUYTyGR-zHmo9p3HzD8,4189
9
+ enumetyped/pydantic/serialization/internally.py,sha256=68DewUOesuifsc5NSnyWR8C8UdEn0xmofN5jIaV2FZo,5421
10
+ enumetyped/pydantic/serialization/tagged.py,sha256=VTZibCnRvvL9HN2c2S98rg3oOWftpwiSTui0HSGQSF0,1224
11
+ enumetyped-0.3.2.dist-info/LICENSE,sha256=puA__lPegsALCuAsYmfAqrLQzreBusiNoJBEWSwxKGE,1128
12
+ enumetyped-0.3.2.dist-info/METADATA,sha256=_VqG7EZ0jvf3LpVyjiYhItNPkb_n89zS4VRMmWzHz5Q,6516
13
+ enumetyped-0.3.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
14
+ enumetyped-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any