composite-enum 0.1.0__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.
- composite_enum-0.1.0/.github/workflows/ci.yml +23 -0
- composite_enum-0.1.0/.github/workflows/publish.yml +20 -0
- composite_enum-0.1.0/.gitignore +43 -0
- composite_enum-0.1.0/CHANGELOG.md +18 -0
- composite_enum-0.1.0/LICENSE +21 -0
- composite_enum-0.1.0/PKG-INFO +425 -0
- composite_enum-0.1.0/README.md +400 -0
- composite_enum-0.1.0/composite_enum/__init__.py +24 -0
- composite_enum-0.1.0/composite_enum/_meta.py +185 -0
- composite_enum-0.1.0/composite_enum/_meta.pyi +41 -0
- composite_enum-0.1.0/composite_enum/py.typed +0 -0
- composite_enum-0.1.0/pyproject.toml +45 -0
- composite_enum-0.1.0/tests/__init__.py +0 -0
- composite_enum-0.1.0/tests/test_composite.py +1064 -0
- composite_enum-0.1.0/uv.lock +156 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
15
|
+
continue-on-error: ${{ matrix.python-version == '3.15' }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7.0.1
|
|
18
|
+
- uses: actions/setup-python@v7
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
allow-prereleases: true
|
|
22
|
+
- run: pip install -e . && pip install pytest
|
|
23
|
+
- run: pytest -v
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v7.0.1
|
|
15
|
+
- uses: actions/setup-python@v7
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: pip install build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
*.egg
|
|
14
|
+
|
|
15
|
+
# Unit test / coverage reports
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.tox/
|
|
18
|
+
.nox/
|
|
19
|
+
.coverage
|
|
20
|
+
.coverage.*
|
|
21
|
+
htmlcov/
|
|
22
|
+
coverage.xml
|
|
23
|
+
|
|
24
|
+
# Environments
|
|
25
|
+
.env
|
|
26
|
+
.venv
|
|
27
|
+
venv/
|
|
28
|
+
env/
|
|
29
|
+
|
|
30
|
+
# Type checkers
|
|
31
|
+
.mypy_cache/
|
|
32
|
+
.pytype/
|
|
33
|
+
.pyre/
|
|
34
|
+
|
|
35
|
+
# Linting
|
|
36
|
+
.ruff_cache/
|
|
37
|
+
|
|
38
|
+
# PyPI configuration
|
|
39
|
+
.pypirc
|
|
40
|
+
|
|
41
|
+
# IDE
|
|
42
|
+
.idea/
|
|
43
|
+
.vscode/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `CompositeEnum` base class for composing members from other enums
|
|
13
|
+
- `CompositeEnumMeta` metaclass for use with `StrEnum`, `IntEnum`, and custom data type mixins
|
|
14
|
+
- Introspection API: `source_enum`, `to_source()`, `from_source()`, `members_from()`, `included_enums()`, `includes_enum()`
|
|
15
|
+
- Support for nested composition (composing from an already-composite enum)
|
|
16
|
+
- Data type validation (e.g., rejects `int` values into a `StrEnum` target)
|
|
17
|
+
- `Flag` / `IntFlag` rejection with clear error message
|
|
18
|
+
- PEP 561 `py.typed` marker for type checker support
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Isaac Fuenmayor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: composite-enum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build superset enums by composing members from other enums
|
|
5
|
+
Project-URL: Homepage, https://github.com/isaacfuenmayora/composite-enum
|
|
6
|
+
Project-URL: Repository, https://github.com/isaacfuenmayora/composite-enum
|
|
7
|
+
Project-URL: Issues, https://github.com/isaacfuenmayora/composite-enum/issues
|
|
8
|
+
Author-email: Isaac Fuenmayor <contact@hireisaac.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: composition,enum,metaclass
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# composite-enum
|
|
27
|
+
|
|
28
|
+
[](https://github.com/isaacfuenmayora/composite-enum/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/composite-enum/)
|
|
30
|
+
[](https://pypi.org/project/composite-enum/)
|
|
31
|
+
[](https://microsoft.github.io/pyright/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Build superset enums by composing members from other enums. Included
|
|
35
|
+
members become real first-class members of the new enum, with introspection
|
|
36
|
+
back to their origin.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from enum import Enum
|
|
40
|
+
from composite_enum import CompositeEnum
|
|
41
|
+
|
|
42
|
+
class Operator(Enum):
|
|
43
|
+
UNION = "|"
|
|
44
|
+
INTERSECT = "&"
|
|
45
|
+
DIFF = "-"
|
|
46
|
+
SYM_DIFF = "^"
|
|
47
|
+
|
|
48
|
+
class TokenType(CompositeEnum, includes=Operator):
|
|
49
|
+
IDENT = "IDENT"
|
|
50
|
+
STRING = "STRING"
|
|
51
|
+
ASSIGN = "="
|
|
52
|
+
LPAREN = "("
|
|
53
|
+
RPAREN = ")"
|
|
54
|
+
|
|
55
|
+
# TokenType has all 9 members: 4 from Operator + 5 of its own
|
|
56
|
+
list(TokenType)
|
|
57
|
+
# [UNION, INTERSECT, DIFF, SYM_DIFF, IDENT, STRING, ASSIGN, LPAREN, RPAREN]
|
|
58
|
+
|
|
59
|
+
# Included members are real members
|
|
60
|
+
TokenType.UNION # <TokenType.UNION: '|'>
|
|
61
|
+
TokenType.UNION.value # '|'
|
|
62
|
+
TokenType("|") # <TokenType.UNION: '|'>
|
|
63
|
+
TokenType["UNION"] # <TokenType.UNION: '|'>
|
|
64
|
+
|
|
65
|
+
# But they know where they came from
|
|
66
|
+
TokenType.UNION.source_enum # <enum 'Operator'>
|
|
67
|
+
TokenType.UNION.to_source() # <Operator.UNION: '|'>
|
|
68
|
+
TokenType.from_source(Operator.UNION) # <TokenType.UNION: '|'>
|
|
69
|
+
TokenType.IDENT.source_enum # None (defined directly)
|
|
70
|
+
TokenType.members_from(Operator) # frozenset({UNION, INTERSECT, DIFF, SYM_DIFF})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install composite-enum
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/isaacfuenmayora/composite-enum
|
|
83
|
+
cd composite-enum
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With uv:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv sync
|
|
90
|
+
uv run pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
With pip:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e . && pip install pytest
|
|
97
|
+
pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Why
|
|
101
|
+
|
|
102
|
+
Python's `Enum` doesn't allow subclassing an enum that already has members.
|
|
103
|
+
This is intentional ([docs](https://docs.python.org/3/howto/enum.html#restricted-enum-subclassing)),
|
|
104
|
+
but it means you can't express "TokenType is Operator plus some extra token
|
|
105
|
+
types" through inheritance. You end up duplicating the values and hoping
|
|
106
|
+
they stay in sync.
|
|
107
|
+
|
|
108
|
+
This restriction exists for good reason.
|
|
109
|
+
[`flufl.enum`](https://gitlab.com/flufl/flufl.enum), the precursor to
|
|
110
|
+
Python's stdlib `enum`, supported member inheritance natively. That
|
|
111
|
+
feature was dropped in [PEP 435](https://peps.python.org/pep-0435/) because it conflicts with members being
|
|
112
|
+
instances of their enum class. CPython core developer Alyssa Coghlan
|
|
113
|
+
[later speculated](https://python-notes.curiousefficiency.org/en/latest/python3/enum_creation.html#support-for-alternate-declaration-syntaxes)
|
|
114
|
+
that extensible enums would require aggregating members from multiple
|
|
115
|
+
independent enumerations, sketching a hypothetical syntax:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
class MoreColors(AggregateEnum, extends=Color):
|
|
119
|
+
cyan = ...
|
|
120
|
+
magenta = ...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This was never implemented in the stdlib. `composite-enum` takes a
|
|
124
|
+
similar approach using `includes` instead of `extends`.
|
|
125
|
+
|
|
126
|
+
`composite-enum` solves this with a metaclass that injects source enum
|
|
127
|
+
members into the new enum's namespace during class creation.
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
The opening example covers the basics. Here's what else you can do.
|
|
132
|
+
|
|
133
|
+
### Multiple sources
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
class Delimiter(Enum):
|
|
137
|
+
COMMA = ","
|
|
138
|
+
SEMICOLON = ";"
|
|
139
|
+
|
|
140
|
+
class TokenType(CompositeEnum, includes=(Operator, Delimiter)):
|
|
141
|
+
IDENT = "IDENT"
|
|
142
|
+
STRING = "STRING"
|
|
143
|
+
ASSIGN = "="
|
|
144
|
+
LPAREN = "("
|
|
145
|
+
RPAREN = ")"
|
|
146
|
+
|
|
147
|
+
TokenType.included_enums() # (Operator, Delimiter)
|
|
148
|
+
|
|
149
|
+
# Included members appear first, in includes order, then class body
|
|
150
|
+
list(TokenType)
|
|
151
|
+
# [UNION, INTERSECT, DIFF, SYM_DIFF, COMMA, SEMICOLON, IDENT, STRING, ASSIGN, LPAREN, RPAREN]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### With StrEnum / IntEnum
|
|
155
|
+
|
|
156
|
+
`CompositeEnum` can't be used alongside `StrEnum` or `IntEnum`
|
|
157
|
+
(Python's enum inheritance rules). Use the metaclass directly:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from enum import StrEnum # 3.11+
|
|
161
|
+
from composite_enum import CompositeEnumMeta
|
|
162
|
+
|
|
163
|
+
class TokenType(StrEnum, metaclass=CompositeEnumMeta, includes=Operator):
|
|
164
|
+
IDENT = "IDENT"
|
|
165
|
+
|
|
166
|
+
isinstance(TokenType.UNION, str) # True
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The metaclass validates that included values match the target's data
|
|
170
|
+
type. All introspection methods work the same either way.
|
|
171
|
+
|
|
172
|
+
The same metaclass approach works for any data type mixin, not just
|
|
173
|
+
`StrEnum` and `IntEnum`. Use `(float, Enum)`, `(bytes, Enum)`, or
|
|
174
|
+
any custom type:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
class Voltage(Enum):
|
|
178
|
+
LOW = 3.3
|
|
179
|
+
HIGH = 5.0
|
|
180
|
+
|
|
181
|
+
class Signal(float, Enum, metaclass=CompositeEnumMeta, includes=Voltage):
|
|
182
|
+
GROUND = 0.0
|
|
183
|
+
|
|
184
|
+
isinstance(Signal.LOW, float) # True
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> **Note:** Type checkers have two limitations with the
|
|
188
|
+
> `metaclass=CompositeEnumMeta` approach:
|
|
189
|
+
>
|
|
190
|
+
> 1. They may flag the `includes` keyword, since they don't infer class
|
|
191
|
+
> keywords from metaclass signatures. Add `# type: ignore[call-arg]`
|
|
192
|
+
> to suppress this.
|
|
193
|
+
> 2. The instance-level attributes `source_enum` and `to_source()` won't
|
|
194
|
+
> be visible to type checkers, because the `.pyi` stub declares these
|
|
195
|
+
> on `CompositeEnum`, not on arbitrary metaclass-created classes. The
|
|
196
|
+
> class-level methods (`from_source()`, `members_from()`,
|
|
197
|
+
> `included_enums()`, `includes_enum()`) work fine on both paths since
|
|
198
|
+
> they're declared on the metaclass. Subclassing `CompositeEnum` is the
|
|
199
|
+
> type-checker-friendly path: `from_source()` narrows to `Self | None`
|
|
200
|
+
> and `members_from()` to `frozenset[Self]`.
|
|
201
|
+
>
|
|
202
|
+
> Both work correctly at runtime regardless. Note that type checkers
|
|
203
|
+
> cannot resolve dynamically injected member names (e.g.
|
|
204
|
+
> `TokenType.UNION`) on either path. This is a general limitation of
|
|
205
|
+
> enum metaclasses, not specific to `composite-enum`.
|
|
206
|
+
|
|
207
|
+
### Nested composition
|
|
208
|
+
|
|
209
|
+
Composing from an already-composite enum works. `source_enum` points
|
|
210
|
+
to the immediate source, not the original:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
class Base(CompositeEnum, includes=Operator):
|
|
214
|
+
IDENT = "IDENT"
|
|
215
|
+
|
|
216
|
+
class Extended(CompositeEnum, includes=Base):
|
|
217
|
+
EXTRA = "extra"
|
|
218
|
+
|
|
219
|
+
Extended.UNION.source_enum # <enum 'Base'>, not Operator
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## API Reference
|
|
223
|
+
|
|
224
|
+
### `CompositeEnum`
|
|
225
|
+
|
|
226
|
+
Base class for composition. Extend this instead of `Enum`.
|
|
227
|
+
|
|
228
|
+
### `CompositeEnumMeta`
|
|
229
|
+
|
|
230
|
+
The metaclass powering composition. Use directly when you need
|
|
231
|
+
`StrEnum`, `IntEnum`, etc. as the base type.
|
|
232
|
+
|
|
233
|
+
#### `includes` (class keyword)
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
class TokenType(CompositeEnum, includes=Operator): # single source
|
|
237
|
+
class TokenType(CompositeEnum, includes=(Operator, Delimiter)): # multiple sources
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
A single `Enum` type or a sequence of them whose members should be included.
|
|
241
|
+
|
|
242
|
+
#### `member.source_enum`
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
TokenType.UNION.source_enum # <enum 'Operator'>
|
|
246
|
+
TokenType.IDENT.source_enum # None
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The source enum this member was included from, or `None`.
|
|
250
|
+
|
|
251
|
+
#### `member.to_source()`
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
TokenType.UNION.to_source() # Operator.UNION
|
|
255
|
+
TokenType.IDENT.to_source() # None
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Convert a composite member back to its source enum member. Returns
|
|
259
|
+
`None` for members defined directly on the composite.
|
|
260
|
+
|
|
261
|
+
#### `cls.from_source(member)`
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
TokenType.from_source(Operator.UNION) # TokenType.UNION
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Convert a source enum member to its composite equivalent. Returns
|
|
268
|
+
`None` when there's no match.
|
|
269
|
+
|
|
270
|
+
#### `cls.members_from(source)`
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
TokenType.members_from(Operator)
|
|
274
|
+
# frozenset({TokenType.UNION, TokenType.INTERSECT, ...})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Returns a `frozenset` of members that originated from `source`.
|
|
278
|
+
|
|
279
|
+
#### `cls.included_enums()` / `cls.includes_enum(source)`
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
TokenType.included_enums() # (Operator, Delimiter)
|
|
283
|
+
TokenType.includes_enum(Operator) # True
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Introspect which source enums were composed in.
|
|
287
|
+
|
|
288
|
+
## Supported Enum Types
|
|
289
|
+
|
|
290
|
+
| Base type | Python | Supported | How |
|
|
291
|
+
|---|---|---|---|
|
|
292
|
+
| `Enum` | 3.10+ | Yes | `CompositeEnum` base class |
|
|
293
|
+
| `StrEnum` | 3.11+ | Yes | `metaclass=CompositeEnumMeta` |
|
|
294
|
+
| `IntEnum` | 3.10+ | Yes | `metaclass=CompositeEnumMeta` |
|
|
295
|
+
| `str, Enum` mixin | 3.10+ | Yes | `metaclass=CompositeEnumMeta` |
|
|
296
|
+
| `int, Enum` mixin | 3.10+ | Yes | `metaclass=CompositeEnumMeta` |
|
|
297
|
+
| `Flag` | any | No | Bitwise semantics across unrelated Flags are ambiguous |
|
|
298
|
+
| `IntFlag` | any | No | Same as Flag |
|
|
299
|
+
|
|
300
|
+
### Source enum types
|
|
301
|
+
|
|
302
|
+
Source enums (the ones in `includes`) can be any `Enum`, `StrEnum`, or
|
|
303
|
+
`IntEnum`. Their values must be compatible with the target's data type:
|
|
304
|
+
|
|
305
|
+
| Target type | Accepted source values |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `Enum` (plain) | Anything |
|
|
308
|
+
| `StrEnum` / `str, Enum` | Must be `str` |
|
|
309
|
+
| `IntEnum` / `int, Enum` | Must be `int` |
|
|
310
|
+
|
|
311
|
+
## Caveats
|
|
312
|
+
|
|
313
|
+
**Implementation detail dependency.** The metaclass injects members via
|
|
314
|
+
`_EnumDict.__setitem__`, which is an implementation detail of CPython's
|
|
315
|
+
enum module. It's been stable since Python 3.6 and is unlikely to
|
|
316
|
+
change, but it's not a guaranteed public API. Tested on 3.10 through
|
|
317
|
+
3.15.
|
|
318
|
+
|
|
319
|
+
**Source members are not `in` the composite.** `Enum.__contains__`
|
|
320
|
+
uses `isinstance`, so `Operator.UNION in TokenType` is `False` even
|
|
321
|
+
though `TokenType.UNION` exists with the same value. Use
|
|
322
|
+
`TokenType.from_source(Operator.UNION)` to check membership.
|
|
323
|
+
|
|
324
|
+
**Reserved member names.** The names `source_enum`, `included_enums`,
|
|
325
|
+
`includes_enum`, `members_from`, `to_source`, and `from_source` are
|
|
326
|
+
reserved by the metaclass. Using any of them as a member name raises `TypeError` at class creation.
|
|
327
|
+
|
|
328
|
+
**Source methods don't transfer.** Only member names and values are
|
|
329
|
+
composed. Methods, properties, and custom `__init__` defined on a
|
|
330
|
+
source enum are not carried over to the composite.
|
|
331
|
+
|
|
332
|
+
**Source enum aliases are preserved.** If a source enum has aliases
|
|
333
|
+
(multiple names for the same value), they transfer as aliases in the
|
|
334
|
+
composite too:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
class Source(Enum):
|
|
338
|
+
PRIMARY = 1
|
|
339
|
+
ALIAS = 1 # alias of PRIMARY
|
|
340
|
+
|
|
341
|
+
class Target(CompositeEnum, includes=Source):
|
|
342
|
+
EXTRA = "extra"
|
|
343
|
+
|
|
344
|
+
Target.PRIMARY # <Target.PRIMARY: 1>
|
|
345
|
+
Target["ALIAS"] # <Target.PRIMARY: 1> (alias, same as source)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Value aliases across sources.** If two included sources share a value
|
|
349
|
+
(different name, same value), the second name becomes an alias of the
|
|
350
|
+
first. This is standard enum behavior, not composite-specific, but it
|
|
351
|
+
has implications for introspection:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
class A(Enum):
|
|
355
|
+
X = 1
|
|
356
|
+
|
|
357
|
+
class B(Enum):
|
|
358
|
+
Y = 1
|
|
359
|
+
|
|
360
|
+
class Combined(CompositeEnum, includes=(A, B)):
|
|
361
|
+
Z = 2
|
|
362
|
+
|
|
363
|
+
Combined.Y # <Combined.X: 1> (Y is an alias)
|
|
364
|
+
Combined.from_source(B.Y) # <Combined.X: 1>
|
|
365
|
+
Combined.from_source(B.Y).source_enum # <enum 'A'> (not B)
|
|
366
|
+
Combined.from_source(B.Y).to_source() # <A.X: 1> (not B.Y)
|
|
367
|
+
Combined.members_from(A) # frozenset({<Combined.X: 1>})
|
|
368
|
+
Combined.members_from(B) # frozenset({<Combined.X: 1>}) (same member)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Because `Y` is an alias for `X`, the canonical member's `source_enum`
|
|
372
|
+
always points to whichever source provided the canonical name (`A`),
|
|
373
|
+
regardless of which source you used in `from_source()`. Likewise,
|
|
374
|
+
`members_from()` returns the canonical member for both sources.
|
|
375
|
+
|
|
376
|
+
## How It Works
|
|
377
|
+
|
|
378
|
+
The metaclass overrides `__prepare__` and `__new__`:
|
|
379
|
+
|
|
380
|
+
1. **`__prepare__`** runs before the class body executes. It creates the
|
|
381
|
+
standard `_EnumDict` namespace, then injects each source enum's
|
|
382
|
+
members via `namespace[name] = value`. `_EnumDict.__setitem__`
|
|
383
|
+
registers these as member candidates. This means included members
|
|
384
|
+
appear first in iteration order.
|
|
385
|
+
|
|
386
|
+
2. The **class body** executes next, adding its own members. If a name
|
|
387
|
+
collides with an already-injected member, `_EnumDict` raises
|
|
388
|
+
`TypeError` immediately.
|
|
389
|
+
|
|
390
|
+
3. **`__new__`** builds the actual enum class via `super().__new__()`,
|
|
391
|
+
then attaches metadata for introspection.
|
|
392
|
+
|
|
393
|
+
The result is a normal stdlib `Enum`. Standard tools like `isinstance`,
|
|
394
|
+
`pickle`, `match/case`, and `list()` all work exactly as they would
|
|
395
|
+
with any hand-written enum. The only additions are the introspection
|
|
396
|
+
methods (`source_enum`, `to_source`, etc.).
|
|
397
|
+
|
|
398
|
+
## Alternatives
|
|
399
|
+
|
|
400
|
+
- **[flufl.enum](https://fluflenum.readthedocs.io/en/stable/using.html#extending-an-enumeration-through-subclassing)**
|
|
401
|
+
is the original Python enum package (predating the stdlib) and still
|
|
402
|
+
supports member inheritance natively. If you want true subclassing
|
|
403
|
+
where parent and child share member identity, and you don't need to
|
|
404
|
+
stay on the stdlib `enum`, `flufl.enum` is actively maintained and
|
|
405
|
+
battle-tested since 2004.
|
|
406
|
+
|
|
407
|
+
- **[aenum](https://github.com/ethanfurman/aenum)** by the stdlib `enum`
|
|
408
|
+
maintainer provides `extend_enum()` for adding members to an existing enum
|
|
409
|
+
at runtime. If you need to modify enums you don't control,
|
|
410
|
+
`aenum` is the mature, well-established choice.
|
|
411
|
+
|
|
412
|
+
- **[extendable-enum](https://pypi.org/project/extendable-enum/)** takes a decorator approach: `@inheritable_enum` makes an existing enum subclassable (so `class Derived(Base):` works directly), while `@copy_enum_members` copies members from one enum into a new, distinct class.
|
|
413
|
+
|
|
414
|
+
- **[unionenum.py](https://gist.github.com/plammens/ab1a2f236b5c6d748f193eb12eefa6dd)**
|
|
415
|
+
is a clever gist that creates union enums where members retain their
|
|
416
|
+
original type identity rather than becoming members of the new class.
|
|
417
|
+
|
|
418
|
+
`composite-enum` occupies a slightly different niche: declarative
|
|
419
|
+
composition of one or more source enums at class-definition time, with
|
|
420
|
+
source tracking and type compatibility checks. If one of the above fits
|
|
421
|
+
your use case better, use it.
|
|
422
|
+
|
|
423
|
+
## License
|
|
424
|
+
|
|
425
|
+
MIT
|