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.
- {enumplus-1.2.1 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- {enumplus-1.2.1 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/config.yml +2 -2
- {enumplus-1.2.1 → enumplus-1.2.2}/.github/workflows/release.yml +0 -1
- {enumplus-1.2.1 → enumplus-1.2.2}/AGENTS.md +7 -3
- {enumplus-1.2.1 → enumplus-1.2.2}/CHANGELOG.md +32 -5
- {enumplus-1.2.1 → enumplus-1.2.2}/CONTRIBUTING.md +4 -4
- {enumplus-1.2.1 → enumplus-1.2.2}/LICENSE +1 -1
- {enumplus-1.2.1 → enumplus-1.2.2}/PKG-INFO +22 -10
- {enumplus-1.2.1 → enumplus-1.2.2}/README.md +18 -6
- {enumplus-1.2.1 → enumplus-1.2.2}/SUPPORT.md +4 -4
- {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/enum.py +24 -10
- {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/serialize.py +5 -2
- {enumplus-1.2.1 → enumplus-1.2.2}/pyproject.toml +4 -4
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_edge_cases.py +132 -1
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_enum.py +1 -1
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_public_api.py +8 -2
- enumplus-1.2.1/.github/FUNDING.yml +0 -3
- {enumplus-1.2.1 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/.github/workflows/ci.yml +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/.gitignore +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/CODE_OF_CONDUCT.md +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/SECURITY.md +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/__init__.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/py.typed +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/enumplus/pydantic.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/__init__.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_compatibility.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_metadata_unpacking.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_new_features.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_ordered.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_pydantic.py +0 -0
- {enumplus-1.2.1 → enumplus-1.2.2}/tests/test_serialize.py +0 -0
- {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
|
|
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/
|
|
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/
|
|
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
|
|
71
|
-
project
|
|
72
|
-
|
|
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/
|
|
88
|
-
[1.2.
|
|
89
|
-
[1.2.
|
|
90
|
-
[1.
|
|
91
|
-
[1.
|
|
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>/
|
|
11
|
-
cd
|
|
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
|
|
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/
|
|
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,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: enumplus
|
|
3
|
-
Version: 1.2.
|
|
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/
|
|
7
|
-
Project-URL: Issues, https://github.com/MathiasPaulenko/
|
|
8
|
-
Project-URL: Changelog, https://github.com/MathiasPaulenko/
|
|
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
|

|
|
37
37
|

|
|
38
38
|

|
|
39
|
-

|
|
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"
|
|
133
|
-
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
|

|
|
4
4
|

|
|
5
5
|

|
|
6
|
-

|
|
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"
|
|
100
|
-
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/
|
|
7
|
-
- **Discussions**: Use [GitHub Discussions](https://github.com/MathiasPaulenko/
|
|
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/
|
|
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/
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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/
|
|
31
|
-
Issues = "https://github.com/MathiasPaulenko/
|
|
32
|
-
Changelog = "https://github.com/MathiasPaulenko/
|
|
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,
|
|
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 == "
|
|
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
|
-
|
|
22
|
+
try:
|
|
23
|
+
expected = version("enumplus")
|
|
24
|
+
except PackageNotFoundError:
|
|
25
|
+
pytest.skip("enumplus is not installed")
|
|
26
|
+
assert enumplus.__version__ == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|