enumplus 1.0.0__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.
- enumplus/__init__.py +6 -0
- enumplus/enum.py +206 -0
- enumplus/pydantic.py +29 -0
- enumplus/serialize.py +38 -0
- enumplus-1.0.0.dist-info/METADATA +300 -0
- enumplus-1.0.0.dist-info/RECORD +8 -0
- enumplus-1.0.0.dist-info/WHEEL +4 -0
- enumplus-1.0.0.dist-info/licenses/LICENSE +21 -0
enumplus/__init__.py
ADDED
enumplus/enum.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from typing import dataclass_transform
|
|
8
|
+
except ImportError:
|
|
9
|
+
from typing_extensions import dataclass_transform
|
|
10
|
+
|
|
11
|
+
_SENTINEL = object()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass_transform()
|
|
15
|
+
class EnumMeta(enum.EnumMeta):
|
|
16
|
+
"""Base metaclass for enumplus enums."""
|
|
17
|
+
|
|
18
|
+
def __new__(
|
|
19
|
+
cls,
|
|
20
|
+
name: str,
|
|
21
|
+
bases: tuple[type, ...],
|
|
22
|
+
namespace: Any,
|
|
23
|
+
**kwargs: Any,
|
|
24
|
+
) -> EnumMeta:
|
|
25
|
+
member_metadata: dict[str, dict[str, Any]] = {}
|
|
26
|
+
|
|
27
|
+
member_names: list[str] = list(getattr(namespace, "_member_names", []))
|
|
28
|
+
last_values: list[Any] = getattr(namespace, "_last_values", [])
|
|
29
|
+
|
|
30
|
+
for key, value in list(namespace.items()):
|
|
31
|
+
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[1], dict):
|
|
32
|
+
actual_value, metadata = value
|
|
33
|
+
member_metadata[key] = metadata
|
|
34
|
+
if key in member_names:
|
|
35
|
+
idx = member_names.index(key)
|
|
36
|
+
last_values[idx] = actual_value
|
|
37
|
+
dict.__setitem__(namespace, key, actual_value)
|
|
38
|
+
|
|
39
|
+
new_cls = super().__new__(cls, name, bases, namespace, **kwargs)
|
|
40
|
+
|
|
41
|
+
members: list[Any] = list(new_cls)
|
|
42
|
+
for index, member in enumerate(members):
|
|
43
|
+
metadata = member_metadata.get(member.name, {})
|
|
44
|
+
member._metadata_ = metadata
|
|
45
|
+
|
|
46
|
+
label = metadata.get("label")
|
|
47
|
+
if not label:
|
|
48
|
+
member._label_ = member.name.title()
|
|
49
|
+
else:
|
|
50
|
+
member._label_ = label
|
|
51
|
+
|
|
52
|
+
member._index_ = index
|
|
53
|
+
|
|
54
|
+
return new_cls
|
|
55
|
+
|
|
56
|
+
def __contains__(cls, item: Any) -> bool:
|
|
57
|
+
if isinstance(item, cls):
|
|
58
|
+
return True
|
|
59
|
+
member: Any
|
|
60
|
+
for member in cls:
|
|
61
|
+
if member.value == item:
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Enum(enum.Enum, metaclass=EnumMeta):
|
|
67
|
+
"""Base enum class for enumplus."""
|
|
68
|
+
|
|
69
|
+
_label_: str
|
|
70
|
+
_metadata_: dict[str, Any]
|
|
71
|
+
_index_: int
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def label(self) -> str:
|
|
75
|
+
return self._label_
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def metadata(self) -> dict[str, Any]:
|
|
79
|
+
return self._metadata_
|
|
80
|
+
|
|
81
|
+
def __getattr__(self, name: str) -> Any:
|
|
82
|
+
if name.startswith("_"):
|
|
83
|
+
raise AttributeError(name)
|
|
84
|
+
try:
|
|
85
|
+
return self._metadata_[name]
|
|
86
|
+
except KeyError:
|
|
87
|
+
raise AttributeError(
|
|
88
|
+
f"{type(self).__name__}.{self.name!s} has no attribute {name!r}"
|
|
89
|
+
) from None
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
return self._label_
|
|
93
|
+
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return f"<{type(self).__name__}.{self.name}: {self.value!r}>"
|
|
96
|
+
|
|
97
|
+
def __eq__(self, other: object) -> bool:
|
|
98
|
+
if isinstance(other, enum.Enum):
|
|
99
|
+
return self is other
|
|
100
|
+
return bool(self.value == other)
|
|
101
|
+
|
|
102
|
+
def __hash__(self) -> int:
|
|
103
|
+
return hash(self.value)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def choices(cls) -> list[tuple[Any, str]]:
|
|
107
|
+
return [(member.value, member.label) for member in cls]
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_value(cls, value: Any, default: Any = _SENTINEL) -> Enum:
|
|
111
|
+
member: Any
|
|
112
|
+
for member in cls:
|
|
113
|
+
if member.value == value:
|
|
114
|
+
return cast(Enum, member)
|
|
115
|
+
if default is not _SENTINEL:
|
|
116
|
+
return cast(Enum, default)
|
|
117
|
+
raise ValueError(f"{value!r} is not a valid {cls.__name__} value")
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_name(cls, name: str, default: Any = _SENTINEL) -> Enum:
|
|
121
|
+
member = cls.__members__.get(name)
|
|
122
|
+
if member is not None:
|
|
123
|
+
return member
|
|
124
|
+
if default is not _SENTINEL:
|
|
125
|
+
return cast(Enum, default)
|
|
126
|
+
raise KeyError(f"{name!r} is not a valid {cls.__name__} name")
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def is_valid(cls, value: Any) -> bool:
|
|
130
|
+
member: Any
|
|
131
|
+
for member in cls:
|
|
132
|
+
if member.value == value or member is value:
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def validate(cls, value: Any) -> Enum:
|
|
138
|
+
return cls.from_value(value)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def values(cls) -> list[Any]:
|
|
142
|
+
return [member.value for member in cls]
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def names(cls) -> list[str]:
|
|
146
|
+
return [member.name for member in cls]
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def labels(cls) -> list[str]:
|
|
150
|
+
return [member.label for member in cls]
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def filter(cls, **kwargs: Any) -> list[Enum]:
|
|
154
|
+
if not kwargs:
|
|
155
|
+
return list(cls)
|
|
156
|
+
result: list[Enum] = []
|
|
157
|
+
member: Any
|
|
158
|
+
for member in cls:
|
|
159
|
+
if all(
|
|
160
|
+
key in member._metadata_ and member._metadata_[key] == value
|
|
161
|
+
for key, value in kwargs.items()
|
|
162
|
+
):
|
|
163
|
+
result.append(cast(Enum, member))
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def to_json(cls) -> str:
|
|
168
|
+
from enumplus.serialize import to_json
|
|
169
|
+
|
|
170
|
+
return to_json(cls)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_json(cls, data: str) -> dict[str, Any]:
|
|
174
|
+
from enumplus.serialize import from_json
|
|
175
|
+
|
|
176
|
+
return from_json(data)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> Any:
|
|
180
|
+
from enumplus.pydantic import get_pydantic_core_schema
|
|
181
|
+
|
|
182
|
+
return get_pydantic_core_schema(cls, source_type, handler)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class OrderedEnum(Enum):
|
|
186
|
+
"""Enum mixin with ordering based on declaration order."""
|
|
187
|
+
|
|
188
|
+
def __lt__(self, other: OrderedEnum) -> bool:
|
|
189
|
+
if not isinstance(other, type(self)):
|
|
190
|
+
return NotImplemented
|
|
191
|
+
return self._index_ < other._index_
|
|
192
|
+
|
|
193
|
+
def __le__(self, other: OrderedEnum) -> bool:
|
|
194
|
+
if not isinstance(other, type(self)):
|
|
195
|
+
return NotImplemented
|
|
196
|
+
return self._index_ <= other._index_
|
|
197
|
+
|
|
198
|
+
def __gt__(self, other: OrderedEnum) -> bool:
|
|
199
|
+
if not isinstance(other, type(self)):
|
|
200
|
+
return NotImplemented
|
|
201
|
+
return self._index_ > other._index_
|
|
202
|
+
|
|
203
|
+
def __ge__(self, other: OrderedEnum) -> bool:
|
|
204
|
+
if not isinstance(other, type(self)):
|
|
205
|
+
return NotImplemented
|
|
206
|
+
return self._index_ >= other._index_
|
enumplus/pydantic.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from enumplus.enum import Enum
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pydantic import GetCoreSchemaHandler
|
|
9
|
+
from pydantic_core import CoreSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_pydantic_core_schema(
|
|
13
|
+
cls: type[Enum], source_type: Any, handler: GetCoreSchemaHandler
|
|
14
|
+
) -> CoreSchema:
|
|
15
|
+
from pydantic_core import core_schema
|
|
16
|
+
|
|
17
|
+
def validate(value: Any) -> Any:
|
|
18
|
+
if isinstance(value, cls):
|
|
19
|
+
return value
|
|
20
|
+
return cls.from_value(value)
|
|
21
|
+
|
|
22
|
+
return core_schema.no_info_after_validator_function(
|
|
23
|
+
validate,
|
|
24
|
+
core_schema.any_schema(),
|
|
25
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
26
|
+
lambda v: v.value,
|
|
27
|
+
return_schema=core_schema.any_schema(),
|
|
28
|
+
),
|
|
29
|
+
)
|
enumplus/serialize.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from enumplus.enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def to_json(cls: type[Enum]) -> str:
|
|
10
|
+
data: dict[str, Any] = {
|
|
11
|
+
"name": cls.__name__,
|
|
12
|
+
"members": [
|
|
13
|
+
{
|
|
14
|
+
"name": member.name,
|
|
15
|
+
"value": member.value,
|
|
16
|
+
"label": member.label,
|
|
17
|
+
"metadata": member._metadata_,
|
|
18
|
+
}
|
|
19
|
+
for member in cls
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def from_json(data: str) -> dict[str, Any]:
|
|
26
|
+
try:
|
|
27
|
+
return cast(dict[str, Any], json.loads(data))
|
|
28
|
+
except json.JSONDecodeError as e:
|
|
29
|
+
raise ValueError(f"Invalid JSON: {e}") from None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SerializableEncoder(json.JSONEncoder):
|
|
33
|
+
def default(self, obj: Any) -> Any:
|
|
34
|
+
from enumplus.enum import Enum
|
|
35
|
+
|
|
36
|
+
if isinstance(obj, Enum):
|
|
37
|
+
return obj.value
|
|
38
|
+
return super().default(obj)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: enumplus
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Enhanced Python enums with metadata, serialization, and choices
|
|
5
|
+
Author-email: Mathias Paulenko <mathias.paulenko@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: choices,enum,enumeration,metadata,serialization
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
12
|
+
Requires-Dist: pydantic; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
15
|
+
Requires-Dist: typing-extensions; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# enumplus — Enhanced Enums for Python
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
Python's `enum.Enum` is basic. `enumplus` adds display names, metadata, JSON serialization, `choices()`, and value-based comparison — all with zero dependencies and full stdlib compatibility. Just change your import and everything still works.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install enumplus
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from enumplus import Enum
|
|
39
|
+
|
|
40
|
+
class Color(Enum):
|
|
41
|
+
RED = ("red", {"label": "Red", "hex": "#FF0000"})
|
|
42
|
+
GREEN = ("green", {"label": "Green", "hex": "#00FF00"})
|
|
43
|
+
BLUE = "blue" # no metadata needed
|
|
44
|
+
|
|
45
|
+
# Display names
|
|
46
|
+
print(Color.RED.label) # "Red"
|
|
47
|
+
print(str(Color.RED)) # "Red"
|
|
48
|
+
|
|
49
|
+
# Metadata access
|
|
50
|
+
print(Color.RED.hex) # "#FF0000"
|
|
51
|
+
print(Color.RED.metadata) # {"label": "Red", "hex": "#FF0000"}
|
|
52
|
+
|
|
53
|
+
# choices() for forms/dropdowns
|
|
54
|
+
print(Color.choices()) # [("red", "Red"), ("green", "Green"), ("blue", "Blue")]
|
|
55
|
+
|
|
56
|
+
# Lookup by value
|
|
57
|
+
print(Color.from_value("red")) # Color.RED
|
|
58
|
+
|
|
59
|
+
# Compare with values directly
|
|
60
|
+
print(Color.RED == "red") # True
|
|
61
|
+
|
|
62
|
+
# Membership test
|
|
63
|
+
print("red" in Color) # True
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
### Display Names (label)
|
|
69
|
+
|
|
70
|
+
Every member gets a human-readable label, auto-generated from the member name or set explicitly via metadata.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
class Status(Enum):
|
|
74
|
+
PENDING = "pending" # label: "Pending"
|
|
75
|
+
IN_PROGRESS = ("in_progress", {"label": "In Progress"})
|
|
76
|
+
|
|
77
|
+
print(Status.PENDING.label) # "Pending"
|
|
78
|
+
print(Status.IN_PROGRESS.label) # "In Progress"
|
|
79
|
+
print(str(Status.IN_PROGRESS)) # "In Progress"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Metadata
|
|
83
|
+
|
|
84
|
+
Attach arbitrary metadata to enum members using `(value, dict)` tuples. Access via attribute or the `metadata` property.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
class Color(Enum):
|
|
88
|
+
RED = ("red", {"hex": "#FF0000", "description": "Pure red"})
|
|
89
|
+
|
|
90
|
+
print(Color.RED.hex) # "#FF0000"
|
|
91
|
+
print(Color.RED.description) # "Pure red"
|
|
92
|
+
print(Color.RED.metadata) # {"hex": "#FF0000", "description": "Pure red"}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### choices()
|
|
96
|
+
|
|
97
|
+
Returns a list of `(value, label)` tuples — perfect for forms and dropdowns.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
class Priority(Enum):
|
|
101
|
+
LOW = 1
|
|
102
|
+
MEDIUM = 2
|
|
103
|
+
HIGH = 3
|
|
104
|
+
|
|
105
|
+
print(Priority.choices()) # [(1, "Low"), (2, "Medium"), (3, "High")]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### from_value() / from_name()
|
|
109
|
+
|
|
110
|
+
Look up members by value or name, with optional defaults.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class Color(Enum):
|
|
114
|
+
RED = "red"
|
|
115
|
+
GREEN = "green"
|
|
116
|
+
|
|
117
|
+
Color.from_value("red") # Color.RED
|
|
118
|
+
Color.from_value("blue") # raises ValueError
|
|
119
|
+
Color.from_value("blue", default=None) # None
|
|
120
|
+
|
|
121
|
+
Color.from_name("RED") # Color.RED
|
|
122
|
+
Color.from_name("BLUE", default=None) # None
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### is_valid() / validate()
|
|
126
|
+
|
|
127
|
+
Check if a value is valid, or validate and raise.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
class Color(Enum):
|
|
131
|
+
RED = "red"
|
|
132
|
+
|
|
133
|
+
Color.is_valid("red") # True
|
|
134
|
+
Color.is_valid("blue") # False
|
|
135
|
+
Color.is_valid(Color.RED) # True
|
|
136
|
+
|
|
137
|
+
Color.validate("red") # Color.RED
|
|
138
|
+
Color.validate("blue") # raises ValueError
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### values() / names() / labels()
|
|
142
|
+
|
|
143
|
+
Get lists of all values, names, or labels.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
class Color(Enum):
|
|
147
|
+
RED = ("red", {"label": "Red"})
|
|
148
|
+
GREEN = ("green", {"label": "Green"})
|
|
149
|
+
|
|
150
|
+
Color.values() # ["red", "green"]
|
|
151
|
+
Color.names() # ["RED", "GREEN"]
|
|
152
|
+
Color.labels() # ["Red", "Green"]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### filter()
|
|
156
|
+
|
|
157
|
+
Filter members by metadata key-value pairs (AND logic).
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
class Color(Enum):
|
|
161
|
+
RED = ("red", {"hex": "#FF0000", "category": "warm"})
|
|
162
|
+
GREEN = ("green", {"hex": "#00FF00", "category": "cool"})
|
|
163
|
+
|
|
164
|
+
Color.filter(category="warm") # [Color.RED]
|
|
165
|
+
Color.filter(hex="#FF0000", category="warm") # [Color.RED]
|
|
166
|
+
Color.filter() # [Color.RED, Color.GREEN]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Comparison with values (==)
|
|
170
|
+
|
|
171
|
+
Members compare equal to their values directly.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
class Color(Enum):
|
|
175
|
+
RED = "red"
|
|
176
|
+
|
|
177
|
+
Color.RED == "red" # True
|
|
178
|
+
Color.RED == Color.RED # True
|
|
179
|
+
Color.RED == "RED" # False (name != value)
|
|
180
|
+
Color.RED != 42 # True
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Membership test (in)
|
|
184
|
+
|
|
185
|
+
Check if a value or member belongs to an enum.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
class Color(Enum):
|
|
189
|
+
RED = "red"
|
|
190
|
+
|
|
191
|
+
"red" in Color # True
|
|
192
|
+
"blue" not in Color # True
|
|
193
|
+
Color.RED in Color # True
|
|
194
|
+
42 not in Color # True
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### OrderedEnum
|
|
198
|
+
|
|
199
|
+
Order members by declaration order using `<`, `<=`, `>`, `>=`.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from enumplus import OrderedEnum
|
|
203
|
+
|
|
204
|
+
class Priority(OrderedEnum):
|
|
205
|
+
LOW = 1
|
|
206
|
+
MEDIUM = 2
|
|
207
|
+
HIGH = 3
|
|
208
|
+
|
|
209
|
+
Priority.LOW < Priority.HIGH # True
|
|
210
|
+
Priority.HIGH > Priority.LOW # True
|
|
211
|
+
sorted([Priority.HIGH, Priority.LOW, Priority.MEDIUM]) # [LOW, MEDIUM, HIGH]
|
|
212
|
+
min(Priority) # Priority.LOW
|
|
213
|
+
max(Priority) # Priority.HIGH
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### JSON Serialization (to_json / from_json)
|
|
217
|
+
|
|
218
|
+
Serialize an enum class to JSON and parse it back.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
class Color(Enum):
|
|
222
|
+
RED = ("red", {"hex": "#FF0000"})
|
|
223
|
+
|
|
224
|
+
json_str = Color.to_json()
|
|
225
|
+
# {
|
|
226
|
+
# "name": "Color",
|
|
227
|
+
# "members": [
|
|
228
|
+
# {"name": "RED", "value": "red", "label": "Red", "metadata": {"hex": "#FF0000"}}
|
|
229
|
+
# ]
|
|
230
|
+
# }
|
|
231
|
+
|
|
232
|
+
data = Color.from_json(json_str) # parse back to dict
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### SerializableEncoder
|
|
236
|
+
|
|
237
|
+
Serialize enum members to their values in JSON via a custom encoder.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import json
|
|
241
|
+
from enumplus import Enum, SerializableEncoder
|
|
242
|
+
|
|
243
|
+
class Color(Enum):
|
|
244
|
+
RED = "red"
|
|
245
|
+
|
|
246
|
+
json.dumps(Color.RED, cls=SerializableEncoder) # '"red"'
|
|
247
|
+
json.dumps([Color.RED], cls=SerializableEncoder) # '["red"]'
|
|
248
|
+
json.dumps({"color": Color.RED}, cls=SerializableEncoder) # '{"color": "red"}'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Pydantic v2
|
|
252
|
+
|
|
253
|
+
`enumplus` works with Pydantic v2 out of the box. Members validate from values and serialize to values.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from pydantic import BaseModel
|
|
257
|
+
from enumplus import Enum
|
|
258
|
+
|
|
259
|
+
class Color(Enum):
|
|
260
|
+
RED = "red"
|
|
261
|
+
GREEN = "green"
|
|
262
|
+
|
|
263
|
+
class MyModel(BaseModel):
|
|
264
|
+
color: Color
|
|
265
|
+
|
|
266
|
+
model = MyModel(color="red") # validates "red" -> Color.RED
|
|
267
|
+
print(model.color) # Color.RED
|
|
268
|
+
print(model.model_dump()) # {"color": "red"}
|
|
269
|
+
print(model.model_dump_json()) # '{"color":"red"}'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Type hints in metadata
|
|
273
|
+
|
|
274
|
+
The `@dataclass_transform()` decorator on the metaclass enables type checkers to recognize metadata fields.
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
class Color(Enum):
|
|
278
|
+
RED = ("red", {"hex": "#FF0000"})
|
|
279
|
+
|
|
280
|
+
# Type checkers recognize .hex as a valid attribute
|
|
281
|
+
reveal_type(Color.RED.hex) # str
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Migration from stdlib
|
|
285
|
+
|
|
286
|
+
Just change one import:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
# Before
|
|
290
|
+
from enum import Enum
|
|
291
|
+
|
|
292
|
+
# After
|
|
293
|
+
from enumplus import Enum
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
All existing enum code continues to work — `Enum["RED"]`, `Enum("red")`, `list(Enum)`, `len(Enum)`, `@unique`, `auto()`, `isinstance` checks, everything.
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
enumplus/__init__.py,sha256=E-sfSE4vS4wUpc6jjCS2mq-DqVrVzMzKql7U6O0V-vo,191
|
|
2
|
+
enumplus/enum.py,sha256=EEusNqwxj0_SIsTDXirMolPfGmF1rEKOSSSyZZyam9I,6072
|
|
3
|
+
enumplus/pydantic.py,sha256=6SNeR7HRwv434IiaD3mVPrVDV2AoG1vVnYeHhL3E2FA,789
|
|
4
|
+
enumplus/serialize.py,sha256=u3jqLZKVYqEGkLIf3xwdrlJP2_pUc1e7VMQxsOLEWYc,953
|
|
5
|
+
enumplus-1.0.0.dist-info/METADATA,sha256=Jy4YsVUPfuvg08CfN1b2npBuGxozvJcsEZ0QKLxqmK0,7379
|
|
6
|
+
enumplus-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
7
|
+
enumplus-1.0.0.dist-info/licenses/LICENSE,sha256=2avxMtQSuO0Ok5IE7aIJP5luiqV6eIKG35wu3NI-rEM,1073
|
|
8
|
+
enumplus-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mathias Paulenko
|
|
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.
|