composite-enum 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,24 @@
1
+ """Compose enum types by including members from other enums.
2
+
3
+ Usage::
4
+
5
+ from composite_enum import CompositeEnum
6
+
7
+ class Operator(Enum):
8
+ UNION = "|"
9
+ INTERSECT = "&"
10
+
11
+ class TokenType(CompositeEnum, includes=Operator):
12
+ IDENT = "IDENT"
13
+ ASSIGN = "="
14
+
15
+ assert TokenType.UNION.value == "|"
16
+ assert TokenType.UNION.source_enum is Operator
17
+ """
18
+
19
+ from importlib.metadata import version
20
+
21
+ from composite_enum._meta import CompositeEnum, CompositeEnumMeta
22
+
23
+ __all__ = ["CompositeEnum", "CompositeEnumMeta"]
24
+ __version__ = version("composite-enum")
@@ -0,0 +1,185 @@
1
+ """Metaclass and base class for composing enum types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from enum import Enum, EnumMeta, Flag
7
+ from functools import cache
8
+ from types import MappingProxyType
9
+ from typing import Any
10
+
11
+
12
+ def _get_data_type(bases: tuple[type, ...]) -> type | None:
13
+ """Return the mixin data type (str, int, …) from enum bases, or None."""
14
+ for base in bases:
15
+ for cls in base.__mro__:
16
+ if cls is object or isinstance(cls, EnumMeta):
17
+ continue
18
+ return cls
19
+ return None
20
+
21
+
22
+ def _check_flag(source: type[Enum]) -> None:
23
+ if issubclass(source, Flag):
24
+ raise TypeError(
25
+ f"{source.__name__} is a Flag enum. Flag composition is not "
26
+ f"supported because bitwise semantics across unrelated Flag "
27
+ f"enums are ambiguous. Use plain Enum, StrEnum, or IntEnum."
28
+ )
29
+
30
+
31
+ def _check_data_type(
32
+ source: type[Enum],
33
+ member: Enum,
34
+ expected: type | None,
35
+ target_name: str,
36
+ ) -> None:
37
+ if expected is not None and not isinstance(member.value, expected):
38
+ raise TypeError(
39
+ f"{source.__name__}.{member.name} has value {member.value!r} "
40
+ f"({type(member.value).__name__}), but {target_name} requires "
41
+ f"{expected.__name__} values"
42
+ )
43
+
44
+
45
+ def _check_duplicate_sources(includes: tuple[type[Enum], ...]) -> None:
46
+ seen: set[type[Enum]] = set()
47
+ for source in includes:
48
+ if source in seen:
49
+ raise TypeError(f"duplicate source enum in includes: {source.__name__}")
50
+ seen.add(source)
51
+
52
+
53
+ def _normalize_includes(
54
+ includes: type[Enum] | Sequence[type[Enum]],
55
+ ) -> tuple[type[Enum], ...]:
56
+ """Wrap a bare enum class or sequence into a tuple, rejecting strings."""
57
+ if isinstance(includes, Sequence) and not isinstance(includes, str):
58
+ return tuple(includes)
59
+ return (includes,) # type: ignore[return-value]
60
+
61
+
62
+ _RESERVED_NAMES = frozenset(
63
+ {
64
+ "source_enum",
65
+ "included_enums",
66
+ "includes_enum",
67
+ "members_from",
68
+ "to_source",
69
+ "from_source",
70
+ }
71
+ )
72
+
73
+
74
+ def _get_source_enum(self: Enum) -> type[Enum] | None:
75
+ """The source enum this member was included from, or None."""
76
+ return self.__class__._composite_source_map_.get(self.name) # type: ignore[attr-defined]
77
+
78
+
79
+ def _to_source(self: Enum) -> Enum | None:
80
+ """Convert this composite member back to its source enum member, or None."""
81
+ source_map = self.__class__._composite_source_map_ # type: ignore[attr-defined]
82
+ source: type[Enum] | None = source_map.get(self.name)
83
+ if source is None:
84
+ return None
85
+ return source[self.name]
86
+
87
+
88
+ class CompositeEnumMeta(EnumMeta):
89
+ """Metaclass that composes members from other enums into a new one."""
90
+
91
+ _composite_source_map_: Mapping[str, type[Enum]]
92
+ _composite_includes_: tuple[type[Enum], ...]
93
+
94
+ @classmethod
95
+ def __prepare__(
96
+ mcls,
97
+ name: str,
98
+ bases: tuple[type, ...],
99
+ includes: type[Enum] | Sequence[type[Enum]] = (),
100
+ **kwds: Any,
101
+ ):
102
+ namespace = super().__prepare__(name, bases, **kwds)
103
+ includes = _normalize_includes(includes)
104
+ _check_duplicate_sources(includes)
105
+
106
+ data_type = _get_data_type(bases)
107
+ seen: dict[str, type[Enum]] = {}
108
+
109
+ for source in includes:
110
+ if not isinstance(source, EnumMeta):
111
+ raise TypeError(
112
+ f"includes expects Enum types, got "
113
+ f"{type(source).__name__}: {source!r}"
114
+ )
115
+ _check_flag(source)
116
+
117
+ for member_name, member in source.__members__.items():
118
+ if member_name in seen:
119
+ raise ValueError(
120
+ f"Name '{member_name}' exists in both "
121
+ f"{seen[member_name].__name__} and {source.__name__}"
122
+ )
123
+ _check_data_type(source, member, data_type, name)
124
+
125
+ seen[member_name] = source
126
+ # _EnumDict.__setitem__ registers this as an enum member candidate.
127
+ namespace[member_name] = member.value
128
+
129
+ return namespace
130
+
131
+ def __new__(
132
+ mcls,
133
+ name: str,
134
+ bases: tuple[type, ...],
135
+ namespace: dict[str, Any],
136
+ includes: type[Enum] | Sequence[type[Enum]] = (),
137
+ **kwds: Any,
138
+ ):
139
+ cls = super().__new__(mcls, name, bases, namespace, **kwds) # type: ignore[arg-type]
140
+
141
+ for member_name in cls.__members__:
142
+ if member_name in _RESERVED_NAMES:
143
+ raise TypeError(
144
+ f"'{member_name}' is reserved by CompositeEnumMeta "
145
+ f"and cannot be used as a member name"
146
+ )
147
+
148
+ includes = _normalize_includes(includes)
149
+
150
+ source_map: dict[str, type[Enum]] = {}
151
+ for source in includes:
152
+ for member_name in source.__members__:
153
+ source_map[member_name] = source
154
+
155
+ cls._composite_source_map_ = MappingProxyType(source_map)
156
+ cls._composite_includes_ = includes
157
+ cls.source_enum = property(_get_source_enum) # type: ignore[attr-defined]
158
+ cls.to_source = _to_source # type: ignore[attr-defined]
159
+ return cls
160
+
161
+ def included_enums(cls) -> tuple[type[Enum], ...]:
162
+ """Return the source enums this composite was built from."""
163
+ return getattr(cls, "_composite_includes_", ())
164
+
165
+ def includes_enum(cls, source: type[Enum]) -> bool:
166
+ """Check if this composite includes members from *source*."""
167
+ return source in cls.included_enums()
168
+
169
+ @cache
170
+ def members_from(cls, source: type[Enum]) -> frozenset[Enum]:
171
+ """Return the subset of members that originated from *source*."""
172
+ source_map = getattr(cls, "_composite_source_map_", {})
173
+ return frozenset(cls[name] for name, src in source_map.items() if src is source) # type: ignore[arg-type]
174
+
175
+ def from_source(cls, member: Enum) -> Enum | None:
176
+ """Convert a source enum member to its composite equivalent, or None."""
177
+ source_map = getattr(cls, "_composite_source_map_", {})
178
+ source = source_map.get(member.name)
179
+ if source is None or source is not type(member):
180
+ return None
181
+ return cls[member.name] # type: ignore[return-value]
182
+
183
+
184
+ class CompositeEnum(Enum, metaclass=CompositeEnumMeta):
185
+ """Enum base class with composition support."""
@@ -0,0 +1,41 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from enum import Enum, EnumMeta
3
+ from typing import Any
4
+
5
+ from typing_extensions import Self
6
+
7
+ class CompositeEnumMeta(EnumMeta):
8
+ _composite_source_map_: Mapping[str, type[Enum]]
9
+ _composite_includes_: tuple[type[Enum], ...]
10
+
11
+ def __new__(
12
+ mcls,
13
+ name: str,
14
+ bases: tuple[type, ...],
15
+ namespace: dict[str, Any],
16
+ includes: type[Enum] | Sequence[type[Enum]] = (),
17
+ **kwds: Any,
18
+ ) -> CompositeEnumMeta: ...
19
+ def included_enums(cls) -> tuple[type[Enum], ...]: ...
20
+ def includes_enum(cls, source: type[Enum]) -> bool: ...
21
+ def members_from(cls, source: type[Enum]) -> frozenset[Enum]: ...
22
+ def from_source(cls, member: Enum) -> Enum | None: ...
23
+
24
+ class CompositeEnum(Enum, metaclass=CompositeEnumMeta):
25
+ def __init_subclass__(
26
+ cls,
27
+ *,
28
+ includes: type[Enum] | Sequence[type[Enum]] = (),
29
+ **kwargs: Any,
30
+ ) -> None: ...
31
+ @property
32
+ def source_enum(self) -> type[Enum] | None: ...
33
+ def to_source(self) -> Enum | None: ...
34
+ @classmethod
35
+ def from_source(cls, member: Enum) -> Self | None: ... # type: ignore[override]
36
+ @classmethod
37
+ def members_from(cls, source: type[Enum]) -> frozenset[Self]: ... # type: ignore[override]
38
+ @classmethod
39
+ def included_enums(cls) -> tuple[type[Enum], ...]: ...
40
+ @classmethod
41
+ def includes_enum(cls, source: type[Enum]) -> bool: ...
File without changes
@@ -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
+ [![CI](https://github.com/isaacfuenmayora/composite-enum/actions/workflows/ci.yml/badge.svg)](https://github.com/isaacfuenmayora/composite-enum/actions/workflows/ci.yml)
29
+ [![PyPI](https://img.shields.io/pypi/v/composite-enum)](https://pypi.org/project/composite-enum/)
30
+ [![Python](https://img.shields.io/pypi/pyversions/composite-enum)](https://pypi.org/project/composite-enum/)
31
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
@@ -0,0 +1,8 @@
1
+ composite_enum/__init__.py,sha256=60FxfZ_SSyBjRVOFziEPHF_s8-fS-zCzVRpa4iIkhsI,581
2
+ composite_enum/_meta.py,sha256=pipggRW6ld34CDmHQXX4RQTAUHxWsa8VecX1OFYpJj0,6478
3
+ composite_enum/_meta.pyi,sha256=Mh8X53Ig_7v_Z8QiUPot7s75EqG-TvE5Pnz_tO0Bjpo,1439
4
+ composite_enum/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ composite_enum-0.1.0.dist-info/METADATA,sha256=4d7H7hzpSSiqXWhIfs5DDwPcvD2ee17E5Ng8qme3JQ8,14662
6
+ composite_enum-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
7
+ composite_enum-0.1.0.dist-info/licenses/LICENSE,sha256=PeXp253EG6h9NhmAnB6YJhPna90wJRJN5dA7jE0gMNA,1072
8
+ composite_enum-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.