enumplus 1.2.1__tar.gz → 1.2.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.
Files changed (34) hide show
  1. {enumplus-1.2.1 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. {enumplus-1.2.1 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/config.yml +2 -2
  3. {enumplus-1.2.1 → enumplus-1.2.2}/.github/workflows/release.yml +0 -1
  4. {enumplus-1.2.1 → enumplus-1.2.2}/AGENTS.md +7 -3
  5. {enumplus-1.2.1 → enumplus-1.2.2}/CHANGELOG.md +32 -5
  6. {enumplus-1.2.1 → enumplus-1.2.2}/CONTRIBUTING.md +4 -4
  7. {enumplus-1.2.1 → enumplus-1.2.2}/LICENSE +1 -1
  8. {enumplus-1.2.1 → enumplus-1.2.2}/PKG-INFO +22 -10
  9. {enumplus-1.2.1 → enumplus-1.2.2}/README.md +18 -6
  10. {enumplus-1.2.1 → enumplus-1.2.2}/SUPPORT.md +4 -4
  11. {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/enum.py +24 -10
  12. {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/serialize.py +5 -2
  13. {enumplus-1.2.1 → enumplus-1.2.2}/pyproject.toml +4 -4
  14. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_edge_cases.py +132 -1
  15. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_enum.py +1 -1
  16. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_public_api.py +8 -2
  17. enumplus-1.2.1/.github/FUNDING.yml +0 -3
  18. {enumplus-1.2.1 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  19. {enumplus-1.2.1 → enumplus-1.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {enumplus-1.2.1 → enumplus-1.2.2}/.github/workflows/ci.yml +0 -0
  21. {enumplus-1.2.1 → enumplus-1.2.2}/.gitignore +0 -0
  22. {enumplus-1.2.1 → enumplus-1.2.2}/CODE_OF_CONDUCT.md +0 -0
  23. {enumplus-1.2.1 → enumplus-1.2.2}/SECURITY.md +0 -0
  24. {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/__init__.py +0 -0
  25. {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/py.typed +0 -0
  26. {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/pydantic.py +0 -0
  27. {enumplus-1.2.1 → enumplus-1.2.2}/tests/__init__.py +0 -0
  28. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_compatibility.py +0 -0
  29. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_metadata_unpacking.py +0 -0
  30. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_new_features.py +0 -0
  31. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_ordered.py +0 -0
  32. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_pydantic.py +0 -0
  33. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_serialize.py +0 -0
  34. {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_type_hints.py +0 -0
@@ -31,7 +31,7 @@ A clear and concise description of what actually happened.
31
31
  ## Environment
32
32
 
33
33
  - Python version: [e.g. 3.12.1]
34
- - enumplus version: [e.g. 1.1.0]
34
+ - enumplus version: [e.g. 1.2.1]
35
35
  - OS: [e.g. Ubuntu 22.04, Windows 11, macOS 14]
36
36
 
37
37
  ## Additional context
@@ -1,8 +1,8 @@
1
1
  blank_issues_enabled: false
2
2
  contact_links:
3
3
  - name: GitHub Discussions
4
- url: https://github.com/MathiasPaulenko/enumpy/discussions
4
+ url: https://github.com/MathiasPaulenko/enumplus/discussions
5
5
  about: Ask questions and discuss ideas with the community.
6
6
  - name: Security Policy
7
- url: https://github.com/MathiasPaulenko/enumpy/blob/main/SECURITY.md
7
+ url: https://github.com/MathiasPaulenko/enumplus/blob/main/SECURITY.md
8
8
  about: Report security vulnerabilities privately — do not open a public issue.
@@ -96,7 +96,6 @@ jobs:
96
96
  DOCS=$(git log --pretty=format:"- %s" ${RANGE} --grep="^docs" --grep="^readme" -i || true)
97
97
  TESTS=$(git log --pretty=format:"- %s" ${RANGE} --grep="^test" -i || true)
98
98
  CHORE=$(git log --pretty=format:"- %s" ${RANGE} --grep="^chore" --grep="^ci" --grep="^build" -i || true)
99
- RELEASE=$(git log --pretty=format:"- %s" ${RANGE} --grep="^release" -i || true)
100
99
 
101
100
  {
102
101
  echo "body<<EOF"
@@ -22,6 +22,10 @@ Requires Python `>=3.11`.
22
22
  - Keep class config keys such as `serialize_by_name` from becoming enum members
23
23
  or affecting `auto()` generation.
24
24
  - Support `auto()` following a metadata tuple on Python 3.11–3.14.
25
+ - `serialize_by_name` is a reserved class config key: it must be a `bool`
26
+ (`TypeError` otherwise) and can never be a member name.
27
+ - Auto-generated labels use `name.replace("_", " ").title()`. `filter(label=...)`
28
+ matches the evaluated label, not the raw metadata dict.
25
29
  - `Enum.__eq__` and `EnumMeta.__contains__` use `_safe_equal`, which coerces
26
30
  `a == b` to a `bool` without raising on shape/length mismatches (e.g. NumPy
27
31
  arrays, unhashable container values).
@@ -67,7 +71,7 @@ py -3.11 -m mypy --strict enumplus/ tests/
67
71
 
68
72
  ## Known constraints
69
73
 
70
- - The git remote is `MathiasPaulenko/enumpy`, while the package name and PyPI
71
- project are `enumplus`. Package URLs in `pyproject.toml` point to the actual
72
- remote repository; update if the repository is renamed.
74
+ - The repository was renamed from `enumpy` to `enumplus` on GitHub. All
75
+ project URLs point to `MathiasPaulenko/enumplus`; old `enumpy` links
76
+ redirect automatically.
73
77
  - `ref/` is intentionally ignored (local reference material). Do not commit it.
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.2] - 2026-10-06
11
+
12
+ ### Fixed
13
+
14
+ - `from_value()`, `get()`, and `validate()` now return the member itself when
15
+ passed a member of the queried enum whose value is a foreign enum member.
16
+ Previously they raised `ValueError` (or returned the default) even though
17
+ `is_valid()` and `in` correctly accepted the member.
18
+ - `is_valid()` and `from_value()` now accept members of subclass enums, matching
19
+ the behavior of `in` (e.g. `Base.is_valid(Child.MEMBER)` is `True` for an
20
+ empty `Base`).
21
+ - `SerializableEncoder` now also serializes stdlib `enum.Enum` members, not just
22
+ enumplus members. `to_json()` and `json.dumps()` no longer raise `TypeError`
23
+ when a member's value or metadata is a stdlib enum member.
24
+
25
+ ### Changed
26
+
27
+ - Auto-generated labels now replace underscores with spaces: `IN_PROGRESS`
28
+ produces `"In Progress"` instead of `"In_Progress"`.
29
+ - `filter(label=...)` now matches against the member's evaluated label, so
30
+ callable and auto-generated labels can be filtered.
31
+ - `serialize_by_name` now raises `TypeError` when set to a non-`bool` value
32
+ instead of being silently swallowed as a member assignment.
33
+ - `OrderedEnum` comparison operators are now typed as returning
34
+ `bool | NotImplementedType`.
35
+
10
36
  ## [1.2.1] - 2026-08-23
11
37
 
12
38
  ### Fixed
@@ -84,8 +110,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
84
110
  - Pydantic v2 integration via `__get_pydantic_core_schema__`.
85
111
  - Full test suite, CI, and release workflows.
86
112
 
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
89
- [1.2.0]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.2.0
90
- [1.1.0]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.1.0
91
- [1.0.0]: https://github.com/MathiasPaulenko/enumpy/releases/tag/v1.0.0
113
+ [Unreleased]: https://github.com/MathiasPaulenko/enumplus/compare/v1.2.2...HEAD
114
+ [1.2.2]: https://github.com/MathiasPaulenko/enumplus/releases/tag/v1.2.2
115
+ [1.2.1]: https://github.com/MathiasPaulenko/enumplus/releases/tag/v1.2.1
116
+ [1.2.0]: https://github.com/MathiasPaulenko/enumplus/releases/tag/v1.2.0
117
+ [1.1.0]: https://github.com/MathiasPaulenko/enumplus/releases/tag/v1.1.0
118
+ [1.0.0]: https://github.com/MathiasPaulenko/enumplus/releases/tag/v1.0.0
@@ -7,8 +7,8 @@ Thank you for your interest in contributing to enumplus! This document describes
7
7
  1. Fork the repository on GitHub.
8
8
  2. Clone your fork locally:
9
9
  ```bash
10
- git clone https://github.com/<your-username>/enumpy.git
11
- cd enumpy
10
+ git clone https://github.com/<your-username>/enumplus.git
11
+ cd enumplus
12
12
  ```
13
13
  3. Create a virtual environment and install dev dependencies:
14
14
  ```bash
@@ -17,7 +17,7 @@ Thank you for your interest in contributing to enumplus! This document describes
17
17
  pip install -e ".[dev]"
18
18
  ```
19
19
 
20
- > **Note:** The git repository is named `enumpy`, while the package name is `enumplus`. This is expected — the repository name predates the package rename.
20
+ > **Note:** The repository was previously named `enumpy`; it has been renamed to `enumplus` to match the package name. Old links redirect automatically.
21
21
 
22
22
  ## Development Workflow
23
23
 
@@ -73,7 +73,7 @@ py -3.11 -m mypy --strict enumplus/ tests/
73
73
 
74
74
  ## Reporting Issues
75
75
 
76
- - Use [GitHub Issues](https://github.com/MathiasPaulenko/enumpy/issues) to report bugs or request features.
76
+ - Use [GitHub Issues](https://github.com/MathiasPaulenko/enumplus/issues) to report bugs or request features.
77
77
  - Use the provided issue templates for consistency.
78
78
 
79
79
  ## Code of Conduct
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Mathias Paulenko
3
+ Copyright (c) 2024-2026 Mathias Paulenko
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: enumplus
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Enhanced Python enums with metadata, serialization, and choices
5
5
  Project-URL: Homepage, https://pypi.org/project/enumplus/
6
- Project-URL: Repository, https://github.com/MathiasPaulenko/enumpy
7
- Project-URL: Issues, https://github.com/MathiasPaulenko/enumpy/issues
8
- Project-URL: Changelog, https://github.com/MathiasPaulenko/enumpy/blob/main/CHANGELOG.md
6
+ Project-URL: Repository, https://github.com/MathiasPaulenko/enumplus
7
+ Project-URL: Issues, https://github.com/MathiasPaulenko/enumplus/issues
8
+ Project-URL: Changelog, https://github.com/MathiasPaulenko/enumplus/blob/main/CHANGELOG.md
9
9
  Author-email: Mathias Paulenko <mathias.paulenko@outlook.com>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
36
36
  ![PyPI](https://img.shields.io/pypi/v/enumplus)
37
37
  ![Python](https://img.shields.io/badge/python-3.11+-blue)
38
38
  ![License](https://img.shields.io/badge/license-MIT-green)
39
- ![Tests](https://github.com/MathiasPaulenko/enumpy/actions/workflows/ci.yml/badge.svg)
39
+ ![Tests](https://github.com/MathiasPaulenko/enumplus/actions/workflows/ci.yml/badge.svg)
40
40
 
41
41
  Python's `enum.Enum` is basic. `enumplus` adds display names, metadata, JSON
42
42
  serialization, `choices()`, and value-based comparison — all with zero runtime
@@ -125,12 +125,12 @@ print("red" in Color) # True
125
125
 
126
126
  ### Display Names (label)
127
127
 
128
- Every member gets a human-readable label, auto-generated from the member name or set explicitly via metadata.
128
+ Every member gets a human-readable label, auto-generated from the member name or set explicitly via metadata. Auto-generated labels replace underscores with spaces (`IN_PROGRESS` becomes `"In Progress"`).
129
129
 
130
130
  ```python
131
131
  class Status(Enum):
132
- PENDING = "pending" # label: "Pending"
133
- IN_PROGRESS = ("in_progress", {"label": "In Progress"})
132
+ PENDING = "pending" # label: "Pending"
133
+ IN_PROGRESS = "in_progress" # label: "In Progress"
134
134
 
135
135
  print(Status.PENDING.label) # "Pending"
136
136
  print(Status.IN_PROGRESS.label) # "In Progress"
@@ -220,7 +220,7 @@ Color.keys() # ["RED", "GREEN"]
220
220
 
221
221
  ### filter()
222
222
 
223
- Filter members by metadata key-value pairs (AND logic).
223
+ Filter members by metadata key-value pairs (AND logic). `label` is special: it matches against the evaluated label (including auto-generated and callable labels), not the raw metadata dict.
224
224
 
225
225
  ```python
226
226
  class Color(Enum):
@@ -230,6 +230,7 @@ class Color(Enum):
230
230
  Color.filter(category="warm") # [Color.RED]
231
231
  Color.filter(hex="#FF0000", category="warm") # [Color.RED]
232
232
  Color.filter() # [Color.RED, Color.GREEN]
233
+ Color.filter(label="Red") # [Color.RED]
233
234
  ```
234
235
 
235
236
  ### Comparison with values (==)
@@ -433,7 +434,7 @@ print(Color.labels()) # ["Rojo", "Verde"]
433
434
  print(Color.choices()) # [("red", "Rojo"), ("green", "Verde")]
434
435
  ```
435
436
 
436
- Callable labels work everywhere: `choices()`, `to_dict()`, `to_json()`, `labels()`, and `str()`.
437
+ Callable labels work everywhere: `choices()`, `to_dict()`, `to_json()`, `labels()`, `filter(label=...)`, and `str()`.
437
438
 
438
439
  ### Type hints in metadata
439
440
 
@@ -465,6 +466,17 @@ from enumplus import Enum
465
466
 
466
467
  All existing enum code continues to work — `Enum["RED"]`, `Enum("red")`, `list(Enum)`, `len(Enum)`, `@unique`, `auto()`, `isinstance` checks, everything.
467
468
 
469
+ A few deliberate differences from `enum.Enum` to keep in mind:
470
+
471
+ - `str(member)` returns the label, not `"ClassName.MEMBER"` (`repr()` shows
472
+ `<ClassName.MEMBER: value>`), and `member == value` compares by value.
473
+ - `serialize_by_name` is a reserved class attribute for Pydantic config — it
474
+ can't be used as a member name and must be a `bool`.
475
+ - Metadata keys that collide with real attributes (`name`, `value`, `label`,
476
+ `metadata`) are only reachable via `member.metadata`, not as attributes.
477
+ - Metadata declared on an alias is ignored — an alias shares the canonical
478
+ member's label and metadata.
479
+
468
480
  ## Changelog
469
481
 
470
482
  See [CHANGELOG.md](CHANGELOG.md) for a full history of changes.
@@ -3,7 +3,7 @@
3
3
  ![PyPI](https://img.shields.io/pypi/v/enumplus)
4
4
  ![Python](https://img.shields.io/badge/python-3.11+-blue)
5
5
  ![License](https://img.shields.io/badge/license-MIT-green)
6
- ![Tests](https://github.com/MathiasPaulenko/enumpy/actions/workflows/ci.yml/badge.svg)
6
+ ![Tests](https://github.com/MathiasPaulenko/enumplus/actions/workflows/ci.yml/badge.svg)
7
7
 
8
8
  Python's `enum.Enum` is basic. `enumplus` adds display names, metadata, JSON
9
9
  serialization, `choices()`, and value-based comparison — all with zero runtime
@@ -92,12 +92,12 @@ print("red" in Color) # True
92
92
 
93
93
  ### Display Names (label)
94
94
 
95
- Every member gets a human-readable label, auto-generated from the member name or set explicitly via metadata.
95
+ Every member gets a human-readable label, auto-generated from the member name or set explicitly via metadata. Auto-generated labels replace underscores with spaces (`IN_PROGRESS` becomes `"In Progress"`).
96
96
 
97
97
  ```python
98
98
  class Status(Enum):
99
- PENDING = "pending" # label: "Pending"
100
- IN_PROGRESS = ("in_progress", {"label": "In Progress"})
99
+ PENDING = "pending" # label: "Pending"
100
+ IN_PROGRESS = "in_progress" # label: "In Progress"
101
101
 
102
102
  print(Status.PENDING.label) # "Pending"
103
103
  print(Status.IN_PROGRESS.label) # "In Progress"
@@ -187,7 +187,7 @@ Color.keys() # ["RED", "GREEN"]
187
187
 
188
188
  ### filter()
189
189
 
190
- Filter members by metadata key-value pairs (AND logic).
190
+ Filter members by metadata key-value pairs (AND logic). `label` is special: it matches against the evaluated label (including auto-generated and callable labels), not the raw metadata dict.
191
191
 
192
192
  ```python
193
193
  class Color(Enum):
@@ -197,6 +197,7 @@ class Color(Enum):
197
197
  Color.filter(category="warm") # [Color.RED]
198
198
  Color.filter(hex="#FF0000", category="warm") # [Color.RED]
199
199
  Color.filter() # [Color.RED, Color.GREEN]
200
+ Color.filter(label="Red") # [Color.RED]
200
201
  ```
201
202
 
202
203
  ### Comparison with values (==)
@@ -400,7 +401,7 @@ print(Color.labels()) # ["Rojo", "Verde"]
400
401
  print(Color.choices()) # [("red", "Rojo"), ("green", "Verde")]
401
402
  ```
402
403
 
403
- Callable labels work everywhere: `choices()`, `to_dict()`, `to_json()`, `labels()`, and `str()`.
404
+ Callable labels work everywhere: `choices()`, `to_dict()`, `to_json()`, `labels()`, `filter(label=...)`, and `str()`.
404
405
 
405
406
  ### Type hints in metadata
406
407
 
@@ -432,6 +433,17 @@ from enumplus import Enum
432
433
 
433
434
  All existing enum code continues to work — `Enum["RED"]`, `Enum("red")`, `list(Enum)`, `len(Enum)`, `@unique`, `auto()`, `isinstance` checks, everything.
434
435
 
436
+ A few deliberate differences from `enum.Enum` to keep in mind:
437
+
438
+ - `str(member)` returns the label, not `"ClassName.MEMBER"` (`repr()` shows
439
+ `<ClassName.MEMBER: value>`), and `member == value` compares by value.
440
+ - `serialize_by_name` is a reserved class attribute for Pydantic config — it
441
+ can't be used as a member name and must be a `bool`.
442
+ - Metadata keys that collide with real attributes (`name`, `value`, `label`,
443
+ `metadata`) are only reachable via `member.metadata`, not as attributes.
444
+ - Metadata declared on an alias is ignored — an alias shares the canonical
445
+ member's label and metadata.
446
+
435
447
  ## Changelog
436
448
 
437
449
  See [CHANGELOG.md](CHANGELOG.md) for a full history of changes.
@@ -3,12 +3,12 @@
3
3
  ## Getting Help
4
4
 
5
5
  - **Documentation**: Start with the [README](README.md) for usage examples and API reference.
6
- - **Issues**: Search [existing issues](https://github.com/MathiasPaulenko/enumpy/issues) to see if your question has already been answered.
7
- - **Discussions**: Use [GitHub Discussions](https://github.com/MathiasPaulenko/enumpy/discussions) for questions, ideas, and general discussion.
6
+ - **Issues**: Search [existing issues](https://github.com/MathiasPaulenko/enumplus/issues) to see if your question has already been answered.
7
+ - **Discussions**: Use [GitHub Discussions](https://github.com/MathiasPaulenko/enumplus/discussions) for questions, ideas, and general discussion.
8
8
 
9
9
  ## Reporting a Bug
10
10
 
11
- Open a [bug report issue](https://github.com/MathiasPaulenko/enumpy/issues/new?template=bug_report.md).
11
+ Open a [bug report issue](https://github.com/MathiasPaulenko/enumplus/issues/new?template=bug_report.md).
12
12
  Include:
13
13
 
14
14
  - Python version
@@ -18,7 +18,7 @@ Include:
18
18
 
19
19
  ## Requesting a Feature
20
20
 
21
- Open a [feature request issue](https://github.com/MathiasPaulenko/enumpy/issues/new?template=feature_request.md).
21
+ Open a [feature request issue](https://github.com/MathiasPaulenko/enumplus/issues/new?template=feature_request.md).
22
22
 
23
23
  ## Security Issues
24
24
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  import sys
5
+ from types import NotImplementedType
5
6
  from typing import Any, Self, TypeVar, dataclass_transform, overload
6
7
 
7
8
  if sys.version_info >= (3, 13):
@@ -39,6 +40,11 @@ class _EnumPlusDict(EnumDict):
39
40
 
40
41
  def __setitem__(self, key: str, value: Any) -> None:
41
42
  if key in self._CONFIG_KEYS:
43
+ if not isinstance(value, bool):
44
+ raise TypeError(
45
+ f"{key} is a reserved class config key and must be a bool, "
46
+ f"got {type(value).__name__}"
47
+ )
42
48
  self._class_config[key] = value
43
49
  # Store as a class attribute, but do not register as a member and
44
50
  # do not affect _last_values used by auto().
@@ -149,7 +155,7 @@ class EnumMeta(enum.EnumMeta):
149
155
 
150
156
  label = metadata.get("label")
151
157
  if label is None:
152
- member._label_ = member.name.title()
158
+ member._label_ = member.name.replace("_", " ").title()
153
159
  else:
154
160
  member._label_ = label
155
161
 
@@ -202,7 +208,7 @@ class Enum(enum.Enum, metaclass=EnumMeta):
202
208
  """Human-readable label for this member.
203
209
 
204
210
  If the label was set to a callable, it is evaluated on every access.
205
- Falls back to ``name.title()`` when no label is set.
211
+ Falls back to ``name.replace("_", " ").title()`` when no label is set.
206
212
  """
207
213
  label = self._label_
208
214
  if callable(label):
@@ -280,7 +286,9 @@ class Enum(enum.Enum, metaclass=EnumMeta):
280
286
  ValueError: If no match is found and no default is provided.
281
287
  """
282
288
  member: Any
283
- if isinstance(value, enum.Enum) and not isinstance(value, cls):
289
+ if isinstance(value, cls):
290
+ return value
291
+ if isinstance(value, enum.Enum):
284
292
  # Foreign enum member: only match if a member's value IS this
285
293
  # exact enum member (identity), not just equal by value.
286
294
  for member in cls:
@@ -354,7 +362,9 @@ class Enum(enum.Enum, metaclass=EnumMeta):
354
362
  @classmethod
355
363
  def is_valid(cls, value: Any) -> bool:
356
364
  """Return ``True`` if ``value`` is a valid member or member value."""
357
- if isinstance(value, enum.Enum) and not isinstance(value, cls):
365
+ if isinstance(value, cls):
366
+ return True
367
+ if isinstance(value, enum.Enum):
358
368
  # Foreign enum member: only match if a member's value IS this
359
369
  # exact enum member (identity), not just equal by value.
360
370
  for member in cls:
@@ -467,14 +477,18 @@ class Enum(enum.Enum, metaclass=EnumMeta):
467
477
  def filter(cls, **kwargs: Any) -> list[Self]:
468
478
  """Filter members by metadata key-value pairs (AND logic).
469
479
 
470
- With no kwargs, returns all members.
480
+ ``label`` is matched against the member's evaluated label, so callable
481
+ labels work too. With no kwargs, returns all members.
471
482
  """
472
483
  if not kwargs:
473
484
  return list(cls)
474
485
  result: list[Self] = []
475
486
  for member in cls:
476
487
  if all(
477
- key in member._metadata_ and _safe_equal(member._metadata_[key], value)
488
+ _safe_equal(member.label, value)
489
+ if key == "label"
490
+ else key in member._metadata_
491
+ and _safe_equal(member._metadata_[key], value)
478
492
  for key, value in kwargs.items()
479
493
  ):
480
494
  result.append(member)
@@ -515,22 +529,22 @@ class OrderedEnum(Enum):
515
529
  Also works with ``sorted()``, ``min()``, and ``max()``.
516
530
  """
517
531
 
518
- def __lt__(self, other: object) -> bool:
532
+ def __lt__(self, other: object) -> bool | NotImplementedType:
519
533
  if not isinstance(other, type(self)):
520
534
  return NotImplemented
521
535
  return self._index_ < other._index_
522
536
 
523
- def __le__(self, other: object) -> bool:
537
+ def __le__(self, other: object) -> bool | NotImplementedType:
524
538
  if not isinstance(other, type(self)):
525
539
  return NotImplemented
526
540
  return self._index_ <= other._index_
527
541
 
528
- def __gt__(self, other: object) -> bool:
542
+ def __gt__(self, other: object) -> bool | NotImplementedType:
529
543
  if not isinstance(other, type(self)):
530
544
  return NotImplemented
531
545
  return self._index_ > other._index_
532
546
 
533
- def __ge__(self, other: object) -> bool:
547
+ def __ge__(self, other: object) -> bool | NotImplementedType:
534
548
  if not isinstance(other, type(self)):
535
549
  return NotImplemented
536
550
  return self._index_ >= other._index_
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import enum
3
4
  import json
4
5
  from typing import Any
5
6
 
@@ -65,7 +66,9 @@ def from_json(data: str) -> dict[str, Any]:
65
66
 
66
67
 
67
68
  class SerializableEncoder(json.JSONEncoder):
68
- """JSON encoder that serializes enumplus members to their values.
69
+ """JSON encoder that serializes enum members to their values.
70
+
71
+ Handles both enumplus members and stdlib ``enum.Enum`` members.
69
72
 
70
73
  Usage::
71
74
 
@@ -73,6 +76,6 @@ class SerializableEncoder(json.JSONEncoder):
73
76
  """
74
77
 
75
78
  def default(self, obj: Any) -> Any:
76
- if isinstance(obj, Enum):
79
+ if isinstance(obj, enum.Enum):
77
80
  return obj.value
78
81
  return super().default(obj)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "enumplus"
7
- version = "1.2.1"
7
+ version = "1.2.2"
8
8
  description = "Enhanced Python enums with metadata, serialization, and choices"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -27,9 +27,9 @@ classifiers = [
27
27
 
28
28
  [project.urls]
29
29
  Homepage = "https://pypi.org/project/enumplus/"
30
- Repository = "https://github.com/MathiasPaulenko/enumpy"
31
- Issues = "https://github.com/MathiasPaulenko/enumpy/issues"
32
- Changelog = "https://github.com/MathiasPaulenko/enumpy/blob/main/CHANGELOG.md"
30
+ Repository = "https://github.com/MathiasPaulenko/enumplus"
31
+ Issues = "https://github.com/MathiasPaulenko/enumplus/issues"
32
+ Changelog = "https://github.com/MathiasPaulenko/enumplus/blob/main/CHANGELOG.md"
33
33
 
34
34
  [project.optional-dependencies]
35
35
  pydantic = ["pydantic>=2.0"]
@@ -2,7 +2,9 @@
2
2
 
3
3
  Covers: pickle, copy, bool/int equivalence, NaN, Unicode, aliases,
4
4
  _ignore_, custom _generate_next_value_, subclassing, __getattr__,
5
- SerializableEncoder with None, empty/whitespace values, bytes values.
5
+ SerializableEncoder with None and stdlib enums, empty/whitespace values,
6
+ bytes values, foreign enum members as values, serialize_by_name validation,
7
+ and filter() matching on evaluated labels.
6
8
  """
7
9
  from __future__ import annotations
8
10
 
@@ -1361,3 +1363,132 @@ def test_to_json_with_nested_enum_member_as_value() -> None:
1361
1363
  data = json.loads(Outer.to_json())
1362
1364
  # Middle.A.value is Innermost.Z, which serializes to "z"
1363
1365
  assert data["members"][0]["value"] == "z"
1366
+
1367
+
1368
+ # ---------- Own member lookup when value is a foreign enum member ----------
1369
+
1370
+
1371
+ def test_from_value_own_member_with_enum_value() -> None:
1372
+ class Inner(Enum):
1373
+ X = "x"
1374
+
1375
+ class Outer(Enum):
1376
+ A = Inner.X
1377
+
1378
+ assert Outer.from_value(Outer.A) is Outer.A
1379
+
1380
+
1381
+ def test_get_own_member_with_enum_value() -> None:
1382
+ class Inner(Enum):
1383
+ X = "x"
1384
+
1385
+ class Outer(Enum):
1386
+ A = Inner.X
1387
+
1388
+ assert Outer.get(Outer.A) is Outer.A
1389
+
1390
+
1391
+ def test_validate_own_member_with_enum_value() -> None:
1392
+ class Inner(Enum):
1393
+ X = "x"
1394
+
1395
+ class Outer(Enum):
1396
+ A = Inner.X
1397
+
1398
+ assert Outer.validate(Outer.A) is Outer.A
1399
+
1400
+
1401
+ # ---------- Subclass member vs empty base consistency ----------
1402
+
1403
+
1404
+ def test_empty_base_is_valid_accepts_subclass_member() -> None:
1405
+ class Base(Enum):
1406
+ pass
1407
+
1408
+ class Child(Base):
1409
+ A = 1
1410
+
1411
+ # Child.A in Base is True (isinstance), so is_valid/from_value must agree
1412
+ assert Child.A in Base
1413
+ assert Base.is_valid(Child.A) is True
1414
+ assert Base.from_value(Child.A) is Child.A
1415
+
1416
+
1417
+ # ---------- SerializableEncoder with stdlib enum members ----------
1418
+
1419
+
1420
+ def test_encoder_stdlib_enum_member() -> None:
1421
+ class Std(enum.Enum):
1422
+ X = "x"
1423
+
1424
+ assert json.dumps(Std.X, cls=SerializableEncoder) == '"x"'
1425
+
1426
+
1427
+ def test_to_json_with_stdlib_enum_member_as_value() -> None:
1428
+ class Std(enum.Enum):
1429
+ X = "x"
1430
+
1431
+ class Outer(Enum):
1432
+ A = Std.X
1433
+
1434
+ data = json.loads(Outer.to_json())
1435
+ assert data["members"][0]["value"] == "x"
1436
+
1437
+
1438
+ def test_to_json_with_stdlib_enum_member_in_metadata() -> None:
1439
+ class Std(enum.Enum):
1440
+ X = "x"
1441
+
1442
+ class Outer(Enum):
1443
+ A = ("a", {"ref": Std.X})
1444
+
1445
+ data = json.loads(Outer.to_json())
1446
+ assert data["members"][0]["metadata"]["ref"] == "x"
1447
+
1448
+
1449
+ # ---------- serialize_by_name config validation ----------
1450
+
1451
+
1452
+ def test_serialize_by_name_non_bool_raises() -> None:
1453
+ with pytest.raises(TypeError, match="must be a bool"):
1454
+
1455
+ class C(Enum):
1456
+ serialize_by_name = "yes"
1457
+ A = 1
1458
+
1459
+
1460
+ def test_serialize_by_name_member_name_is_reserved() -> None:
1461
+ with pytest.raises(TypeError, match="must be a bool"):
1462
+
1463
+ class C(Enum):
1464
+ serialize_by_name = 5
1465
+ A = 1
1466
+
1467
+
1468
+ # ---------- filter() matches evaluated label ----------
1469
+
1470
+
1471
+ def test_filter_by_auto_label() -> None:
1472
+ class C(Enum):
1473
+ IN_PROGRESS = "ip"
1474
+ DONE = "done"
1475
+
1476
+ assert C.filter(label="In Progress") == [C.IN_PROGRESS]
1477
+
1478
+
1479
+ def test_filter_by_callable_label() -> None:
1480
+ class C(Enum):
1481
+ A = ("a", {"label": lambda: "X"})
1482
+ B = "b"
1483
+
1484
+ assert C.filter(label="X") == [C.A]
1485
+
1486
+
1487
+ def test_filter_by_explicit_label() -> None:
1488
+ class C(Enum):
1489
+ A = ("a", {"label": "Alpha", "kind": "k"})
1490
+ B = ("b", {"label": "Beta"})
1491
+
1492
+ assert C.filter(label="Alpha") == [C.A]
1493
+ assert C.filter(label="Alpha", kind="k") == [C.A]
1494
+ assert C.filter(label="Alpha", kind="other") == []
@@ -95,7 +95,7 @@ def test_label_underscore_without_label() -> None:
95
95
  class Status(Enum):
96
96
  IN_PROGRESS = "in_progress"
97
97
 
98
- assert Status.IN_PROGRESS.label == "In_Progress"
98
+ assert Status.IN_PROGRESS.label == "In Progress"
99
99
 
100
100
 
101
101
  def test_metadata_by_attribute() -> None:
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import pytest
4
+
3
5
  import enumplus
4
6
 
5
7
 
@@ -15,6 +17,10 @@ def test_all_exports() -> None:
15
17
 
16
18
 
17
19
  def test_version() -> None:
18
- from importlib.metadata import version
20
+ from importlib.metadata import PackageNotFoundError, version
19
21
 
20
- assert enumplus.__version__ == version("enumplus")
22
+ try:
23
+ expected = version("enumplus")
24
+ except PackageNotFoundError:
25
+ pytest.skip("enumplus is not installed")
26
+ assert enumplus.__version__ == expected
@@ -1,3 +0,0 @@
1
- # github: [MathiasPaulenko]
2
- # buy_me_a_coffee: mathiaspaulenko
3
- # ko_fi: mathiaspaulenko
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes