literalenum 0.1.1__tar.gz → 0.3.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 (46) hide show
  1. {literalenum-0.1.1 → literalenum-0.3.0}/PKG-INFO +30 -4
  2. {literalenum-0.1.1 → literalenum-0.3.0}/README.md +29 -3
  3. {literalenum-0.1.1 → literalenum-0.3.0}/pyproject.toml +1 -1
  4. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/stubgen.py +102 -20
  5. {literalenum-0.1.1 → literalenum-0.3.0}/src/typing_literalenum.py +56 -0
  6. {literalenum-0.1.1 → literalenum-0.3.0}/tests/test_typing_literalenum.py +97 -0
  7. literalenum-0.1.1/pyrightconfig.json +0 -3
  8. {literalenum-0.1.1 → literalenum-0.3.0}/.github/workflows/publish.yml +0 -0
  9. {literalenum-0.1.1 → literalenum-0.3.0}/.gitignore +0 -0
  10. {literalenum-0.1.1 → literalenum-0.3.0}/LICENSE +0 -0
  11. {literalenum-0.1.1 → literalenum-0.3.0}/LITMUS.md +0 -0
  12. {literalenum-0.1.1 → literalenum-0.3.0}/PEP.md +0 -0
  13. {literalenum-0.1.1 → literalenum-0.3.0}/TYPING_DISCUSSION.md +0 -0
  14. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/__init__.py +0 -0
  15. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/__init__.py +0 -0
  16. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/annotated.py +0 -0
  17. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/bare_class.py +0 -0
  18. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/base_model.py +0 -0
  19. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/click_choice.py +0 -0
  20. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/django_choices.py +0 -0
  21. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/enum.py +0 -0
  22. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/graphene_enum.py +0 -0
  23. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/int_enum.py +0 -0
  24. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/json_schema.py +0 -0
  25. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/literal.py +0 -0
  26. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/random_choice.py +0 -0
  27. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/regex.py +0 -0
  28. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/sqlalchemy_enum.py +0 -0
  29. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/str_enum.py +0 -0
  30. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/compatibility_extensions/strawberry_enum.py +0 -0
  31. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/literal_enum.py +0 -0
  32. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/mypy_plugin.py +0 -0
  33. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/py.typed +0 -0
  34. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/samples/__init__.py +0 -0
  35. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/samples/http.py +0 -0
  36. {literalenum-0.1.1 → literalenum-0.3.0}/src/literalenum/samples/http.pyi +0 -0
  37. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/__init__.py +0 -0
  38. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/a_strenum.py +0 -0
  39. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/b_str_enum.py +0 -0
  40. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/c_enum.py +0 -0
  41. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/d_literal.py +0 -0
  42. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/e_literal_plus_namespace.py +0 -0
  43. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/f_literal_hack.py +0 -0
  44. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/g_custom_type.py +0 -0
  45. {literalenum-0.1.1 → literalenum-0.3.0}/src/sample_str_enum_solutions/h_custom_literal_namespace.py +0 -0
  46. {literalenum-0.1.1 → literalenum-0.3.0}/tests/test_literalenum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: literalenum
3
- Version: 0.1.1
3
+ Version: 0.3.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
@@ -60,10 +60,36 @@ This repo is currently set up as a package under `src/`.
60
60
  #source .venv/bin/activate
61
61
  pip install literalenum
62
62
  ```
63
-
63
+ ## Realistic Current Usage
64
64
  ```python
65
+ from typing import Literal
65
66
  from literalenum import LiteralEnum
66
67
 
68
+ HttpMethodT = Literal["GET", "POST", "DELETE"]
69
+ class HttpMethod(LiteralEnum):
70
+ GET = "GET"
71
+ POST = "POST"
72
+ DELETE = "DELETE"
73
+
74
+ def handle(method: HttpMethodT) -> None:
75
+ print(f"{method=}")
76
+
77
+ handle("GET") # this should type-check ✅
78
+ handle(HttpMethod.GET) # this should type-check ✅
79
+ handle("git") # ❌ should be rejected by a type checker
80
+
81
+ assert HttpMethod.GET == "GET"
82
+ assert list(HttpMethod) == ["GET", "POST", "DELETE"]
83
+ assert "GET" in HttpMethod
84
+ print(HttpMethod.keys())
85
+ print(HttpMethod.values())
86
+ print(HttpMethod.mapping)
87
+ ```
88
+
89
+ ## Optimistic Future Usage
90
+ ```python
91
+ from typing import LiteralEnum # NOT valid right now
92
+
67
93
  class HttpMethod(LiteralEnum):
68
94
  GET = "GET"
69
95
  POST = "POST"
@@ -72,8 +98,8 @@ class HttpMethod(LiteralEnum):
72
98
  def handle(method: HttpMethod) -> None:
73
99
  print(f"{method=}")
74
100
 
75
- handle("GET") # should type-check
76
- handle(HttpMethod.GET) # should type-check
101
+ handle("GET") # the GOAL is that this should type-check ✅ , in reality: it will not unless typecheckers change
102
+ handle(HttpMethod.GET) # the GOAL is that this should type-check ✅ , in reality: it will not unless typecheckers change
77
103
  handle("git") # ❌ should be rejected by a type checker
78
104
 
79
105
  assert HttpMethod.GET == "GET"
@@ -46,10 +46,36 @@ This repo is currently set up as a package under `src/`.
46
46
  #source .venv/bin/activate
47
47
  pip install literalenum
48
48
  ```
49
-
49
+ ## Realistic Current Usage
50
50
  ```python
51
+ from typing import Literal
51
52
  from literalenum import LiteralEnum
52
53
 
54
+ HttpMethodT = Literal["GET", "POST", "DELETE"]
55
+ class HttpMethod(LiteralEnum):
56
+ GET = "GET"
57
+ POST = "POST"
58
+ DELETE = "DELETE"
59
+
60
+ def handle(method: HttpMethodT) -> None:
61
+ print(f"{method=}")
62
+
63
+ handle("GET") # this should type-check ✅
64
+ handle(HttpMethod.GET) # this should type-check ✅
65
+ handle("git") # ❌ should be rejected by a type checker
66
+
67
+ assert HttpMethod.GET == "GET"
68
+ assert list(HttpMethod) == ["GET", "POST", "DELETE"]
69
+ assert "GET" in HttpMethod
70
+ print(HttpMethod.keys())
71
+ print(HttpMethod.values())
72
+ print(HttpMethod.mapping)
73
+ ```
74
+
75
+ ## Optimistic Future Usage
76
+ ```python
77
+ from typing import LiteralEnum # NOT valid right now
78
+
53
79
  class HttpMethod(LiteralEnum):
54
80
  GET = "GET"
55
81
  POST = "POST"
@@ -58,8 +84,8 @@ class HttpMethod(LiteralEnum):
58
84
  def handle(method: HttpMethod) -> None:
59
85
  print(f"{method=}")
60
86
 
61
- handle("GET") # should type-check
62
- handle(HttpMethod.GET) # should type-check
87
+ handle("GET") # the GOAL is that this should type-check ✅ , in reality: it will not unless typecheckers change
88
+ handle(HttpMethod.GET) # the GOAL is that this should type-check ✅ , in reality: it will not unless typecheckers change
63
89
  handle("git") # ❌ should be rejected by a type checker
64
90
 
65
91
  assert HttpMethod.GET == "GET"
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "literalenum"
8
- version = "0.1.1"
8
+ version = "0.3.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")
150
152
 
151
- out.append(f" values: ClassVar[Iterable[{alias}]]\n")
152
- out.append(f" mapping: ClassVar[dict[str, {alias}]]\n\n")
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")
153
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")
165
+
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")
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
 
@@ -384,9 +424,50 @@ def _parse_out_args(raw: list[str] | None) -> list[Path]:
384
424
  return uniq
385
425
 
386
426
 
427
+ def _resolve_root(raw: str) -> str:
428
+ """Resolve *raw* to a dotted module name importable by Python.
429
+
430
+ Accepts:
431
+ - A dotted import path: ``myapp.models``
432
+ - A file path: ``src/myapp/models.py``
433
+ - A directory path: ``src/myapp/``
434
+
435
+ When a file or directory is given its parent is added to ``sys.path``
436
+ so that ``importlib`` can find it.
437
+ """
438
+ import sys
439
+
440
+ p = Path(raw)
441
+
442
+ # --- file path: /some/path/module.py ---
443
+ if p.is_file() and p.suffix == ".py":
444
+ parent = str(p.parent.resolve())
445
+ if parent not in sys.path:
446
+ sys.path.insert(0, parent)
447
+ return p.stem if p.name != "__init__.py" else p.parent.name
448
+
449
+ # --- directory path: /some/path/package/ ---
450
+ if p.is_dir():
451
+ parent = str(p.parent.resolve())
452
+ if parent not in sys.path:
453
+ sys.path.insert(0, parent)
454
+ return p.name
455
+
456
+ # --- dotted import name (ensure cwd is on path) ---
457
+ cwd = str(Path.cwd())
458
+ if cwd not in sys.path:
459
+ sys.path.insert(0, cwd)
460
+ return raw
461
+
462
+
387
463
  def main() -> int:
388
464
  ap = argparse.ArgumentParser()
389
- ap.add_argument("root", help="Root import (package or module) to scan, e.g. myapp")
465
+ ap.add_argument(
466
+ "root",
467
+ help="Root to scan: a dotted import (myapp), "
468
+ "a .py file (src/myapp/models.py), "
469
+ "or a directory (src/myapp/).",
470
+ )
390
471
  ap.add_argument(
391
472
  "--out",
392
473
  action="append",
@@ -402,7 +483,8 @@ def main() -> int:
402
483
 
403
484
  write_adjacent: bool = not args.no_adjacent
404
485
  out_roots: list[Path] = _parse_out_args(args.out) if args.out else []
405
- infos = _find_literal_enums(args.root)
486
+ root = _resolve_root(args.root)
487
+ infos = _find_literal_enums(root)
406
488
 
407
489
  by_module: dict[str, list[EnumInfo]] = {}
408
490
  for e in infos:
@@ -609,6 +609,62 @@ class LiteralEnumMeta(type):
609
609
  """
610
610
  return validate_is_member(cls, x)
611
611
 
612
+ # ---- Testing utilities ----
613
+
614
+ def matches_enum(cls, enum_cls: type) -> bool:
615
+ """Check whether this LiteralEnum has exactly the same values as *enum_cls*.
616
+
617
+ Compares the set of unique values in this LiteralEnum against the
618
+ ``.value`` of every member in the given ``enum.Enum`` (or subclass).
619
+ Useful in test suites to assert two parallel definitions stay in sync::
620
+
621
+ import enum
622
+
623
+ class Color(enum.StrEnum):
624
+ RED = "red"
625
+ GREEN = "green"
626
+
627
+ class ColorLE(LiteralEnum):
628
+ RED = "red"
629
+ GREEN = "green"
630
+
631
+ assert ColorLE.matches_enum(Color)
632
+
633
+ Returns:
634
+ ``True`` if the value sets are identical.
635
+ """
636
+ try:
637
+ enum_values = {m.value for m in enum_cls}
638
+ except TypeError:
639
+ return False
640
+ return set(cls._ordered_values_) == enum_values
641
+
642
+ def matches_literal(cls, literal_type: Any) -> bool:
643
+ """Check whether this LiteralEnum has exactly the same values as *literal_type*.
644
+
645
+ Extracts the arguments from a ``typing.Literal[...]`` and compares
646
+ them against this LiteralEnum's unique values. Useful in test suites
647
+ to assert a Literal type alias stays in sync with a LiteralEnum::
648
+
649
+ from typing import Literal
650
+
651
+ ColorLiteral = Literal["red", "green"]
652
+
653
+ class Color(LiteralEnum):
654
+ RED = "red"
655
+ GREEN = "green"
656
+
657
+ assert Color.matches_literal(ColorLiteral)
658
+
659
+ Returns:
660
+ ``True`` if the value sets are identical.
661
+ """
662
+ from typing import get_args
663
+ args = get_args(literal_type)
664
+ if not args:
665
+ return False
666
+ return set(cls._ordered_values_) == set(args)
667
+
612
668
 
613
669
  # ---------------------------------------------------------------------------
614
670
  # Base class
@@ -629,6 +629,103 @@ class TestAnd:
629
629
  assert list(C) == [] # True and 1 are distinct by strict key
630
630
 
631
631
 
632
+ # ===================================================================
633
+ # matches_enum / matches_literal
634
+ # ===================================================================
635
+
636
+ class TestMatchesEnum:
637
+ def test_matches_identical(self):
638
+ import enum
639
+
640
+ class E(enum.Enum):
641
+ GET = "GET"
642
+ POST = "POST"
643
+ DELETE = "DELETE"
644
+
645
+ assert HttpMethod.matches_enum(E) is True
646
+
647
+ def test_matches_strenum(self):
648
+ import enum
649
+
650
+ class E(enum.StrEnum):
651
+ GET = "GET"
652
+ POST = "POST"
653
+ DELETE = "DELETE"
654
+
655
+ assert HttpMethod.matches_enum(E) is True
656
+
657
+ def test_matches_intenum(self):
658
+ import enum
659
+
660
+ class E(enum.IntEnum):
661
+ OK = 200
662
+ NOT_FOUND = 404
663
+
664
+ assert StatusCode.matches_enum(E) is True
665
+
666
+ def test_mismatch_extra_in_enum(self):
667
+ import enum
668
+
669
+ class E(enum.Enum):
670
+ GET = "GET"
671
+ POST = "POST"
672
+ DELETE = "DELETE"
673
+ PATCH = "PATCH"
674
+
675
+ assert HttpMethod.matches_enum(E) is False
676
+
677
+ def test_mismatch_missing_in_enum(self):
678
+ import enum
679
+
680
+ class E(enum.Enum):
681
+ GET = "GET"
682
+
683
+ assert HttpMethod.matches_enum(E) is False
684
+
685
+ def test_mismatch_different_values(self):
686
+ import enum
687
+
688
+ class E(enum.Enum):
689
+ GET = "get"
690
+ POST = "post"
691
+ DELETE = "delete"
692
+
693
+ assert HttpMethod.matches_enum(E) is False
694
+
695
+ def test_non_enum_returns_false(self):
696
+ assert HttpMethod.matches_enum(str) is False
697
+
698
+
699
+ class TestMatchesLiteral:
700
+ def test_matches_identical(self):
701
+ from typing import Literal
702
+ assert HttpMethod.matches_literal(Literal["GET", "POST", "DELETE"]) is True
703
+
704
+ def test_matches_int_literal(self):
705
+ from typing import Literal
706
+ assert StatusCode.matches_literal(Literal[200, 404]) is True
707
+
708
+ def test_mismatch_extra(self):
709
+ from typing import Literal
710
+ assert HttpMethod.matches_literal(Literal["GET", "POST", "DELETE", "PATCH"]) is False
711
+
712
+ def test_mismatch_missing(self):
713
+ from typing import Literal
714
+ assert HttpMethod.matches_literal(Literal["GET"]) is False
715
+
716
+ def test_order_independent(self):
717
+ from typing import Literal
718
+ assert HttpMethod.matches_literal(Literal["DELETE", "GET", "POST"]) is True
719
+
720
+ def test_non_literal_returns_false(self):
721
+ assert HttpMethod.matches_literal(str) is False
722
+
723
+ def test_matches_type_alias(self):
724
+ from typing import Literal
725
+ MyType = Literal["GET", "POST", "DELETE"]
726
+ assert HttpMethod.matches_literal(MyType) is True
727
+
728
+
632
729
  # ===================================================================
633
730
  # Edge cases
634
731
  # ===================================================================
@@ -1,3 +0,0 @@
1
- {
2
- "stubPath": "typings"
3
- }
File without changes
File without changes
File without changes
File without changes