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.
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
+ )