enumplus 1.2.0__tar.gz → 1.2.1__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.
- {enumplus-1.2.0 → enumplus-1.2.1}/CHANGELOG.md +16 -1
- {enumplus-1.2.0 → enumplus-1.2.1}/PKG-INFO +1 -1
- {enumplus-1.2.0 → enumplus-1.2.1}/SECURITY.md +1 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/__init__.py +7 -1
- {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/enum.py +40 -11
- {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/serialize.py +17 -25
- {enumplus-1.2.0 → enumplus-1.2.1}/pyproject.toml +1 -1
- enumplus-1.2.1/tests/test_edge_cases.py +1363 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_enum.py +2 -2
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_new_features.py +35 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_public_api.py +3 -1
- enumplus-1.2.0/tests/test_edge_cases.py +0 -581
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/FUNDING.yml +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/workflows/ci.yml +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.github/workflows/release.yml +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/.gitignore +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/AGENTS.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/CODE_OF_CONDUCT.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/CONTRIBUTING.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/LICENSE +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/README.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/SUPPORT.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/py.typed +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/pydantic.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/__init__.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_compatibility.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_metadata_unpacking.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_ordered.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_pydantic.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_serialize.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_type_hints.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.1] - 2026-08-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `to_dict()` and `to_json()` no longer double-evaluate callable labels — the callable is invoked exactly once and the result is reused in both the top-level `label` field and the `metadata` dict.
|
|
15
|
+
- `__version__` is now loaded dynamically from package metadata via `importlib.metadata`, eliminating version mismatch between `__init__.py` and `pyproject.toml`.
|
|
16
|
+
- `__contains__`, `from_value()`, `is_valid()`, and `get()` no longer produce false positives when passed a foreign enum member with the same value as a member of the queried enum.
|
|
17
|
+
- `__contains__`, `from_value()`, `is_valid()`, and `get()` now correctly handle members whose value is an enum member from another class. Previously, the foreign-enum guard rejected all foreign enum members, making it impossible to look up such members by value.
|
|
18
|
+
- `to_json()` now uses `SerializableEncoder` internally, so members whose value (or metadata) is an enum member from another class are serialized correctly instead of raising `TypeError`.
|
|
19
|
+
- `_safe_equal()` now catches all exceptions (not just `TypeError` and `ValueError`) from custom `__eq__` implementations, preventing crashes when comparing with objects that raise unexpected exceptions.
|
|
20
|
+
- Falsy but valid labels (`0`, `""`, `False`) are now respected instead of being silently replaced by `name.title()`. Only `None` and missing labels trigger the fallback.
|
|
21
|
+
- Removed dead `_serialize_value` function from `serialize.py`.
|
|
22
|
+
- Updated `SECURITY.md` to include `1.2.x` in supported versions.
|
|
23
|
+
|
|
10
24
|
## [1.2.0] - 2026-08-23
|
|
11
25
|
|
|
12
26
|
### Fixed
|
|
@@ -70,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
70
84
|
- Pydantic v2 integration via `__get_pydantic_core_schema__`.
|
|
71
85
|
- Full test suite, CI, and release workflows.
|
|
72
86
|
|
|
73
|
-
[Unreleased]: https://github.com/MathiasPaulenko/enumpy/compare/v1.2.
|
|
87
|
+
[Unreleased]: https://github.com/MathiasPaulenko/enumpy/compare/v1.2.1...HEAD
|
|
88
|
+
[1.2.1]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.2.1
|
|
74
89
|
[1.2.0]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.2.0
|
|
75
90
|
[1.1.0]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.1.0
|
|
76
91
|
[1.0.0]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: enumplus
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Enhanced Python enums with metadata, serialization, and choices
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/enumplus/
|
|
6
6
|
Project-URL: Repository, https://github.com/MathiasPaulenko/enumpy
|
|
@@ -4,9 +4,15 @@ Drop-in replacement for ``enum.Enum`` with labels, metadata, serialization,
|
|
|
4
4
|
and Pydantic v2 integration.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from importlib.metadata import PackageNotFoundError
|
|
8
|
+
from importlib.metadata import version as _pkg_version
|
|
9
|
+
|
|
7
10
|
from enumplus.enum import Enum, OrderedEnum
|
|
8
11
|
from enumplus.serialize import SerializableEncoder
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
try:
|
|
14
|
+
__version__ = _pkg_version("enumplus")
|
|
15
|
+
except PackageNotFoundError: # pragma: no cover
|
|
16
|
+
__version__ = "0.0.0.dev0"
|
|
11
17
|
|
|
12
18
|
__all__ = ["Enum", "OrderedEnum", "SerializableEncoder", "__version__"]
|
|
@@ -58,20 +58,21 @@ class _EnumPlusDict(EnumDict):
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _safe_equal(a: Any, b: Any) -> bool:
|
|
61
|
-
"""Return ``a == b`` as a bool, swallowing
|
|
61
|
+
"""Return ``a == b`` as a bool, swallowing comparison errors.
|
|
62
62
|
|
|
63
63
|
Handles values whose ``__eq__`` may raise (e.g. NumPy arrays with shape
|
|
64
|
-
mismatches
|
|
64
|
+
mismatches, custom objects with side-effecting ``__eq__``) or return
|
|
65
|
+
non-boolean objects (e.g. arrays, ``NotImplemented``).
|
|
65
66
|
"""
|
|
66
67
|
try:
|
|
67
68
|
result = a == b
|
|
68
|
-
except
|
|
69
|
+
except Exception:
|
|
69
70
|
return False
|
|
70
71
|
if result is NotImplemented:
|
|
71
72
|
return False
|
|
72
73
|
try:
|
|
73
74
|
return bool(result)
|
|
74
|
-
except
|
|
75
|
+
except Exception:
|
|
75
76
|
return False
|
|
76
77
|
|
|
77
78
|
|
|
@@ -147,7 +148,7 @@ class EnumMeta(enum.EnumMeta):
|
|
|
147
148
|
member._metadata_ = metadata
|
|
148
149
|
|
|
149
150
|
label = metadata.get("label")
|
|
150
|
-
if
|
|
151
|
+
if label is None:
|
|
151
152
|
member._label_ = member.name.title()
|
|
152
153
|
else:
|
|
153
154
|
member._label_ = label
|
|
@@ -160,6 +161,13 @@ class EnumMeta(enum.EnumMeta):
|
|
|
160
161
|
if isinstance(item, cls):
|
|
161
162
|
return True
|
|
162
163
|
member: Any
|
|
164
|
+
if isinstance(item, enum.Enum):
|
|
165
|
+
# Foreign enum member: only match if a member's value IS this
|
|
166
|
+
# exact enum member (identity), not just equal by value.
|
|
167
|
+
for member in cls:
|
|
168
|
+
if member.value is item:
|
|
169
|
+
return True
|
|
170
|
+
return False
|
|
163
171
|
for member in cls:
|
|
164
172
|
if _safe_equal(member.value, item):
|
|
165
173
|
return True
|
|
@@ -185,7 +193,7 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
185
193
|
- Pydantic v2 integration
|
|
186
194
|
"""
|
|
187
195
|
|
|
188
|
-
_label_:
|
|
196
|
+
_label_: Any
|
|
189
197
|
_metadata_: dict[str, Any]
|
|
190
198
|
_index_: int
|
|
191
199
|
|
|
@@ -272,6 +280,16 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
272
280
|
ValueError: If no match is found and no default is provided.
|
|
273
281
|
"""
|
|
274
282
|
member: Any
|
|
283
|
+
if isinstance(value, enum.Enum) and not isinstance(value, cls):
|
|
284
|
+
# Foreign enum member: only match if a member's value IS this
|
|
285
|
+
# exact enum member (identity), not just equal by value.
|
|
286
|
+
for member in cls:
|
|
287
|
+
if member.value is value:
|
|
288
|
+
return member
|
|
289
|
+
if default is not _SENTINEL:
|
|
290
|
+
return default
|
|
291
|
+
raise ValueError(f"{value!r} is not a valid {cls.__name__} value")
|
|
292
|
+
|
|
275
293
|
if case_insensitive and isinstance(value, str):
|
|
276
294
|
lowered = value.lower()
|
|
277
295
|
for member in cls:
|
|
@@ -336,6 +354,13 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
336
354
|
@classmethod
|
|
337
355
|
def is_valid(cls, value: Any) -> bool:
|
|
338
356
|
"""Return ``True`` if ``value`` is a valid member or member value."""
|
|
357
|
+
if isinstance(value, enum.Enum) and not isinstance(value, cls):
|
|
358
|
+
# Foreign enum member: only match if a member's value IS this
|
|
359
|
+
# exact enum member (identity), not just equal by value.
|
|
360
|
+
for member in cls:
|
|
361
|
+
if member.value is value:
|
|
362
|
+
return True
|
|
363
|
+
return False
|
|
339
364
|
for member in cls:
|
|
340
365
|
if member is value or _safe_equal(member.value, value):
|
|
341
366
|
return True
|
|
@@ -424,13 +449,17 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
424
449
|
"""Serialize the enum to a nested dict with ``value``, ``label``, ``metadata``."""
|
|
425
450
|
result: dict[str, dict[str, Any]] = {}
|
|
426
451
|
for member in cls:
|
|
452
|
+
label = member.label
|
|
453
|
+
metadata: dict[str, Any] = {}
|
|
454
|
+
for k, v in member._metadata_.items():
|
|
455
|
+
if k == "label" and callable(v):
|
|
456
|
+
metadata[k] = label
|
|
457
|
+
else:
|
|
458
|
+
metadata[k] = v
|
|
427
459
|
result[member.name] = {
|
|
428
460
|
"value": member.value,
|
|
429
|
-
"label":
|
|
430
|
-
"metadata":
|
|
431
|
-
k: (v() if callable(v) and k == "label" else v)
|
|
432
|
-
for k, v in member._metadata_.items()
|
|
433
|
-
},
|
|
461
|
+
"label": label,
|
|
462
|
+
"metadata": metadata,
|
|
434
463
|
}
|
|
435
464
|
return result
|
|
436
465
|
|
|
@@ -8,18 +8,6 @@ from enumplus.enum import Enum
|
|
|
8
8
|
__all__ = ["SerializableEncoder", "from_json", "to_json"]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _serialize_value(value: Any, *, evaluate_callable: bool = False) -> Any:
|
|
12
|
-
"""Serialize a single metadata value.
|
|
13
|
-
|
|
14
|
-
Only callables in the ``label`` key are evaluated; all other values are
|
|
15
|
-
returned as-is so that function references or class objects stored in
|
|
16
|
-
metadata are not accidentally invoked.
|
|
17
|
-
"""
|
|
18
|
-
if evaluate_callable and callable(value):
|
|
19
|
-
return value()
|
|
20
|
-
return value
|
|
21
|
-
|
|
22
|
-
|
|
23
11
|
def to_json(cls: type[Enum]) -> str:
|
|
24
12
|
"""Serialize an enum class to a pretty-printed JSON string.
|
|
25
13
|
|
|
@@ -32,22 +20,26 @@ def to_json(cls: type[Enum]) -> str:
|
|
|
32
20
|
]
|
|
33
21
|
}
|
|
34
22
|
"""
|
|
23
|
+
members: list[dict[str, Any]] = []
|
|
24
|
+
for member in cls:
|
|
25
|
+
label = member.label
|
|
26
|
+
metadata: dict[str, Any] = {}
|
|
27
|
+
for k, v in member._metadata_.items():
|
|
28
|
+
if k == "label" and callable(v):
|
|
29
|
+
metadata[k] = label
|
|
30
|
+
else:
|
|
31
|
+
metadata[k] = v
|
|
32
|
+
members.append({
|
|
33
|
+
"name": member.name,
|
|
34
|
+
"value": member.value,
|
|
35
|
+
"label": label,
|
|
36
|
+
"metadata": metadata,
|
|
37
|
+
})
|
|
35
38
|
data: dict[str, Any] = {
|
|
36
39
|
"name": cls.__name__,
|
|
37
|
-
"members":
|
|
38
|
-
{
|
|
39
|
-
"name": member.name,
|
|
40
|
-
"value": member.value,
|
|
41
|
-
"label": member.label,
|
|
42
|
-
"metadata": {
|
|
43
|
-
k: _serialize_value(v, evaluate_callable=(k == "label"))
|
|
44
|
-
for k, v in member._metadata_.items()
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
for member in cls
|
|
48
|
-
],
|
|
40
|
+
"members": members,
|
|
49
41
|
}
|
|
50
|
-
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
42
|
+
return json.dumps(data, ensure_ascii=False, indent=2, cls=SerializableEncoder)
|
|
51
43
|
|
|
52
44
|
|
|
53
45
|
def from_json(data: str) -> dict[str, Any]:
|