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.
Files changed (35) hide show
  1. {enumplus-1.2.0 → enumplus-1.2.1}/CHANGELOG.md +16 -1
  2. {enumplus-1.2.0 → enumplus-1.2.1}/PKG-INFO +1 -1
  3. {enumplus-1.2.0 → enumplus-1.2.1}/SECURITY.md +1 -0
  4. {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/__init__.py +7 -1
  5. {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/enum.py +40 -11
  6. {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/serialize.py +17 -25
  7. {enumplus-1.2.0 → enumplus-1.2.1}/pyproject.toml +1 -1
  8. enumplus-1.2.1/tests/test_edge_cases.py +1363 -0
  9. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_enum.py +2 -2
  10. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_new_features.py +35 -0
  11. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_public_api.py +3 -1
  12. enumplus-1.2.0/tests/test_edge_cases.py +0 -581
  13. {enumplus-1.2.0 → enumplus-1.2.1}/.github/FUNDING.yml +0 -0
  14. {enumplus-1.2.0 → enumplus-1.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {enumplus-1.2.0 → enumplus-1.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  16. {enumplus-1.2.0 → enumplus-1.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {enumplus-1.2.0 → enumplus-1.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {enumplus-1.2.0 → enumplus-1.2.1}/.github/workflows/ci.yml +0 -0
  19. {enumplus-1.2.0 → enumplus-1.2.1}/.github/workflows/release.yml +0 -0
  20. {enumplus-1.2.0 → enumplus-1.2.1}/.gitignore +0 -0
  21. {enumplus-1.2.0 → enumplus-1.2.1}/AGENTS.md +0 -0
  22. {enumplus-1.2.0 → enumplus-1.2.1}/CODE_OF_CONDUCT.md +0 -0
  23. {enumplus-1.2.0 → enumplus-1.2.1}/CONTRIBUTING.md +0 -0
  24. {enumplus-1.2.0 → enumplus-1.2.1}/LICENSE +0 -0
  25. {enumplus-1.2.0 → enumplus-1.2.1}/README.md +0 -0
  26. {enumplus-1.2.0 → enumplus-1.2.1}/SUPPORT.md +0 -0
  27. {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/py.typed +0 -0
  28. {enumplus-1.2.0 → enumplus-1.2.1}/enumplus/pydantic.py +0 -0
  29. {enumplus-1.2.0 → enumplus-1.2.1}/tests/__init__.py +0 -0
  30. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_compatibility.py +0 -0
  31. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_metadata_unpacking.py +0 -0
  32. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_ordered.py +0 -0
  33. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_pydantic.py +0 -0
  34. {enumplus-1.2.0 → enumplus-1.2.1}/tests/test_serialize.py +0 -0
  35. {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.0...HEAD
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.0
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,6 +4,7 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |---------|--------------------|
7
+ | 1.2.x | :white_check_mark: |
7
8
  | 1.1.x | :white_check_mark: |
8
9
  | < 1.1 | :x: |
9
10
 
@@ -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
- __version__ = "1.1.0"
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 shape/length comparison errors.
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) or return non-boolean objects (e.g. arrays, ``NotImplemented``).
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 (TypeError, ValueError):
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 (TypeError, ValueError):
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 not label:
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_: str
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": member.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]:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "enumplus"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Enhanced Python enums with metadata, serialization, and choices"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"