enumplus 1.2.0__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.0 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- {enumplus-1.2.0 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/config.yml +2 -2
- {enumplus-1.2.0 → enumplus-1.2.2}/.github/workflows/release.yml +0 -1
- {enumplus-1.2.0 → enumplus-1.2.2}/AGENTS.md +7 -3
- enumplus-1.2.2/CHANGELOG.md +118 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/CONTRIBUTING.md +4 -4
- {enumplus-1.2.0 → enumplus-1.2.2}/LICENSE +1 -1
- {enumplus-1.2.0 → enumplus-1.2.2}/PKG-INFO +22 -10
- {enumplus-1.2.0 → enumplus-1.2.2}/README.md +18 -6
- {enumplus-1.2.0 → enumplus-1.2.2}/SECURITY.md +1 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/SUPPORT.md +4 -4
- {enumplus-1.2.0 → enumplus-1.2.2}/enumplus/__init__.py +7 -1
- {enumplus-1.2.0 → enumplus-1.2.2}/enumplus/enum.py +62 -19
- {enumplus-1.2.0 → enumplus-1.2.2}/enumplus/serialize.py +22 -27
- {enumplus-1.2.0 → enumplus-1.2.2}/pyproject.toml +4 -4
- enumplus-1.2.2/tests/test_edge_cases.py +1494 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_enum.py +3 -3
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_new_features.py +35 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_public_api.py +9 -1
- enumplus-1.2.0/.github/FUNDING.yml +0 -3
- enumplus-1.2.0/CHANGELOG.md +0 -76
- enumplus-1.2.0/tests/test_edge_cases.py +0 -581
- {enumplus-1.2.0 → enumplus-1.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/.github/workflows/ci.yml +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/.gitignore +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/CODE_OF_CONDUCT.md +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/enumplus/py.typed +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/enumplus/pydantic.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/__init__.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_compatibility.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_metadata_unpacking.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_ordered.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_pydantic.py +0 -0
- {enumplus-1.2.0 → enumplus-1.2.2}/tests/test_serialize.py +0 -0
- {enumplus-1.2.0 → 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.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
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
|
+
|
|
36
|
+
## [1.2.1] - 2026-08-23
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `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.
|
|
41
|
+
- `__version__` is now loaded dynamically from package metadata via `importlib.metadata`, eliminating version mismatch between `__init__.py` and `pyproject.toml`.
|
|
42
|
+
- `__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.
|
|
43
|
+
- `__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.
|
|
44
|
+
- `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`.
|
|
45
|
+
- `_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.
|
|
46
|
+
- 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.
|
|
47
|
+
- Removed dead `_serialize_value` function from `serialize.py`.
|
|
48
|
+
- Updated `SECURITY.md` to include `1.2.x` in supported versions.
|
|
49
|
+
|
|
50
|
+
## [1.2.0] - 2026-08-23
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `serialize_by_name` is now correctly inherited by subclass enums instead of being silently reset to `False`.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- Improved README with table of contents, requirements section, and dynamic CI badge.
|
|
59
|
+
- Added `pydantic` optional dependency extra for Pydantic v2 integration.
|
|
60
|
+
- Added concurrency groups to CI and release workflows to cancel superseded runs.
|
|
61
|
+
- Updated issue templates with correct version examples.
|
|
62
|
+
- Updated CONTRIBUTING with cross-version testing instructions and repository name clarification.
|
|
63
|
+
|
|
64
|
+
## [1.1.0] - 2025-01-15
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- Case-insensitive lookup via `case_insensitive=True` on `from_value()` and `from_name()`.
|
|
69
|
+
- `serialize_by_name` class config for Pydantic v2 validation and serialization by member name.
|
|
70
|
+
- `to_dict()` method for serializing an enum to a nested dictionary.
|
|
71
|
+
- `get()` method for dict-style lookup with a default.
|
|
72
|
+
- `map()` method for mapping each member to a value.
|
|
73
|
+
- `keys()` method as an alias of `names()`.
|
|
74
|
+
- `get_initial()` and `get_final()` for declaration-order access.
|
|
75
|
+
- Callable labels for i18n / translatable labels.
|
|
76
|
+
- `@dataclass_transform()` on the metaclass for type-checker metadata attribute support.
|
|
77
|
+
- `py.typed` marker for PEP 561 typed-package support.
|
|
78
|
+
- Python 3.14 support and CI coverage.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- `auto()` now works correctly after metadata tuples on Python 3.11–3.14.
|
|
83
|
+
- Class config (`serialize_by_name`) no longer registers as an enum member or corrupts `auto()` state.
|
|
84
|
+
- `__eq__`, `__hash__`, `__contains__`, `from_value`, `is_valid`, and `filter` are now safe for unhashable values and non-boolean comparison results.
|
|
85
|
+
- `from_json` validates input type and JSON shape (`TypeError` for wrong type, `ValueError` for invalid JSON).
|
|
86
|
+
- `from_name` raises `TypeError` for non-string input instead of `KeyError`.
|
|
87
|
+
- `to_dict()` and `to_json()` no longer invoke non-label callable metadata values.
|
|
88
|
+
- Removed redundant import in `SerializableEncoder`.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Improved package metadata: classifiers, project URLs, and dev dependency lower bounds.
|
|
93
|
+
- Release workflow uses `${{ github.repository }}` for dynamic changelog URLs.
|
|
94
|
+
- CONTRIBUTING and SUPPORT docs updated with correct commands.
|
|
95
|
+
|
|
96
|
+
## [1.0.0] - 2024-12-01
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- `Enum` base class as a drop-in replacement for `enum.Enum`.
|
|
101
|
+
- `OrderedEnum` with declaration-order comparison operators.
|
|
102
|
+
- Per-member labels with automatic `name.title()` fallback.
|
|
103
|
+
- Per-member metadata via `(value, dict)` tuple syntax.
|
|
104
|
+
- Metadata attribute access via `__getattr__`.
|
|
105
|
+
- `choices()`, `from_value()`, `from_name()`, `is_valid()`, `validate()`.
|
|
106
|
+
- `values()`, `names()`, `labels()`.
|
|
107
|
+
- `filter()` by metadata key-value pairs.
|
|
108
|
+
- `to_json()` / `from_json()` serialization.
|
|
109
|
+
- `SerializableEncoder` for `json.dumps`.
|
|
110
|
+
- Pydantic v2 integration via `__get_pydantic_core_schema__`.
|
|
111
|
+
- Full test suite, CI, and release workflows.
|
|
112
|
+
|
|
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
|
|
|
@@ -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__"]
|
|
@@ -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().
|
|
@@ -58,20 +64,21 @@ class _EnumPlusDict(EnumDict):
|
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
def _safe_equal(a: Any, b: Any) -> bool:
|
|
61
|
-
"""Return ``a == b`` as a bool, swallowing
|
|
67
|
+
"""Return ``a == b`` as a bool, swallowing comparison errors.
|
|
62
68
|
|
|
63
69
|
Handles values whose ``__eq__`` may raise (e.g. NumPy arrays with shape
|
|
64
|
-
mismatches
|
|
70
|
+
mismatches, custom objects with side-effecting ``__eq__``) or return
|
|
71
|
+
non-boolean objects (e.g. arrays, ``NotImplemented``).
|
|
65
72
|
"""
|
|
66
73
|
try:
|
|
67
74
|
result = a == b
|
|
68
|
-
except
|
|
75
|
+
except Exception:
|
|
69
76
|
return False
|
|
70
77
|
if result is NotImplemented:
|
|
71
78
|
return False
|
|
72
79
|
try:
|
|
73
80
|
return bool(result)
|
|
74
|
-
except
|
|
81
|
+
except Exception:
|
|
75
82
|
return False
|
|
76
83
|
|
|
77
84
|
|
|
@@ -147,8 +154,8 @@ class EnumMeta(enum.EnumMeta):
|
|
|
147
154
|
member._metadata_ = metadata
|
|
148
155
|
|
|
149
156
|
label = metadata.get("label")
|
|
150
|
-
if
|
|
151
|
-
member._label_ = member.name.title()
|
|
157
|
+
if label is None:
|
|
158
|
+
member._label_ = member.name.replace("_", " ").title()
|
|
152
159
|
else:
|
|
153
160
|
member._label_ = label
|
|
154
161
|
|
|
@@ -160,6 +167,13 @@ class EnumMeta(enum.EnumMeta):
|
|
|
160
167
|
if isinstance(item, cls):
|
|
161
168
|
return True
|
|
162
169
|
member: Any
|
|
170
|
+
if isinstance(item, enum.Enum):
|
|
171
|
+
# Foreign enum member: only match if a member's value IS this
|
|
172
|
+
# exact enum member (identity), not just equal by value.
|
|
173
|
+
for member in cls:
|
|
174
|
+
if member.value is item:
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
163
177
|
for member in cls:
|
|
164
178
|
if _safe_equal(member.value, item):
|
|
165
179
|
return True
|
|
@@ -185,7 +199,7 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
185
199
|
- Pydantic v2 integration
|
|
186
200
|
"""
|
|
187
201
|
|
|
188
|
-
_label_:
|
|
202
|
+
_label_: Any
|
|
189
203
|
_metadata_: dict[str, Any]
|
|
190
204
|
_index_: int
|
|
191
205
|
|
|
@@ -194,7 +208,7 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
194
208
|
"""Human-readable label for this member.
|
|
195
209
|
|
|
196
210
|
If the label was set to a callable, it is evaluated on every access.
|
|
197
|
-
Falls back to ``name.title()`` when no label is set.
|
|
211
|
+
Falls back to ``name.replace("_", " ").title()`` when no label is set.
|
|
198
212
|
"""
|
|
199
213
|
label = self._label_
|
|
200
214
|
if callable(label):
|
|
@@ -272,6 +286,18 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
272
286
|
ValueError: If no match is found and no default is provided.
|
|
273
287
|
"""
|
|
274
288
|
member: Any
|
|
289
|
+
if isinstance(value, cls):
|
|
290
|
+
return value
|
|
291
|
+
if isinstance(value, enum.Enum):
|
|
292
|
+
# Foreign enum member: only match if a member's value IS this
|
|
293
|
+
# exact enum member (identity), not just equal by value.
|
|
294
|
+
for member in cls:
|
|
295
|
+
if member.value is value:
|
|
296
|
+
return member
|
|
297
|
+
if default is not _SENTINEL:
|
|
298
|
+
return default
|
|
299
|
+
raise ValueError(f"{value!r} is not a valid {cls.__name__} value")
|
|
300
|
+
|
|
275
301
|
if case_insensitive and isinstance(value, str):
|
|
276
302
|
lowered = value.lower()
|
|
277
303
|
for member in cls:
|
|
@@ -336,6 +362,15 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
336
362
|
@classmethod
|
|
337
363
|
def is_valid(cls, value: Any) -> bool:
|
|
338
364
|
"""Return ``True`` if ``value`` is a valid member or member value."""
|
|
365
|
+
if isinstance(value, cls):
|
|
366
|
+
return True
|
|
367
|
+
if isinstance(value, enum.Enum):
|
|
368
|
+
# Foreign enum member: only match if a member's value IS this
|
|
369
|
+
# exact enum member (identity), not just equal by value.
|
|
370
|
+
for member in cls:
|
|
371
|
+
if member.value is value:
|
|
372
|
+
return True
|
|
373
|
+
return False
|
|
339
374
|
for member in cls:
|
|
340
375
|
if member is value or _safe_equal(member.value, value):
|
|
341
376
|
return True
|
|
@@ -424,13 +459,17 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
424
459
|
"""Serialize the enum to a nested dict with ``value``, ``label``, ``metadata``."""
|
|
425
460
|
result: dict[str, dict[str, Any]] = {}
|
|
426
461
|
for member in cls:
|
|
462
|
+
label = member.label
|
|
463
|
+
metadata: dict[str, Any] = {}
|
|
464
|
+
for k, v in member._metadata_.items():
|
|
465
|
+
if k == "label" and callable(v):
|
|
466
|
+
metadata[k] = label
|
|
467
|
+
else:
|
|
468
|
+
metadata[k] = v
|
|
427
469
|
result[member.name] = {
|
|
428
470
|
"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
|
-
},
|
|
471
|
+
"label": label,
|
|
472
|
+
"metadata": metadata,
|
|
434
473
|
}
|
|
435
474
|
return result
|
|
436
475
|
|
|
@@ -438,14 +477,18 @@ class Enum(enum.Enum, metaclass=EnumMeta):
|
|
|
438
477
|
def filter(cls, **kwargs: Any) -> list[Self]:
|
|
439
478
|
"""Filter members by metadata key-value pairs (AND logic).
|
|
440
479
|
|
|
441
|
-
|
|
480
|
+
``label`` is matched against the member's evaluated label, so callable
|
|
481
|
+
labels work too. With no kwargs, returns all members.
|
|
442
482
|
"""
|
|
443
483
|
if not kwargs:
|
|
444
484
|
return list(cls)
|
|
445
485
|
result: list[Self] = []
|
|
446
486
|
for member in cls:
|
|
447
487
|
if all(
|
|
448
|
-
|
|
488
|
+
_safe_equal(member.label, value)
|
|
489
|
+
if key == "label"
|
|
490
|
+
else key in member._metadata_
|
|
491
|
+
and _safe_equal(member._metadata_[key], value)
|
|
449
492
|
for key, value in kwargs.items()
|
|
450
493
|
):
|
|
451
494
|
result.append(member)
|
|
@@ -486,22 +529,22 @@ class OrderedEnum(Enum):
|
|
|
486
529
|
Also works with ``sorted()``, ``min()``, and ``max()``.
|
|
487
530
|
"""
|
|
488
531
|
|
|
489
|
-
def __lt__(self, other: object) -> bool:
|
|
532
|
+
def __lt__(self, other: object) -> bool | NotImplementedType:
|
|
490
533
|
if not isinstance(other, type(self)):
|
|
491
534
|
return NotImplemented
|
|
492
535
|
return self._index_ < other._index_
|
|
493
536
|
|
|
494
|
-
def __le__(self, other: object) -> bool:
|
|
537
|
+
def __le__(self, other: object) -> bool | NotImplementedType:
|
|
495
538
|
if not isinstance(other, type(self)):
|
|
496
539
|
return NotImplemented
|
|
497
540
|
return self._index_ <= other._index_
|
|
498
541
|
|
|
499
|
-
def __gt__(self, other: object) -> bool:
|
|
542
|
+
def __gt__(self, other: object) -> bool | NotImplementedType:
|
|
500
543
|
if not isinstance(other, type(self)):
|
|
501
544
|
return NotImplemented
|
|
502
545
|
return self._index_ > other._index_
|
|
503
546
|
|
|
504
|
-
def __ge__(self, other: object) -> bool:
|
|
547
|
+
def __ge__(self, other: object) -> bool | NotImplementedType:
|
|
505
548
|
if not isinstance(other, type(self)):
|
|
506
549
|
return NotImplemented
|
|
507
550
|
return self._index_ >= other._index_
|