literalenum 0.2.0__tar.gz → 0.4.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.
Files changed (47) hide show
  1. literalenum-0.4.0/CONTRIBUTING.md +129 -0
  2. literalenum-0.4.0/PITCH.md +32 -0
  3. {literalenum-0.2.0 → literalenum-0.4.0}/PKG-INFO +1 -3
  4. {literalenum-0.2.0 → literalenum-0.4.0}/README.md +0 -2
  5. {literalenum-0.2.0 → literalenum-0.4.0}/pyproject.toml +1 -1
  6. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/stubgen.py +58 -18
  7. {literalenum-0.2.0 → literalenum-0.4.0}/src/typing_literalenum.py +98 -0
  8. {literalenum-0.2.0 → literalenum-0.4.0}/tests/test_typing_literalenum.py +127 -0
  9. {literalenum-0.2.0 → literalenum-0.4.0}/.github/workflows/publish.yml +0 -0
  10. {literalenum-0.2.0 → literalenum-0.4.0}/.gitignore +0 -0
  11. {literalenum-0.2.0 → literalenum-0.4.0}/LICENSE +0 -0
  12. {literalenum-0.2.0 → literalenum-0.4.0}/LITMUS.md +0 -0
  13. {literalenum-0.2.0 → literalenum-0.4.0}/PEP.md +0 -0
  14. {literalenum-0.2.0 → literalenum-0.4.0}/TYPING_DISCUSSION.md +0 -0
  15. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/__init__.py +0 -0
  16. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/__init__.py +0 -0
  17. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/annotated.py +0 -0
  18. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/bare_class.py +0 -0
  19. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/base_model.py +0 -0
  20. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/click_choice.py +0 -0
  21. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/django_choices.py +0 -0
  22. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/enum.py +0 -0
  23. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/graphene_enum.py +0 -0
  24. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/int_enum.py +0 -0
  25. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/json_schema.py +0 -0
  26. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/literal.py +0 -0
  27. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/random_choice.py +0 -0
  28. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/regex.py +0 -0
  29. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/sqlalchemy_enum.py +0 -0
  30. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/str_enum.py +0 -0
  31. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/strawberry_enum.py +0 -0
  32. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/literal_enum.py +0 -0
  33. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/mypy_plugin.py +0 -0
  34. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/py.typed +0 -0
  35. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/samples/__init__.py +0 -0
  36. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/samples/http.py +0 -0
  37. {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/samples/http.pyi +0 -0
  38. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/__init__.py +0 -0
  39. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/a_strenum.py +0 -0
  40. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/b_str_enum.py +0 -0
  41. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/c_enum.py +0 -0
  42. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/d_literal.py +0 -0
  43. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/e_literal_plus_namespace.py +0 -0
  44. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/f_literal_hack.py +0 -0
  45. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/g_custom_type.py +0 -0
  46. {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/h_custom_literal_namespace.py +0 -0
  47. {literalenum-0.2.0 → literalenum-0.4.0}/tests/test_literalenum.py +0 -0
@@ -0,0 +1,129 @@
1
+ # Contributing to LiteralEnum
2
+
3
+ Thanks for your interest in contributing. This document covers setup, project layout, and guidelines.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/modularizer/LiteralEnum.git
9
+ cd LiteralEnum
10
+ python -m venv .venv
11
+ source .venv/bin/activate
12
+ pip install -e .
13
+ pip install pytest
14
+ ```
15
+
16
+ Requires Python 3.10+.
17
+
18
+ ## Project layout
19
+
20
+ ```
21
+ src/
22
+ typing_literalenum.py # Core module (proposed for typing_extensions / stdlib)
23
+ literalenum/
24
+ __init__.py # Package entry point
25
+ literal_enum.py # Extended metaclass wrapping the core
26
+ mypy_plugin.py # mypy plugin
27
+ stubgen.py # .pyi stub generator (lestub CLI)
28
+ compatibility_extensions/ # Converters to other frameworks
29
+ enum.py, str_enum.py, ...
30
+ tests/
31
+ test_typing_literalenum.py # Tests for the core module
32
+ test_literalenum.py # Tests for the package (extended metaclass + compat)
33
+ test_typing.py # mypy type-checking validation
34
+ ```
35
+
36
+ The two-module split is intentional:
37
+
38
+ - **`typing_literalenum`** is the minimal, zero-dependency core proposed for the standard library. It should stay lean.
39
+ - **`literalenum`** is the full package with ecosystem integrations (Pydantic, SQLAlchemy, Django, GraphQL, etc.). This is what gets published to PyPI.
40
+
41
+ ## Running tests
42
+
43
+ ```bash
44
+ # All tests
45
+ pytest
46
+
47
+ # Core module only
48
+ pytest tests/test_typing_literalenum.py
49
+
50
+ # Package only
51
+ pytest tests/test_literalenum.py
52
+
53
+ # Verbose
54
+ pytest -v
55
+ ```
56
+
57
+ Tests for optional dependencies (strawberry, graphene, sqlalchemy, pydantic, click) are automatically skipped if the dependency isn't installed.
58
+
59
+ ## What goes where
60
+
61
+ | Change | File(s) |
62
+ |-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
63
+ | Core runtime behavior (iteration, containment, validation, aliases, extend) | `src/typing_literalenum.py` + `tests/test_typing_literalenum.py` |
64
+ | New compatibility converter (e.g. marshmallow, attrs) | New file in `compatibility_extensions/`, method in `literal_enum.py`, export in `compatibility_extensions/__init__.py`, test in `tests/test_literalenum.py` |
65
+ | Stub generation | `src/literalenum/stubgen.py` |
66
+ | mypy plugin | `src/literalenum/mypy_plugin.py` |
67
+
68
+ ## Adding a compatibility extension
69
+
70
+ 1. Create `src/literalenum/compatibility_extensions/my_thing.py`:
71
+ ```python
72
+ def my_thing(cls):
73
+ # cls is a LiteralEnum class with _ordered_values_, _members_, etc.
74
+ ...
75
+ ```
76
+
77
+ 2. Export it in `compatibility_extensions/__init__.py`:
78
+ ```python
79
+ from .my_thing import my_thing
80
+ ```
81
+
82
+ 3. Add the method to `LiteralEnumMeta` in `literal_enum.py`:
83
+ ```python
84
+ def my_thing(cls):
85
+ return compat.my_thing(cls)
86
+ ```
87
+
88
+ 4. Add tests in `tests/test_literalenum.py`. If the extension requires an optional dependency, use `pytest.importorskip`:
89
+ ```python
90
+ def test_my_thing(self):
91
+ pytest.importorskip("some_library")
92
+ result = HttpMethod.my_thing()
93
+ assert ...
94
+ ```
95
+
96
+ ## Guidelines
97
+
98
+ - **Don't add dependencies.** The core module has zero dependencies and must stay that way. The package has zero required dependencies; optional integrations use local imports.
99
+ - **Keep the core minimal.** `typing_literalenum.py` is a standard library proposal. Only add features there if they belong in `typing_extensions`.
100
+ - **Test what you add.** Every new method needs tests. Core features go in `test_typing_literalenum.py`, package features in `test_literalenum.py`.
101
+ - **Avoid breaking changes to the metaclass protocol.** Existing LiteralEnum subclasses shouldn't break when upgrading.
102
+
103
+ ## Stub generation
104
+
105
+ The `lestub` CLI generates `.pyi` stubs for LiteralEnum subclasses:
106
+
107
+ ```bash
108
+ # From a dotted import
109
+ lestub myapp.models
110
+
111
+ # From a file
112
+ lestub src/myapp/models.py
113
+
114
+ # From a directory
115
+ lestub src/myapp/
116
+
117
+ # Write overlay stubs to a directory
118
+ lestub myapp --out typings
119
+ ```
120
+
121
+ If you change the public API surface of `LiteralEnumMeta`, update `_render_enum_blocks` in `stubgen.py` to match.
122
+
123
+ ## Reporting issues
124
+
125
+ Open an issue at https://github.com/modularizer/LiteralEnum/issues.
126
+
127
+ ## License
128
+
129
+ This project is released under [The Unlicense](LICENSE) (public domain). By contributing, you agree that your contributions are released under the same terms.
@@ -0,0 +1,32 @@
1
+ # What if one object could be a namespace AND a typehint?
2
+
3
+ ```python
4
+ # Wouldn't it be nice if this worked?
5
+ from typing import LiteralEnum
6
+
7
+ class HttpMethod(LiteralEnum):
8
+ GET = "GET"
9
+ POST = "POST"
10
+
11
+ def handle(method: HttpMethod) -> None: ... # when used as a typehint, HttpMethod would behave like Literal["GET", "POST"]
12
+
13
+ # core
14
+ handle("GET") # type-checks: "GET" is a valid HttpMethod
15
+ handle(HttpMethod.POST) # type-checks: HttpMethod.POST is just "POST"
16
+ handle("PATCH") # type error: not a valid HttpMethod
17
+ assert list(HttpMethod) == ["GET", "POST"]
18
+ assert "GET" in HttpMethod # runtime validation
19
+
20
+ # NOTHING fancy, value is JUST the literal, not a subclass, not an instance of HttpMethod, not an instance of LiteralEnum, not modified in any way
21
+ assert HttpMethod.GET == "GET" and type(HttpMethod.GET) is str and HttpMethod.GET.__class__.__bases__ == (object,)
22
+
23
+ HttpMethod.validate(x) # raises ValueError if invalid
24
+ assert dict(HttpMethod.mapping) == {"GET": "GET", "POST": "POST"}
25
+ assert HttpMethod.names_by_value[HttpMethod.POST] == "POST"
26
+ ```
27
+
28
+ One definition. Plain raw literals at runtime. Exhaustive checking at type-check time.
29
+
30
+ - A realistic runtime version is available at `pip install literalenum` (but is not as good as advertised above)
31
+ - Support in the [Python Community Discussion](https://discuss.python.org/t/proposal-literalenum-runtime-literals-with-static-exhaustiveness/106000/15)
32
+ would be needed to have a shot at a PEP that could improve Python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: literalenum
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A Python typing construct that provides namespaced literal constants with advanced typing features.
5
5
  Project-URL: Homepage, https://github.com/modularizer/literalenum
6
6
  Project-URL: Repository, https://github.com/modularizer/literalenum
@@ -12,8 +12,6 @@ Keywords: python,typehinting
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
 
15
- from sample_str_enum_solutions.a_strenum import HttpMethod
16
-
17
15
  # LiteralEnum
18
16
 
19
17
  **LiteralEnum** is an experiment/prototype for a proposed Python typing construct:
@@ -1,5 +1,3 @@
1
- from sample_str_enum_solutions.a_strenum import HttpMethod
2
-
3
1
  # LiteralEnum
4
2
 
5
3
  **LiteralEnum** is an experiment/prototype for a proposed Python typing construct:
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "literalenum"
8
- version = "0.2.0"
8
+ version = "0.4.0"
9
9
  description = "A Python typing construct that provides namespaced literal constants with advanced typing features."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -129,10 +129,10 @@ def _render_enum_blocks(enums: list[EnumInfo]) -> str:
129
129
 
130
130
  out: list[str] = []
131
131
  for e in sorted(enums, key=lambda x: x.name):
132
- alias = f"{e.name}T"
132
+ T = f"{e.name}T"
133
133
  values = list(e.members.values())
134
134
  literal_union = ", ".join(_py_literal(v) for v in values) if values else ""
135
- out.append(f"{alias}: TypeAlias = Literal[{literal_union}]\n\n")
135
+ out.append(f"{T}: TypeAlias = Literal[{literal_union}]\n\n")
136
136
 
137
137
  base = _enum_base_decl(e)
138
138
  inherited = _inherited_member_names(e)
@@ -141,27 +141,62 @@ def _render_enum_blocks(enums: list[EnumInfo]) -> str:
141
141
  own_members = [(k, v) for (k, v) in e.members.items() if k not in inherited]
142
142
 
143
143
  out.append(f"class {e.name}({base}):\n")
144
- out.append(f" T_ = Literal[{literal_union}]\n")
145
- if not own_members:
146
- out.append(" ...\n")
147
- else:
144
+
145
+ # -- Members --
146
+ if own_members:
148
147
  for k, v in own_members:
149
148
  out.append(f" {k}: Final[Literal[{_py_literal(v)}]] = {_py_literal(v)}\n")
149
+ else:
150
+ out.append(" ...\n")
151
+ out.append("\n")
152
+
153
+ # -- Mappings --
154
+ out.append(f" mapping: ClassVar[MappingProxyType[str, {T}]]\n")
155
+ out.append(f" unique_mapping: ClassVar[MappingProxyType[str, {T}]]\n")
156
+ out.append(f" __members__: ClassVar[MappingProxyType[str, {T}]]\n\n")
157
+
158
+ # -- Dict-like --
159
+ out.append(" @classmethod\n")
160
+ out.append(" def keys(cls) -> tuple[str, ...]: ...\n")
161
+ out.append(" @classmethod\n")
162
+ out.append(f" def values(cls) -> tuple[{T}, ...]: ...\n")
163
+ out.append(" @classmethod\n")
164
+ out.append(f" def items(cls) -> tuple[tuple[str, {T}], ...]: ...\n\n")
150
165
 
151
- out.append(f" values: ClassVar[Iterable[{alias}]]\n")
152
- out.append(f" mapping: ClassVar[dict[str, {alias}]]\n\n")
166
+ # -- Alias introspection --
167
+ out.append(" @classmethod\n")
168
+ out.append(f" def names(cls, value: {T}) -> tuple[str, ...]: ...\n")
169
+ out.append(" @classmethod\n")
170
+ out.append(f" def canonical_name(cls, value: {T}) -> str: ...\n\n")
153
171
 
172
+ # -- Validation --
173
+ out.append(" @classmethod\n")
174
+ out.append(f" def is_valid(cls, x: object) -> TypeGuard[{T}]: ...\n")
175
+ out.append(" @classmethod\n")
176
+ out.append(f" def validate(cls, x: object) -> {T}: ...\n\n")
177
+
178
+ # -- Container protocol --
179
+ out.append(f" def __iter__(cls) -> Iterator[{T}]: ...\n")
180
+ out.append(f" def __reversed__(cls) -> Iterator[{T}]: ...\n")
181
+ out.append(" def __len__(cls) -> int: ...\n")
182
+ out.append(" def __bool__(cls) -> bool: ...\n")
183
+ out.append(" def __contains__(cls, value: object) -> bool: ...\n")
184
+ out.append(f" def __getitem__(cls, key: str) -> {T}: ...\n")
185
+ out.append(" def __repr__(cls) -> str: ...\n\n")
186
+
187
+ # -- Operators --
188
+ out.append(" def __or__(cls, other: LiteralEnumMeta) -> LiteralEnumMeta: ...\n")
189
+ out.append(" def __and__(cls, other: LiteralEnumMeta) -> LiteralEnumMeta: ...\n\n")
190
+
191
+ # -- Constructor --
154
192
  if e.call_to_validate:
155
193
  out.append(" @overload\n")
156
- out.append(f" def __new__(cls, value: {alias}) -> {alias}: ...\n")
194
+ out.append(f" def __new__(cls, value: {T}) -> {T}: ...\n")
157
195
  out.append(" @overload\n")
158
- out.append(f" def __new__(cls, value: object) -> {alias}: ...\n\n")
196
+ out.append(f" def __new__(cls, value: object) -> {T}: ...\n\n")
159
197
  else:
160
198
  out.append(f" def __new__(cls, value: Never) -> NoReturn: ...\n\n")
161
199
 
162
- out.append(" @classmethod\n")
163
- out.append(f" def is_member(cls, value: object) -> TypeGuard[{alias}]: ...\n\n")
164
-
165
200
  return "".join(out)
166
201
 
167
202
 
@@ -202,8 +237,9 @@ def _render_overlay_stub_module(enums: list[EnumInfo]) -> str:
202
237
  """
203
238
  out: list[str] = []
204
239
  out.append("from __future__ import annotations\n")
205
- out.append("from typing import ClassVar, Final, Literal, Iterable, Never, NoReturn, TypeGuard, TypeAlias, overload\n")
206
- out.append("from literalenum import LiteralEnum\n\n")
240
+ out.append("from types import MappingProxyType\n")
241
+ out.append("from typing import ClassVar, Final, Iterator, Literal, Never, NoReturn, TypeAlias, TypeGuard, overload\n")
242
+ out.append("from literalenum import LiteralEnum, LiteralEnumMeta\n\n")
207
243
  out.append(_render_enum_blocks(enums))
208
244
  return "".join(out)
209
245
 
@@ -212,8 +248,9 @@ def _render_overlay_stub_module(enums: list[EnumInfo]) -> str:
212
248
  # Adjacent stubs (preserve module)
213
249
  # ----------------------------
214
250
 
215
- _TYPING_INJECT = "from typing import ClassVar, Final, Literal, Iterable, Never, NoReturn, TypeGuard, TypeAlias, overload"
216
- _LITERALENUM_INJECT = "from literalenum import LiteralEnum"
251
+ _TYPES_INJECT = "from types import MappingProxyType"
252
+ _TYPING_INJECT = "from typing import ClassVar, Final, Iterator, Literal, Never, NoReturn, TypeAlias, TypeGuard, overload"
253
+ _LITERALENUM_INJECT = "from literalenum import LiteralEnum, LiteralEnumMeta"
217
254
  _FUTURE = "from __future__ import annotations"
218
255
 
219
256
 
@@ -250,10 +287,12 @@ def _normalize_imports(import_lines: list[str]) -> list[str]:
250
287
  continue
251
288
  if s.startswith("from literalenum import") and "LiteralEnum" in s:
252
289
  continue
290
+ if s.startswith("from types import") and "MappingProxyType" in s:
291
+ continue
253
292
  if s.startswith("from typing import"):
254
293
  # drop any typing line that imports our injected symbols
255
294
  # (simple heuristic: if it mentions any of them, drop it)
256
- symbols = ["ClassVar", "Final", "Literal", "Iterable", "TypeGuard", "overload"]
295
+ symbols = ["ClassVar", "Final", "Iterator", "Literal", "TypeGuard", "overload"]
257
296
  if any(sym in s for sym in symbols):
258
297
  continue
259
298
  out.append(s)
@@ -319,6 +358,7 @@ def _render_adjacent_preserving_stub(module: str, enums: list[EnumInfo]) -> str:
319
358
  out.append("\n")
320
359
 
321
360
  # Inject what enum blocks need, exactly once
361
+ out.append(_TYPES_INJECT + "\n")
322
362
  out.append(_TYPING_INJECT + "\n")
323
363
  out.append(_LITERALENUM_INJECT + "\n\n")
324
364
 
@@ -370,6 +370,48 @@ class LiteralEnumMeta(type):
370
370
  for names in cls._value_names_.values()
371
371
  })
372
372
 
373
+ @property
374
+ def name_mapping(cls) -> Mapping[Any, str]:
375
+ """A read-only ``{value: canonical_name}`` inverse mapping.
376
+
377
+ Each unique value maps to its first-declared (canonical) name::
378
+
379
+ class Method(LiteralEnum):
380
+ GET = "GET"
381
+ get = "GET" # alias
382
+
383
+ Method.name_mapping # {"GET": "GET"}
384
+
385
+ See also :attr:`names_mapping` for all names including aliases.
386
+ """
387
+ return MappingProxyType({
388
+ v: names[0]
389
+ for v, names in zip(cls._ordered_values_, cls._value_names_.values())
390
+ })
391
+
392
+ @property
393
+ def names_by_value(cls) -> Mapping[Any, str]:
394
+ return cls.name_mapping
395
+
396
+
397
+ @property
398
+ def names_mapping(cls) -> Mapping[Any, tuple[str, ...]]:
399
+ """A read-only ``{value: (name, ...)}`` inverse mapping.
400
+
401
+ Each unique value maps to a tuple of all its declared names
402
+ (canonical first, then aliases)::
403
+
404
+ class Method(LiteralEnum):
405
+ GET = "GET"
406
+ get = "GET" # alias
407
+
408
+ Method.names_mapping # {"GET": ("GET", "get")}
409
+ """
410
+ return MappingProxyType({
411
+ v: names
412
+ for v, names in zip(cls._ordered_values_, cls._value_names_.values())
413
+ })
414
+
373
415
  def keys(cls) -> tuple[str, ...]:
374
416
  """Return canonical member names in definition order (aliases excluded)."""
375
417
  return tuple(
@@ -609,6 +651,62 @@ class LiteralEnumMeta(type):
609
651
  """
610
652
  return validate_is_member(cls, x)
611
653
 
654
+ # ---- Testing utilities ----
655
+
656
+ def matches_enum(cls, enum_cls: type) -> bool:
657
+ """Check whether this LiteralEnum has exactly the same values as *enum_cls*.
658
+
659
+ Compares the set of unique values in this LiteralEnum against the
660
+ ``.value`` of every member in the given ``enum.Enum`` (or subclass).
661
+ Useful in test suites to assert two parallel definitions stay in sync::
662
+
663
+ import enum
664
+
665
+ class Color(enum.StrEnum):
666
+ RED = "red"
667
+ GREEN = "green"
668
+
669
+ class ColorLE(LiteralEnum):
670
+ RED = "red"
671
+ GREEN = "green"
672
+
673
+ assert ColorLE.matches_enum(Color)
674
+
675
+ Returns:
676
+ ``True`` if the value sets are identical.
677
+ """
678
+ try:
679
+ enum_values = {m.value for m in enum_cls}
680
+ except TypeError:
681
+ return False
682
+ return set(cls._ordered_values_) == enum_values
683
+
684
+ def matches_literal(cls, literal_type: Any) -> bool:
685
+ """Check whether this LiteralEnum has exactly the same values as *literal_type*.
686
+
687
+ Extracts the arguments from a ``typing.Literal[...]`` and compares
688
+ them against this LiteralEnum's unique values. Useful in test suites
689
+ to assert a Literal type alias stays in sync with a LiteralEnum::
690
+
691
+ from typing import Literal
692
+
693
+ ColorLiteral = Literal["red", "green"]
694
+
695
+ class Color(LiteralEnum):
696
+ RED = "red"
697
+ GREEN = "green"
698
+
699
+ assert Color.matches_literal(ColorLiteral)
700
+
701
+ Returns:
702
+ ``True`` if the value sets are identical.
703
+ """
704
+ from typing import get_args
705
+ args = get_args(literal_type)
706
+ if not args:
707
+ return False
708
+ return set(cls._ordered_values_) == set(args)
709
+
612
710
 
613
711
  # ---------------------------------------------------------------------------
614
712
  # Base class
@@ -250,6 +250,36 @@ class TestMappings:
250
250
  assert isinstance(HttpMethod.__members__, MappingProxyType)
251
251
  assert dict(HttpMethod.__members__) == dict(HttpMethod.mapping)
252
252
 
253
+ def test_name_mapping(self):
254
+ assert isinstance(HttpMethod.name_mapping, MappingProxyType)
255
+ assert dict(HttpMethod.name_mapping) == {
256
+ "GET": "GET", "POST": "POST", "DELETE": "DELETE",
257
+ }
258
+
259
+ def test_name_mapping_returns_canonical(self):
260
+ assert dict(WithAliases.name_mapping) == {"GET": "GET", "POST": "POST"}
261
+
262
+ def test_name_mapping_int(self):
263
+ assert dict(StatusCode.name_mapping) == {200: "OK", 404: "NOT_FOUND"}
264
+
265
+ def test_names_mapping(self):
266
+ assert isinstance(HttpMethod.names_mapping, MappingProxyType)
267
+ assert dict(HttpMethod.names_mapping) == {
268
+ "GET": ("GET",), "POST": ("POST",), "DELETE": ("DELETE",),
269
+ }
270
+
271
+ def test_names_mapping_with_aliases(self):
272
+ assert dict(WithAliases.names_mapping) == {
273
+ "GET": ("GET", "get"),
274
+ "POST": ("POST", "post"),
275
+ }
276
+
277
+ def test_name_mapping_empty(self):
278
+ assert dict(Empty.name_mapping) == {}
279
+
280
+ def test_names_mapping_empty(self):
281
+ assert dict(Empty.names_mapping) == {}
282
+
253
283
 
254
284
  # ===================================================================
255
285
  # Aliases
@@ -629,6 +659,103 @@ class TestAnd:
629
659
  assert list(C) == [] # True and 1 are distinct by strict key
630
660
 
631
661
 
662
+ # ===================================================================
663
+ # matches_enum / matches_literal
664
+ # ===================================================================
665
+
666
+ class TestMatchesEnum:
667
+ def test_matches_identical(self):
668
+ import enum
669
+
670
+ class E(enum.Enum):
671
+ GET = "GET"
672
+ POST = "POST"
673
+ DELETE = "DELETE"
674
+
675
+ assert HttpMethod.matches_enum(E) is True
676
+
677
+ def test_matches_strenum(self):
678
+ import enum
679
+
680
+ class E(enum.StrEnum):
681
+ GET = "GET"
682
+ POST = "POST"
683
+ DELETE = "DELETE"
684
+
685
+ assert HttpMethod.matches_enum(E) is True
686
+
687
+ def test_matches_intenum(self):
688
+ import enum
689
+
690
+ class E(enum.IntEnum):
691
+ OK = 200
692
+ NOT_FOUND = 404
693
+
694
+ assert StatusCode.matches_enum(E) is True
695
+
696
+ def test_mismatch_extra_in_enum(self):
697
+ import enum
698
+
699
+ class E(enum.Enum):
700
+ GET = "GET"
701
+ POST = "POST"
702
+ DELETE = "DELETE"
703
+ PATCH = "PATCH"
704
+
705
+ assert HttpMethod.matches_enum(E) is False
706
+
707
+ def test_mismatch_missing_in_enum(self):
708
+ import enum
709
+
710
+ class E(enum.Enum):
711
+ GET = "GET"
712
+
713
+ assert HttpMethod.matches_enum(E) is False
714
+
715
+ def test_mismatch_different_values(self):
716
+ import enum
717
+
718
+ class E(enum.Enum):
719
+ GET = "get"
720
+ POST = "post"
721
+ DELETE = "delete"
722
+
723
+ assert HttpMethod.matches_enum(E) is False
724
+
725
+ def test_non_enum_returns_false(self):
726
+ assert HttpMethod.matches_enum(str) is False
727
+
728
+
729
+ class TestMatchesLiteral:
730
+ def test_matches_identical(self):
731
+ from typing import Literal
732
+ assert HttpMethod.matches_literal(Literal["GET", "POST", "DELETE"]) is True
733
+
734
+ def test_matches_int_literal(self):
735
+ from typing import Literal
736
+ assert StatusCode.matches_literal(Literal[200, 404]) is True
737
+
738
+ def test_mismatch_extra(self):
739
+ from typing import Literal
740
+ assert HttpMethod.matches_literal(Literal["GET", "POST", "DELETE", "PATCH"]) is False
741
+
742
+ def test_mismatch_missing(self):
743
+ from typing import Literal
744
+ assert HttpMethod.matches_literal(Literal["GET"]) is False
745
+
746
+ def test_order_independent(self):
747
+ from typing import Literal
748
+ assert HttpMethod.matches_literal(Literal["DELETE", "GET", "POST"]) is True
749
+
750
+ def test_non_literal_returns_false(self):
751
+ assert HttpMethod.matches_literal(str) is False
752
+
753
+ def test_matches_type_alias(self):
754
+ from typing import Literal
755
+ MyType = Literal["GET", "POST", "DELETE"]
756
+ assert HttpMethod.matches_literal(MyType) is True
757
+
758
+
632
759
  # ===================================================================
633
760
  # Edge cases
634
761
  # ===================================================================
File without changes
File without changes
File without changes
File without changes