literalenum 0.1.1__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.
- literalenum/__init__.py +21 -0
- literalenum/compatibility_extensions/__init__.py +15 -0
- literalenum/compatibility_extensions/annotated.py +6 -0
- literalenum/compatibility_extensions/bare_class.py +2 -0
- literalenum/compatibility_extensions/base_model.py +23 -0
- literalenum/compatibility_extensions/click_choice.py +3 -0
- literalenum/compatibility_extensions/django_choices.py +2 -0
- literalenum/compatibility_extensions/enum.py +7 -0
- literalenum/compatibility_extensions/graphene_enum.py +18 -0
- literalenum/compatibility_extensions/int_enum.py +9 -0
- literalenum/compatibility_extensions/json_schema.py +145 -0
- literalenum/compatibility_extensions/literal.py +9 -0
- literalenum/compatibility_extensions/random_choice.py +3 -0
- literalenum/compatibility_extensions/regex.py +10 -0
- literalenum/compatibility_extensions/sqlalchemy_enum.py +6 -0
- literalenum/compatibility_extensions/str_enum.py +9 -0
- literalenum/compatibility_extensions/strawberry_enum.py +18 -0
- literalenum/literal_enum.py +137 -0
- literalenum/mypy_plugin.py +333 -0
- literalenum/py.typed +0 -0
- literalenum/stubgen.py +438 -0
- literalenum-0.1.1.dist-info/METADATA +108 -0
- literalenum-0.1.1.dist-info/RECORD +27 -0
- literalenum-0.1.1.dist-info/WHEEL +4 -0
- literalenum-0.1.1.dist-info/entry_points.txt +2 -0
- literalenum-0.1.1.dist-info/licenses/LICENSE +24 -0
- typing_literalenum.py +670 -0
typing_literalenum.py
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LiteralEnum — a runtime namespace for literal values with static exhaustiveness.
|
|
3
|
+
|
|
4
|
+
LiteralEnum bridges the gap between ``typing.Literal`` and ``enum.Enum`` /
|
|
5
|
+
``enum.StrEnum``. It defines a finite, named set of literal values that:
|
|
6
|
+
|
|
7
|
+
* are plain runtime scalars (``str``, ``int``, ``bytes``, ``bool``, ``None``),
|
|
8
|
+
* provide a runtime namespace, iteration, and validation, and
|
|
9
|
+
* can be treated by type checkers as an exhaustive ``Literal[...]`` union.
|
|
10
|
+
|
|
11
|
+
Minimal example::
|
|
12
|
+
|
|
13
|
+
class HttpMethod(LiteralEnum):
|
|
14
|
+
GET = "GET"
|
|
15
|
+
POST = "POST"
|
|
16
|
+
DELETE = "DELETE"
|
|
17
|
+
|
|
18
|
+
HttpMethod.GET # "GET" (plain str at runtime)
|
|
19
|
+
list(HttpMethod) # ["GET", "POST", "DELETE"]
|
|
20
|
+
"GET" in HttpMethod # True
|
|
21
|
+
HttpMethod.validate(x) # returns x if valid, raises ValueError otherwise
|
|
22
|
+
|
|
23
|
+
Duplicate values are permitted; the first declared name is canonical.
|
|
24
|
+
Subsequent names for the same value are aliases. Use ``names(value)``
|
|
25
|
+
and ``canonical_name(value)`` to introspect.
|
|
26
|
+
|
|
27
|
+
See the companion PEP draft for the full motivation and typing semantics.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import Any, Iterator, Mapping, TypeVar, NoReturn, Never, TypeGuard
|
|
33
|
+
from types import MappingProxyType
|
|
34
|
+
import inspect
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Allowed literal types — mirrors the set accepted by ``typing.Literal``
|
|
38
|
+
# (PEP 586). Enum members are technically allowed by Literal but are
|
|
39
|
+
# excluded here because LiteralEnum is an *alternative* to Enum.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
_LITERAL_TYPES: tuple[type, ...] = (str, int, bytes, bool, type(None))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Internal helpers
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _is_literal_type(value: object) -> bool:
|
|
49
|
+
"""Return ``True`` if *value* is a type supported by ``typing.Literal``.
|
|
50
|
+
|
|
51
|
+
Supported types: ``str``, ``int``, ``bytes``, ``bool``, and ``None``.
|
|
52
|
+
"""
|
|
53
|
+
return isinstance(value, _LITERAL_TYPES)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _strict_key(value: object) -> tuple[type, object]:
|
|
57
|
+
"""Return a hashable ``(type, value)`` pair for identity-safe comparison.
|
|
58
|
+
|
|
59
|
+
Python considers ``True == 1`` and ``False == 0``. Using the type as part
|
|
60
|
+
of the key prevents a ``bool`` member from colliding with an ``int``
|
|
61
|
+
member (or vice versa) inside the value set.
|
|
62
|
+
|
|
63
|
+
Example::
|
|
64
|
+
|
|
65
|
+
_strict_key(True) # (bool, True)
|
|
66
|
+
_strict_key(1) # (int, 1)
|
|
67
|
+
# These are distinct despite True == 1.
|
|
68
|
+
"""
|
|
69
|
+
return type(value), value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_descriptor(obj: object) -> bool:
|
|
73
|
+
"""Return ``True`` if *obj* looks like a descriptor or function.
|
|
74
|
+
|
|
75
|
+
Enum-style semantics: functions, method descriptors, ``property``,
|
|
76
|
+
``classmethod``, ``staticmethod``, and anything with ``__get__`` are
|
|
77
|
+
treated as class infrastructure rather than member values.
|
|
78
|
+
"""
|
|
79
|
+
return (
|
|
80
|
+
inspect.isfunction(obj)
|
|
81
|
+
or inspect.ismethoddescriptor(obj)
|
|
82
|
+
or hasattr(obj, "__get__")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_ignore(ns: Mapping[str, Any]) -> set[str]:
|
|
87
|
+
"""Parse an optional ``_ignore_`` directive from the class namespace.
|
|
88
|
+
|
|
89
|
+
Follows the same convention as ``enum.Enum._ignore_``:
|
|
90
|
+
|
|
91
|
+
* A whitespace- or comma-separated string: ``"a b c"`` or ``"a, b, c"``
|
|
92
|
+
* A list, tuple, set, or frozenset of name strings.
|
|
93
|
+
* ``None`` (treated as empty).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A set of attribute names to skip when collecting members.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
TypeError: If ``_ignore_`` is present but not a recognized format.
|
|
100
|
+
"""
|
|
101
|
+
ignore = ns.get("_ignore_", ())
|
|
102
|
+
if ignore is None:
|
|
103
|
+
return set()
|
|
104
|
+
if isinstance(ignore, str):
|
|
105
|
+
return {name for name in ignore.replace(",", " ").split() if name}
|
|
106
|
+
if isinstance(ignore, (list, tuple, set, frozenset)):
|
|
107
|
+
return {str(x) for x in ignore}
|
|
108
|
+
raise TypeError("_ignore_ must be a str or a sequence of names")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Public helpers
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
LE = TypeVar("LE", bound="LiteralEnum")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_member(literalenum: type[LE], x: object) -> TypeGuard[LE]:
|
|
119
|
+
"""Check whether *x* is a valid member value of *literalenum*.
|
|
120
|
+
|
|
121
|
+
Acts as a ``TypeGuard`` so that, after a successful check, a type
|
|
122
|
+
checker can narrow ``x`` to the ``LiteralEnum`` type::
|
|
123
|
+
|
|
124
|
+
if is_member(HttpMethod, value):
|
|
125
|
+
reveal_type(value) # HttpMethod (i.e. Literal["GET", "POST", ...])
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
literalenum: The ``LiteralEnum`` subclass to check against.
|
|
129
|
+
x: The value to test.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
``True`` if *x* is one of the literal values defined in *literalenum*.
|
|
133
|
+
"""
|
|
134
|
+
return x in literalenum
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_is_member(literalenum: type[LE], x: object) -> LE:
|
|
138
|
+
"""Validate that *x* is a member of *literalenum*, or raise ``ValueError``.
|
|
139
|
+
|
|
140
|
+
Unlike :func:`is_member`, this function raises on failure rather than
|
|
141
|
+
returning ``False``, making it suitable for input validation::
|
|
142
|
+
|
|
143
|
+
method = validate_is_member(HttpMethod, user_input)
|
|
144
|
+
# method is now narrowed to HttpMethod
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
literalenum: The ``LiteralEnum`` subclass to validate against.
|
|
148
|
+
x: The value to validate.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
*x* unchanged (the runtime value is already a plain literal).
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If *x* is not a valid member of *literalenum*.
|
|
155
|
+
"""
|
|
156
|
+
if is_member(literalenum, x):
|
|
157
|
+
return x # type: ignore[return-value]
|
|
158
|
+
raise ValueError(f"{x!r} is not a valid {literalenum.__name__}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Metaclass
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
class LiteralEnumMeta(type):
|
|
166
|
+
"""Metaclass that powers ``LiteralEnum``.
|
|
167
|
+
|
|
168
|
+
Responsibilities:
|
|
169
|
+
|
|
170
|
+
1. **Member collection** — during ``__new__``, scans the class namespace
|
|
171
|
+
for public, non-descriptor attributes whose values are literal types.
|
|
172
|
+
2. **Inheritance control** — subclassing a populated ``LiteralEnum``
|
|
173
|
+
requires ``extend=True`` to prevent accidental widening of the value
|
|
174
|
+
set.
|
|
175
|
+
3. **Runtime container protocol** — makes the *class itself* iterable and
|
|
176
|
+
supportive of ``in`` / ``len`` / ``[]`` so that ``"GET" in HttpMethod``
|
|
177
|
+
and ``for m in HttpMethod`` work directly on the class.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
# ---- Internal attributes set on every LiteralEnum subclass ----
|
|
181
|
+
#
|
|
182
|
+
# _members_: dict[str, Any] — name -> value mapping (all names, including aliases)
|
|
183
|
+
# _ordered_values_: tuple[Any, ...] — unique values in first-seen order
|
|
184
|
+
# _value_keys_: frozenset[tuple[type, object]] — strict-key set for O(1) ``in``
|
|
185
|
+
# _value_names_: dict[tuple[type, object], tuple[str, ...]]
|
|
186
|
+
# — strict-key -> declared names (first = canonical)
|
|
187
|
+
# _allow_aliases_: bool — whether duplicate values are permitted
|
|
188
|
+
# _call_to_validate_: bool — whether __call__ validates instead of raising
|
|
189
|
+
# __members__: MappingProxyType[str, Any] — public read-only view (all names, including aliases)
|
|
190
|
+
|
|
191
|
+
def __new__(
|
|
192
|
+
mcls,
|
|
193
|
+
name: str,
|
|
194
|
+
bases: tuple[type, ...],
|
|
195
|
+
ns: dict[str, Any],
|
|
196
|
+
*,
|
|
197
|
+
extend: bool = False,
|
|
198
|
+
allow_aliases: bool | None = None,
|
|
199
|
+
call_to_validate: bool | None = None,
|
|
200
|
+
**kwds: Any,
|
|
201
|
+
) -> LiteralEnumMeta:
|
|
202
|
+
"""Create a new LiteralEnum class, collecting its members.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
name: The class name.
|
|
206
|
+
bases: Base classes.
|
|
207
|
+
ns: The class body namespace.
|
|
208
|
+
extend: If ``True``, allow subclassing a populated LiteralEnum
|
|
209
|
+
and inherit its members. Defaults to ``False``.
|
|
210
|
+
allow_aliases: If ``False``, raise ``TypeError`` when two names
|
|
211
|
+
map to the same value. ``None`` (the default) inherits the
|
|
212
|
+
parent's setting, or ``True`` at the root.
|
|
213
|
+
call_to_validate: If ``True``, calling the class (e.g.
|
|
214
|
+
``HttpMethod("GET")``) validates and returns the value
|
|
215
|
+
instead of raising ``TypeError``. ``None`` (the default)
|
|
216
|
+
inherits the parent's setting, or ``False`` at the root.
|
|
217
|
+
**kwds: Reserved for future keyword arguments.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The newly created class.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
TypeError: On multiple LiteralEnum bases, non-literal member
|
|
224
|
+
values, name conflicts during extension, duplicate values
|
|
225
|
+
when ``allow_aliases=False``, or subclassing without
|
|
226
|
+
``extend=True``.
|
|
227
|
+
"""
|
|
228
|
+
cls = super().__new__(mcls, name, bases, ns)
|
|
229
|
+
|
|
230
|
+
# --- Identify LiteralEnum bases ---
|
|
231
|
+
literal_bases: list[type] = [b for b in bases if isinstance(b, LiteralEnumMeta)]
|
|
232
|
+
is_subclass: bool = bool(literal_bases)
|
|
233
|
+
|
|
234
|
+
# The root LiteralEnum class itself has no members.
|
|
235
|
+
if not is_subclass:
|
|
236
|
+
cls._members_ = {}
|
|
237
|
+
cls._ordered_values_ = ()
|
|
238
|
+
cls._value_keys_ = frozenset()
|
|
239
|
+
cls._value_names_ = {}
|
|
240
|
+
cls._allow_aliases_ = True if allow_aliases is None else allow_aliases
|
|
241
|
+
cls._call_to_validate_ = False if call_to_validate is None else call_to_validate
|
|
242
|
+
cls.__members__ = MappingProxyType(cls._members_)
|
|
243
|
+
return cls
|
|
244
|
+
|
|
245
|
+
# --- Enforce single-base inheritance ---
|
|
246
|
+
if len(literal_bases) > 1:
|
|
247
|
+
raise TypeError(
|
|
248
|
+
f"{name} may not inherit from multiple LiteralEnum bases "
|
|
249
|
+
f"({', '.join(b.__name__ for b in literal_bases)})."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
base: type = literal_bases[0]
|
|
253
|
+
|
|
254
|
+
# --- Guard against accidental subclassing ---
|
|
255
|
+
# If the parent already has members and ``extend=True`` wasn't
|
|
256
|
+
# passed, the user probably didn't intend to widen the value set.
|
|
257
|
+
if not extend and getattr(base, "_members_", {}):
|
|
258
|
+
raise TypeError(
|
|
259
|
+
f"{name} inherits from {base.__name__}; use "
|
|
260
|
+
f"`class {name}({base.__name__}, extend=True): ...` "
|
|
261
|
+
"to inherit and extend members. "
|
|
262
|
+
"Subclassing without extend=True is not allowed."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# --- Resolve inheritable flags: explicit kwarg wins, else inherit ---
|
|
266
|
+
if allow_aliases is None:
|
|
267
|
+
allow_aliases = getattr(base, "_allow_aliases_", True)
|
|
268
|
+
cls._allow_aliases_ = allow_aliases
|
|
269
|
+
|
|
270
|
+
if call_to_validate is None:
|
|
271
|
+
call_to_validate = getattr(base, "_call_to_validate_", False)
|
|
272
|
+
cls._call_to_validate_ = call_to_validate
|
|
273
|
+
|
|
274
|
+
# --- Seed from parent if extending, otherwise start fresh ---
|
|
275
|
+
if extend:
|
|
276
|
+
members: dict[str, Any] = dict(getattr(base, "_members_", {}))
|
|
277
|
+
values: list[Any] = list(getattr(base, "_ordered_values_", ()))
|
|
278
|
+
value_keys: set[tuple[type, object]] = set(getattr(base, "_value_keys_", frozenset()))
|
|
279
|
+
# Deep-copy so appending alias names doesn't mutate the parent.
|
|
280
|
+
value_names: dict[tuple[type, object], list[str]] = {
|
|
281
|
+
k: list(v) for k, v in getattr(base, "_value_names_", {}).items()
|
|
282
|
+
}
|
|
283
|
+
else:
|
|
284
|
+
members = {}
|
|
285
|
+
values = []
|
|
286
|
+
value_keys = set()
|
|
287
|
+
value_names = {}
|
|
288
|
+
|
|
289
|
+
ignore: set[str] = _parse_ignore(ns)
|
|
290
|
+
|
|
291
|
+
# --- Scan namespace for member candidates ---
|
|
292
|
+
# A name is treated as a member if it:
|
|
293
|
+
# - is not in the _ignore_ set
|
|
294
|
+
# - does not start with "_"
|
|
295
|
+
# - is not a descriptor (function, property, classmethod, etc.)
|
|
296
|
+
# - has a value whose type is allowed by typing.Literal
|
|
297
|
+
for k, v in ns.items():
|
|
298
|
+
if k in ignore:
|
|
299
|
+
continue
|
|
300
|
+
if k.startswith("_"):
|
|
301
|
+
continue
|
|
302
|
+
if _is_descriptor(v):
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
if not _is_literal_type(v):
|
|
306
|
+
raise TypeError(
|
|
307
|
+
f"Member '{name}.{k}' has value {v!r} (type {type(v).__name__}), "
|
|
308
|
+
"not a supported Literal value."
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Prevent name collisions when extending a parent.
|
|
312
|
+
if extend and k in members:
|
|
313
|
+
raise TypeError(
|
|
314
|
+
f"Member name '{name}.{k}' conflicts with inherited member "
|
|
315
|
+
f"'{base.__name__}.{k}'."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
members[k] = v
|
|
319
|
+
|
|
320
|
+
# Deduplicate by strict key so that e.g. True and 1 remain
|
|
321
|
+
# distinct, but the same (type, value) pair isn't added twice.
|
|
322
|
+
# Duplicate values are permitted; the first declared name is
|
|
323
|
+
# canonical. Later names for the same value become aliases.
|
|
324
|
+
key: tuple[type, object] = _strict_key(v)
|
|
325
|
+
if key not in value_keys:
|
|
326
|
+
value_keys.add(key)
|
|
327
|
+
values.append(v)
|
|
328
|
+
value_names[key] = [k]
|
|
329
|
+
else:
|
|
330
|
+
if not allow_aliases:
|
|
331
|
+
canonical: str = value_names[key][0]
|
|
332
|
+
raise TypeError(
|
|
333
|
+
f"Duplicate value {v!r} in '{name}': "
|
|
334
|
+
f"'{k}' conflicts with canonical member '{canonical}'. "
|
|
335
|
+
f"Use allow_aliases=True to permit aliases."
|
|
336
|
+
)
|
|
337
|
+
value_names[key].append(k)
|
|
338
|
+
|
|
339
|
+
# --- Freeze the collected members onto the class ---
|
|
340
|
+
cls._members_ = members
|
|
341
|
+
cls._ordered_values_ = tuple(values)
|
|
342
|
+
cls._value_keys_ = frozenset(value_keys)
|
|
343
|
+
cls._value_names_ = {k: tuple(v) for k, v in value_names.items()}
|
|
344
|
+
cls.__members__ = MappingProxyType(cls._members_)
|
|
345
|
+
return cls
|
|
346
|
+
|
|
347
|
+
# ---- Container protocol (operates on the *class*, not instances) ----
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def mapping(cls) -> Mapping[str, Any]:
|
|
351
|
+
"""A read-only ``{name: value}`` mapping of all members, including aliases."""
|
|
352
|
+
return cls.__members__
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def unique_mapping(cls) -> Mapping[str, Any]:
|
|
356
|
+
"""A read-only ``{name: value}`` mapping of canonical members only.
|
|
357
|
+
|
|
358
|
+
Aliases are excluded — each unique value appears under its
|
|
359
|
+
first-declared name only::
|
|
360
|
+
|
|
361
|
+
class Method(LiteralEnum):
|
|
362
|
+
GET = "GET"
|
|
363
|
+
get = "GET" # alias
|
|
364
|
+
|
|
365
|
+
Method.unique_mapping # {"GET": "GET"}
|
|
366
|
+
Method.mapping # {"GET": "GET", "get": "GET"}
|
|
367
|
+
"""
|
|
368
|
+
return MappingProxyType({
|
|
369
|
+
names[0]: cls._members_[names[0]]
|
|
370
|
+
for names in cls._value_names_.values()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
def keys(cls) -> tuple[str, ...]:
|
|
374
|
+
"""Return canonical member names in definition order (aliases excluded)."""
|
|
375
|
+
return tuple(
|
|
376
|
+
names[0] for names in cls._value_names_.values()
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def values(cls) -> tuple[Any, ...]:
|
|
380
|
+
"""Return unique member values in definition order (aliases excluded).
|
|
381
|
+
|
|
382
|
+
Equivalent to ``tuple(cls)``.
|
|
383
|
+
"""
|
|
384
|
+
return cls._ordered_values_
|
|
385
|
+
|
|
386
|
+
def items(cls) -> tuple[tuple[str, Any], ...]:
|
|
387
|
+
"""Return ``(canonical_name, value)`` pairs in definition order (aliases excluded)."""
|
|
388
|
+
return tuple(zip(cls.keys(), cls._ordered_values_))
|
|
389
|
+
|
|
390
|
+
# ---- Alias introspection ----
|
|
391
|
+
|
|
392
|
+
def names(cls, value: object) -> tuple[str, ...]:
|
|
393
|
+
"""Return all declared names for *value*, in definition order.
|
|
394
|
+
|
|
395
|
+
The first element is the canonical name; any subsequent elements
|
|
396
|
+
are aliases. Useful for synonyms, deprecations, or backwards
|
|
397
|
+
compatibility::
|
|
398
|
+
|
|
399
|
+
class Method(LiteralEnum):
|
|
400
|
+
GET = "GET"
|
|
401
|
+
get = "GET" # alias
|
|
402
|
+
|
|
403
|
+
Method.names("GET") # ("GET", "get")
|
|
404
|
+
Method.canonical_name("GET") # "GET"
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
KeyError: If *value* is not a member of this LiteralEnum.
|
|
408
|
+
"""
|
|
409
|
+
try:
|
|
410
|
+
return cls._value_names_[_strict_key(value)]
|
|
411
|
+
except KeyError:
|
|
412
|
+
raise KeyError(
|
|
413
|
+
f"{value!r} is not a member of {cls.__name__}"
|
|
414
|
+
) from None
|
|
415
|
+
|
|
416
|
+
def canonical_name(cls, value: object) -> str:
|
|
417
|
+
"""Return the canonical (first-declared) name for *value*.
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
KeyError: If *value* is not a member of this LiteralEnum.
|
|
421
|
+
"""
|
|
422
|
+
return cls.names(value)[0]
|
|
423
|
+
|
|
424
|
+
def __iter__(cls) -> Iterator[Any]:
|
|
425
|
+
"""Iterate over unique member *values* in first-seen order.
|
|
426
|
+
|
|
427
|
+
Aliases are collapsed — each underlying value appears exactly once.
|
|
428
|
+
|
|
429
|
+
Example::
|
|
430
|
+
|
|
431
|
+
class Method(LiteralEnum):
|
|
432
|
+
GET = "GET"
|
|
433
|
+
get = "GET" # alias, not yielded separately
|
|
434
|
+
|
|
435
|
+
list(Method) # ["GET"]
|
|
436
|
+
"""
|
|
437
|
+
return iter(cls._ordered_values_)
|
|
438
|
+
|
|
439
|
+
def __reversed__(cls) -> Iterator[Any]:
|
|
440
|
+
"""Iterate over unique member values in reverse definition order.
|
|
441
|
+
|
|
442
|
+
Example::
|
|
443
|
+
|
|
444
|
+
list(reversed(HttpMethod)) # ["DELETE", "POST", "GET"]
|
|
445
|
+
"""
|
|
446
|
+
return reversed(cls._ordered_values_)
|
|
447
|
+
|
|
448
|
+
def __len__(cls) -> int:
|
|
449
|
+
"""Return the number of unique member values (aliases are not counted)."""
|
|
450
|
+
return len(cls._ordered_values_)
|
|
451
|
+
|
|
452
|
+
def __bool__(cls) -> bool:
|
|
453
|
+
"""A LiteralEnum class is truthy if it has any members.
|
|
454
|
+
|
|
455
|
+
Example::
|
|
456
|
+
|
|
457
|
+
class Empty(LiteralEnum):
|
|
458
|
+
pass
|
|
459
|
+
|
|
460
|
+
bool(Empty) # False
|
|
461
|
+
bool(HttpMethod) # True
|
|
462
|
+
"""
|
|
463
|
+
return bool(cls._ordered_values_)
|
|
464
|
+
|
|
465
|
+
def __contains__(cls, value: object) -> bool:
|
|
466
|
+
"""Test membership using strict (type-aware) equality.
|
|
467
|
+
|
|
468
|
+
Example::
|
|
469
|
+
|
|
470
|
+
"GET" in HttpMethod # True
|
|
471
|
+
"git" in HttpMethod # False
|
|
472
|
+
True in BoolFlags # won't collide with 1
|
|
473
|
+
|
|
474
|
+
Returns ``False`` for unhashable values instead of raising.
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
return _strict_key(value) in cls._value_keys_
|
|
478
|
+
except TypeError:
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def __getitem__(cls, key: str) -> Any:
|
|
482
|
+
"""Look up a member value by its attribute name.
|
|
483
|
+
|
|
484
|
+
Example::
|
|
485
|
+
|
|
486
|
+
HttpMethod["GET"] # "GET"
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
KeyError: If *key* is not a member name.
|
|
490
|
+
"""
|
|
491
|
+
try:
|
|
492
|
+
return cls._members_[key]
|
|
493
|
+
except KeyError:
|
|
494
|
+
raise KeyError(f"'{key}' is not a member of {cls.__name__}") from None
|
|
495
|
+
|
|
496
|
+
def __repr__(cls) -> str:
|
|
497
|
+
"""Return a readable representation of the LiteralEnum class.
|
|
498
|
+
|
|
499
|
+
Example::
|
|
500
|
+
|
|
501
|
+
repr(HttpMethod)
|
|
502
|
+
# "<LiteralEnum 'HttpMethod' [GET='GET', POST='POST', DELETE='DELETE']>"
|
|
503
|
+
"""
|
|
504
|
+
if not cls._members_:
|
|
505
|
+
return f"<LiteralEnum '{cls.__name__}'>"
|
|
506
|
+
members: str = ", ".join(f"{k}={v!r}" for k, v in cls._members_.items())
|
|
507
|
+
return f"<LiteralEnum '{cls.__name__}' [{members}]>"
|
|
508
|
+
|
|
509
|
+
def __or__(cls, other: LiteralEnumMeta) -> LiteralEnumMeta:
|
|
510
|
+
"""Combine two LiteralEnums into a new anonymous LiteralEnum.
|
|
511
|
+
|
|
512
|
+
The result contains the union of both value sets. Canonical name
|
|
513
|
+
order is: all of *cls*'s values first, then *other*'s new values.
|
|
514
|
+
|
|
515
|
+
Example::
|
|
516
|
+
|
|
517
|
+
class Get(LiteralEnum):
|
|
518
|
+
GET = "GET"
|
|
519
|
+
|
|
520
|
+
class Post(LiteralEnum):
|
|
521
|
+
POST = "POST"
|
|
522
|
+
|
|
523
|
+
Combined = Get | Post
|
|
524
|
+
list(Combined) # ["GET", "POST"]
|
|
525
|
+
"GET" in Combined # True
|
|
526
|
+
Combined.__name__ # "Get|Post"
|
|
527
|
+
"""
|
|
528
|
+
if not isinstance(other, LiteralEnumMeta):
|
|
529
|
+
return NotImplemented
|
|
530
|
+
ns: dict[str, Any] = {}
|
|
531
|
+
ns.update(cls._members_)
|
|
532
|
+
ns.update(other._members_)
|
|
533
|
+
combined_name: str = f"{cls.__name__}|{other.__name__}"
|
|
534
|
+
return LiteralEnumMeta(combined_name, (LiteralEnum,), ns)
|
|
535
|
+
|
|
536
|
+
def __and__(cls, other: LiteralEnumMeta) -> LiteralEnumMeta:
|
|
537
|
+
"""Intersect two LiteralEnums into a new anonymous LiteralEnum.
|
|
538
|
+
|
|
539
|
+
The result contains only values present in *both* operands.
|
|
540
|
+
Names and order are taken from the left operand (*cls*).
|
|
541
|
+
|
|
542
|
+
Example::
|
|
543
|
+
|
|
544
|
+
class ReadWrite(LiteralEnum):
|
|
545
|
+
GET = "GET"
|
|
546
|
+
POST = "POST"
|
|
547
|
+
|
|
548
|
+
class ReadOnly(LiteralEnum):
|
|
549
|
+
GET = "GET"
|
|
550
|
+
HEAD = "HEAD"
|
|
551
|
+
|
|
552
|
+
Common = ReadWrite & ReadOnly
|
|
553
|
+
list(Common) # ["GET"]
|
|
554
|
+
Common.__name__ # "ReadWrite&ReadOnly"
|
|
555
|
+
"""
|
|
556
|
+
if not isinstance(other, LiteralEnumMeta):
|
|
557
|
+
return NotImplemented
|
|
558
|
+
ns: dict[str, Any] = {
|
|
559
|
+
k: v for k, v in cls._members_.items()
|
|
560
|
+
if _strict_key(v) in other._value_keys_
|
|
561
|
+
}
|
|
562
|
+
combined_name: str = f"{cls.__name__}&{other.__name__}"
|
|
563
|
+
return LiteralEnumMeta(combined_name, (LiteralEnum,), ns)
|
|
564
|
+
|
|
565
|
+
def __call__(cls, value: Any) -> Any:
|
|
566
|
+
"""Call the LiteralEnum class to validate a value.
|
|
567
|
+
|
|
568
|
+
Behavior depends on ``call_to_validate``:
|
|
569
|
+
|
|
570
|
+
* ``False`` (default): raises ``TypeError`` — LiteralEnum is not
|
|
571
|
+
instantiable.
|
|
572
|
+
* ``True``: validates *value* and returns it if it's a member,
|
|
573
|
+
otherwise raises ``ValueError``. Equivalent to
|
|
574
|
+
``cls.validate(value)``::
|
|
575
|
+
|
|
576
|
+
class HttpMethod(LiteralEnum, call_to_validate=True):
|
|
577
|
+
GET = "GET"
|
|
578
|
+
POST = "POST"
|
|
579
|
+
|
|
580
|
+
HttpMethod("GET") # "GET"
|
|
581
|
+
HttpMethod("git") # ValueError
|
|
582
|
+
"""
|
|
583
|
+
if cls._call_to_validate_:
|
|
584
|
+
return validate_is_member(cls, value)
|
|
585
|
+
raise TypeError(
|
|
586
|
+
f"{cls.__name__} is not instantiable; "
|
|
587
|
+
f"use {cls.__name__}.validate(x) or x in {cls.__name__}"
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# ---- Validation helpers (available as classmethods on the literalenum) ----
|
|
591
|
+
|
|
592
|
+
def is_valid(cls: type[LE], x: object) -> TypeGuard[LE]:
|
|
593
|
+
"""Check if *x* is a valid member value (with type narrowing).
|
|
594
|
+
|
|
595
|
+
Equivalent to ``is_member(cls, x)`` but available as a method on
|
|
596
|
+
the class itself::
|
|
597
|
+
|
|
598
|
+
if HttpMethod.is_valid(user_input):
|
|
599
|
+
... # user_input is narrowed to HttpMethod
|
|
600
|
+
"""
|
|
601
|
+
return is_member(cls, x)
|
|
602
|
+
|
|
603
|
+
def validate(cls: type[LE], x: object) -> LE:
|
|
604
|
+
"""Validate *x* is a member value, or raise ``ValueError``.
|
|
605
|
+
|
|
606
|
+
Equivalent to ``validate_is_member(cls, x)``::
|
|
607
|
+
|
|
608
|
+
method = HttpMethod.validate(user_input) # raises on bad input
|
|
609
|
+
"""
|
|
610
|
+
return validate_is_member(cls, x)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ---------------------------------------------------------------------------
|
|
614
|
+
# Base class
|
|
615
|
+
# ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
class LiteralEnum(metaclass=LiteralEnumMeta):
|
|
618
|
+
"""Base class for defining a set of named literal values.
|
|
619
|
+
|
|
620
|
+
Subclass ``LiteralEnum`` and assign literal values as class attributes::
|
|
621
|
+
|
|
622
|
+
class Color(LiteralEnum):
|
|
623
|
+
RED = "red"
|
|
624
|
+
GREEN = "green"
|
|
625
|
+
BLUE = "blue"
|
|
626
|
+
|
|
627
|
+
At runtime:
|
|
628
|
+
|
|
629
|
+
* ``Color.RED`` evaluates to ``"red"`` (a plain ``str``).
|
|
630
|
+
* ``list(Color)`` returns ``["red", "green", "blue"]``.
|
|
631
|
+
* ``"red" in Color`` returns ``True``.
|
|
632
|
+
* ``Color.validate(x)`` returns *x* if valid or raises ``ValueError``.
|
|
633
|
+
|
|
634
|
+
At type-check time, ``Color`` is intended to be equivalent to
|
|
635
|
+
``Literal["red", "green", "blue"]``.
|
|
636
|
+
|
|
637
|
+
``LiteralEnum`` is **not instantiable** — its values are plain scalars,
|
|
638
|
+
not wrapper objects. Use ``validate()`` or ``is_valid()`` instead.
|
|
639
|
+
|
|
640
|
+
Duplicate values are permitted; the first declared name is canonical
|
|
641
|
+
and later names are aliases::
|
|
642
|
+
|
|
643
|
+
class Method(LiteralEnum):
|
|
644
|
+
GET = "GET"
|
|
645
|
+
get = "GET" # alias for GET
|
|
646
|
+
|
|
647
|
+
Method.names("GET") # ("GET", "get")
|
|
648
|
+
Method.canonical_name("GET") # "GET"
|
|
649
|
+
list(Method) # ["GET"] (aliases not yielded)
|
|
650
|
+
|
|
651
|
+
To extend an existing LiteralEnum, pass ``extend=True``::
|
|
652
|
+
|
|
653
|
+
class ExtendedColor(Color, extend=True):
|
|
654
|
+
YELLOW = "yellow"
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
def __new__(cls, value: Never) -> NoReturn | LE:
|
|
658
|
+
"""Signal to type checkers that LiteralEnum is not instantiable.
|
|
659
|
+
|
|
660
|
+
At runtime, the metaclass ``__call__`` intercepts before this is
|
|
661
|
+
reached — either validating (``call_to_validate=True``) or raising
|
|
662
|
+
``TypeError``. This method exists solely so that type checkers
|
|
663
|
+
flag ``HttpMethod("GET")`` as an error by default.
|
|
664
|
+
"""
|
|
665
|
+
if cls._call_to_validate_:
|
|
666
|
+
return validate_is_member(cls, value)
|
|
667
|
+
raise TypeError(
|
|
668
|
+
f"{cls.__name__} is not instantiable; "
|
|
669
|
+
f"use {cls.__name__}.validate(x) or x in {cls.__name__}"
|
|
670
|
+
)
|