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
literalenum/stubgen.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import ast
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
import pkgutil
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
|
|
13
|
+
from literalenum import LiteralEnum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ----------------------------
|
|
17
|
+
# Discovery
|
|
18
|
+
# ----------------------------
|
|
19
|
+
|
|
20
|
+
def _iter_modules(root: str) -> Iterable[str]:
|
|
21
|
+
pkg = importlib.import_module(root)
|
|
22
|
+
if not hasattr(pkg, "__path__"):
|
|
23
|
+
yield root
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
yield root
|
|
27
|
+
for modinfo in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
|
|
28
|
+
yield modinfo.name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _module_origin_py(module: str) -> Path | None:
|
|
32
|
+
spec = importlib.util.find_spec(module)
|
|
33
|
+
if not spec or not spec.origin or spec.origin in ("built-in", "namespace"):
|
|
34
|
+
return None
|
|
35
|
+
p = Path(spec.origin)
|
|
36
|
+
return p if p.suffix == ".py" else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _module_to_adjacent_stub_path(module: str) -> Path | None:
|
|
40
|
+
origin = _module_origin_py(module)
|
|
41
|
+
if origin is None:
|
|
42
|
+
return None
|
|
43
|
+
if origin.name == "__init__.py":
|
|
44
|
+
return origin.with_name("__init__.pyi")
|
|
45
|
+
return origin.with_suffix(".pyi")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _module_to_stub_path(stub_root: Path, module: str) -> Path:
|
|
49
|
+
rel = Path(*module.split("."))
|
|
50
|
+
return stub_root / (str(rel) + ".pyi")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _ensure_parent(path: Path) -> None:
|
|
54
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _py_literal(v: Any) -> str:
|
|
58
|
+
# Emit double quotes for strings for nicer stubs.
|
|
59
|
+
if isinstance(v, str):
|
|
60
|
+
return '"' + v.replace('"', '\\"') + '"'
|
|
61
|
+
return repr(v)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class EnumInfo:
|
|
66
|
+
module: str
|
|
67
|
+
name: str
|
|
68
|
+
qualname: str
|
|
69
|
+
bases: tuple[type, ...]
|
|
70
|
+
members: dict[str, Any] # name -> value (from runtime mapping)
|
|
71
|
+
call_to_validate: bool = False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _find_literal_enums(root: str) -> list[EnumInfo]:
|
|
75
|
+
infos: list[EnumInfo] = []
|
|
76
|
+
for modname in _iter_modules(root):
|
|
77
|
+
try:
|
|
78
|
+
mod = importlib.import_module(modname)
|
|
79
|
+
except Exception:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
for _, obj in inspect.getmembers(mod, inspect.isclass):
|
|
83
|
+
if obj is LiteralEnum:
|
|
84
|
+
continue
|
|
85
|
+
if issubclass(obj, LiteralEnum) and obj.__module__ == mod.__name__:
|
|
86
|
+
members = dict(getattr(obj, "mapping"))
|
|
87
|
+
infos.append(
|
|
88
|
+
EnumInfo(
|
|
89
|
+
module=obj.__module__,
|
|
90
|
+
name=obj.__name__,
|
|
91
|
+
qualname=f"{obj.__module__}.{obj.__name__}",
|
|
92
|
+
bases=getattr(obj, "__bases__", ()),
|
|
93
|
+
members=members,
|
|
94
|
+
call_to_validate=getattr(obj, "_call_to_validate_", False),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return infos
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ----------------------------
|
|
101
|
+
# Rendering enum stubs
|
|
102
|
+
# ----------------------------
|
|
103
|
+
|
|
104
|
+
def _render_enum_blocks(enums: list[EnumInfo]) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Render ONLY enum-related stubs for a module:
|
|
107
|
+
- <EnumName>T = Literal[...]
|
|
108
|
+
- class <EnumName>(Base): ...
|
|
109
|
+
"""
|
|
110
|
+
by_qual = {e.qualname: e for e in enums}
|
|
111
|
+
|
|
112
|
+
def _qual_of(cls: type) -> str:
|
|
113
|
+
return f"{getattr(cls, '__module__', '')}.{getattr(cls, '__name__', '')}"
|
|
114
|
+
|
|
115
|
+
def _enum_base_decl(e: EnumInfo) -> str:
|
|
116
|
+
# If base is another emitted enum in the same module, preserve that inheritance.
|
|
117
|
+
for b in e.bases:
|
|
118
|
+
if _qual_of(b) in by_qual:
|
|
119
|
+
return b.__name__
|
|
120
|
+
return f"LiteralEnum[{e.name}T]"
|
|
121
|
+
|
|
122
|
+
def _inherited_member_names(e: EnumInfo) -> set[str]:
|
|
123
|
+
names: set[str] = set()
|
|
124
|
+
for b in e.bases:
|
|
125
|
+
qb = _qual_of(b)
|
|
126
|
+
if qb in by_qual:
|
|
127
|
+
names |= set(by_qual[qb].members.keys())
|
|
128
|
+
return names
|
|
129
|
+
|
|
130
|
+
out: list[str] = []
|
|
131
|
+
for e in sorted(enums, key=lambda x: x.name):
|
|
132
|
+
alias = f"{e.name}T"
|
|
133
|
+
values = list(e.members.values())
|
|
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")
|
|
136
|
+
|
|
137
|
+
base = _enum_base_decl(e)
|
|
138
|
+
inherited = _inherited_member_names(e)
|
|
139
|
+
|
|
140
|
+
# Only emit new member attributes (avoid duplicating inherited ones).
|
|
141
|
+
own_members = [(k, v) for (k, v) in e.members.items() if k not in inherited]
|
|
142
|
+
|
|
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:
|
|
148
|
+
for k, v in own_members:
|
|
149
|
+
out.append(f" {k}: Final[Literal[{_py_literal(v)}]] = {_py_literal(v)}\n")
|
|
150
|
+
|
|
151
|
+
out.append(f" values: ClassVar[Iterable[{alias}]]\n")
|
|
152
|
+
out.append(f" mapping: ClassVar[dict[str, {alias}]]\n\n")
|
|
153
|
+
|
|
154
|
+
if e.call_to_validate:
|
|
155
|
+
out.append(" @overload\n")
|
|
156
|
+
out.append(f" def __new__(cls, value: {alias}) -> {alias}: ...\n")
|
|
157
|
+
out.append(" @overload\n")
|
|
158
|
+
out.append(f" def __new__(cls, value: object) -> {alias}: ...\n\n")
|
|
159
|
+
else:
|
|
160
|
+
out.append(f" def __new__(cls, value: Never) -> NoReturn: ...\n\n")
|
|
161
|
+
|
|
162
|
+
out.append(" @classmethod\n")
|
|
163
|
+
out.append(f" def is_member(cls, value: object) -> TypeGuard[{alias}]: ...\n\n")
|
|
164
|
+
|
|
165
|
+
return "".join(out)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def stub_for(literalenum_cls: type) -> str:
|
|
169
|
+
"""Return a stub string for a single LiteralEnum class.
|
|
170
|
+
|
|
171
|
+
Takes a LiteralEnum subclass at runtime and produces the ``.pyi``
|
|
172
|
+
content for it — a ``TypeAlias``, a class with ``Final`` members,
|
|
173
|
+
and typed helper signatures::
|
|
174
|
+
|
|
175
|
+
from literalenum import LiteralEnum
|
|
176
|
+
from literalenum.stubgen import stub_for
|
|
177
|
+
|
|
178
|
+
class HttpMethod(LiteralEnum):
|
|
179
|
+
GET = "GET"
|
|
180
|
+
POST = "POST"
|
|
181
|
+
|
|
182
|
+
print(stub_for(HttpMethod))
|
|
183
|
+
|
|
184
|
+
The output is a self-contained snippet (no import header). Wrap it
|
|
185
|
+
with your own imports or use the CLI for full-module stubs.
|
|
186
|
+
"""
|
|
187
|
+
info = EnumInfo(
|
|
188
|
+
module=getattr(literalenum_cls, "__module__", ""),
|
|
189
|
+
name=literalenum_cls.__name__,
|
|
190
|
+
qualname=f"{getattr(literalenum_cls, '__module__', '')}.{literalenum_cls.__name__}",
|
|
191
|
+
bases=getattr(literalenum_cls, "__bases__", ()),
|
|
192
|
+
members=dict(getattr(literalenum_cls, "mapping")),
|
|
193
|
+
call_to_validate=getattr(literalenum_cls, "_call_to_validate_", False),
|
|
194
|
+
)
|
|
195
|
+
return _render_enum_blocks([info])
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _render_overlay_stub_module(enums: list[EnumInfo]) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Overlay stub intended for pyright stubPath:
|
|
201
|
+
only contains the enum stubs (module is "typing overlay").
|
|
202
|
+
"""
|
|
203
|
+
out: list[str] = []
|
|
204
|
+
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")
|
|
207
|
+
out.append(_render_enum_blocks(enums))
|
|
208
|
+
return "".join(out)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ----------------------------
|
|
212
|
+
# Adjacent stubs (preserve module)
|
|
213
|
+
# ----------------------------
|
|
214
|
+
|
|
215
|
+
_TYPING_INJECT = "from typing import ClassVar, Final, Literal, Iterable, Never, NoReturn, TypeGuard, TypeAlias, overload"
|
|
216
|
+
_LITERALENUM_INJECT = "from literalenum import LiteralEnum"
|
|
217
|
+
_FUTURE = "from __future__ import annotations"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _read_source(path: Path) -> str:
|
|
221
|
+
return path.read_text(encoding="utf-8")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _collect_docstring(tree: ast.Module, src: str) -> str | None:
|
|
225
|
+
if tree.body and isinstance(tree.body[0], ast.Expr) and isinstance(tree.body[0].value, ast.Constant):
|
|
226
|
+
if isinstance(tree.body[0].value.value, str):
|
|
227
|
+
seg = ast.get_source_segment(src, tree.body[0])
|
|
228
|
+
return seg.strip() if seg else None
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _collect_import_lines(tree: ast.Module, src: str) -> list[str]:
|
|
233
|
+
lines: list[str] = []
|
|
234
|
+
for stmt in tree.body:
|
|
235
|
+
if isinstance(stmt, (ast.Import, ast.ImportFrom)):
|
|
236
|
+
seg = ast.get_source_segment(src, stmt)
|
|
237
|
+
if seg:
|
|
238
|
+
lines.append(seg.strip())
|
|
239
|
+
return lines
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _normalize_imports(import_lines: list[str]) -> list[str]:
|
|
243
|
+
"""
|
|
244
|
+
Remove imports we will inject exactly once to avoid dupes.
|
|
245
|
+
"""
|
|
246
|
+
out: list[str] = []
|
|
247
|
+
for line in import_lines:
|
|
248
|
+
s = line.strip()
|
|
249
|
+
if s == _FUTURE:
|
|
250
|
+
continue
|
|
251
|
+
if s.startswith("from literalenum import") and "LiteralEnum" in s:
|
|
252
|
+
continue
|
|
253
|
+
if s.startswith("from typing import"):
|
|
254
|
+
# drop any typing line that imports our injected symbols
|
|
255
|
+
# (simple heuristic: if it mentions any of them, drop it)
|
|
256
|
+
symbols = ["ClassVar", "Final", "Literal", "Iterable", "TypeGuard", "overload"]
|
|
257
|
+
if any(sym in s for sym in symbols):
|
|
258
|
+
continue
|
|
259
|
+
out.append(s)
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _is_enum_classdef(stmt: ast.stmt, enum_names: set[str]) -> bool:
|
|
264
|
+
return isinstance(stmt, ast.ClassDef) and stmt.name in enum_names
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _is_safe_preserve_stmt(stmt: ast.stmt) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Preserve harmless top-level statements verbatim.
|
|
270
|
+
- imports handled separately
|
|
271
|
+
- docstring handled separately
|
|
272
|
+
"""
|
|
273
|
+
return isinstance(stmt, (ast.Assign, ast.AnnAssign))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _stub_skeleton(stmt: ast.stmt, src: str) -> str | None:
|
|
277
|
+
# Keep names for everything else, but as stubs.
|
|
278
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
279
|
+
header = (ast.get_source_segment(src, stmt) or f"def {stmt.name}(...):").splitlines()[0]
|
|
280
|
+
if not header.rstrip().endswith(":"):
|
|
281
|
+
header = header.rstrip() + ":"
|
|
282
|
+
return header + "\n ...\n"
|
|
283
|
+
if isinstance(stmt, ast.AsyncFunctionDef):
|
|
284
|
+
header = (ast.get_source_segment(src, stmt) or f"async def {stmt.name}(...):").splitlines()[0]
|
|
285
|
+
if not header.rstrip().endswith(":"):
|
|
286
|
+
header = header.rstrip() + ":"
|
|
287
|
+
return header + "\n ...\n"
|
|
288
|
+
if isinstance(stmt, ast.ClassDef):
|
|
289
|
+
# Non-enum class: preserve header with bases, stub body
|
|
290
|
+
bases = ""
|
|
291
|
+
if stmt.bases:
|
|
292
|
+
bases_src = ", ".join(ast.get_source_segment(src, b) or "object" for b in stmt.bases)
|
|
293
|
+
bases = f"({bases_src})"
|
|
294
|
+
return f"class {stmt.name}{bases}:\n ...\n"
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _render_adjacent_preserving_stub(module: str, enums: list[EnumInfo]) -> str:
|
|
299
|
+
origin = _module_origin_py(module)
|
|
300
|
+
if origin is None:
|
|
301
|
+
return _render_overlay_stub_module(enums)
|
|
302
|
+
|
|
303
|
+
src = _read_source(origin)
|
|
304
|
+
tree = ast.parse(src, filename=str(origin))
|
|
305
|
+
|
|
306
|
+
enum_names = {e.name for e in enums}
|
|
307
|
+
|
|
308
|
+
doc = _collect_docstring(tree, src)
|
|
309
|
+
imports = _normalize_imports(_collect_import_lines(tree, src))
|
|
310
|
+
|
|
311
|
+
out: list[str] = []
|
|
312
|
+
out.append(_FUTURE + "\n")
|
|
313
|
+
if doc:
|
|
314
|
+
out.append(doc + "\n\n")
|
|
315
|
+
|
|
316
|
+
# Original imports (minus ones we inject)
|
|
317
|
+
if imports:
|
|
318
|
+
out.extend(line + "\n" for line in imports)
|
|
319
|
+
out.append("\n")
|
|
320
|
+
|
|
321
|
+
# Inject what enum blocks need, exactly once
|
|
322
|
+
out.append(_TYPING_INJECT + "\n")
|
|
323
|
+
out.append(_LITERALENUM_INJECT + "\n\n")
|
|
324
|
+
|
|
325
|
+
# Walk original statements in order:
|
|
326
|
+
for stmt in tree.body:
|
|
327
|
+
# skip docstring/imports (already handled)
|
|
328
|
+
if stmt is tree.body[0] and doc:
|
|
329
|
+
continue
|
|
330
|
+
if isinstance(stmt, (ast.Import, ast.ImportFrom)):
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
# Replace enum classdefs with generated blocks later; skip here.
|
|
334
|
+
if _is_enum_classdef(stmt, enum_names):
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# Preserve simple assignments verbatim
|
|
338
|
+
if _is_safe_preserve_stmt(stmt):
|
|
339
|
+
seg = ast.get_source_segment(src, stmt)
|
|
340
|
+
if seg:
|
|
341
|
+
out.append(seg.strip() + "\n\n")
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
# For everything else, emit skeleton stubs so names remain
|
|
345
|
+
sk = _stub_skeleton(stmt, src)
|
|
346
|
+
if sk:
|
|
347
|
+
out.append(sk + "\n")
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Drop other statements (loops, runtime code, etc.)—not valid/meaningful in stubs.
|
|
351
|
+
|
|
352
|
+
# Now append enum stubs (aliases + class stubs)
|
|
353
|
+
out.append(_render_enum_blocks(enums))
|
|
354
|
+
|
|
355
|
+
return "".join(out)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ----------------------------
|
|
359
|
+
# CLI
|
|
360
|
+
# ----------------------------
|
|
361
|
+
|
|
362
|
+
def _parse_out_args(raw: list[str] | None) -> list[Path]:
|
|
363
|
+
"""
|
|
364
|
+
Supports:
|
|
365
|
+
--out typings
|
|
366
|
+
--out typings --out stubs
|
|
367
|
+
--out "typings,stubs"
|
|
368
|
+
--out "typings;stubs"
|
|
369
|
+
"""
|
|
370
|
+
if not raw:
|
|
371
|
+
return [Path("typings")]
|
|
372
|
+
out: list[Path] = []
|
|
373
|
+
for item in raw:
|
|
374
|
+
parts = [p.strip() for p in item.replace(";", ",").split(",") if p.strip()]
|
|
375
|
+
out.extend(Path(p) for p in parts)
|
|
376
|
+
|
|
377
|
+
# de-dupe while preserving order
|
|
378
|
+
seen: set[Path] = set()
|
|
379
|
+
uniq: list[Path] = []
|
|
380
|
+
for p in out:
|
|
381
|
+
if p not in seen:
|
|
382
|
+
seen.add(p)
|
|
383
|
+
uniq.append(p)
|
|
384
|
+
return uniq
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main() -> int:
|
|
388
|
+
ap = argparse.ArgumentParser()
|
|
389
|
+
ap.add_argument("root", help="Root import (package or module) to scan, e.g. myapp")
|
|
390
|
+
ap.add_argument(
|
|
391
|
+
"--out",
|
|
392
|
+
action="append",
|
|
393
|
+
help="Also write overlay stubs to this directory (e.g. typings). "
|
|
394
|
+
"Repeatable or comma/semicolon-separated.",
|
|
395
|
+
)
|
|
396
|
+
ap.add_argument(
|
|
397
|
+
"--no-adjacent",
|
|
398
|
+
action="store_true",
|
|
399
|
+
help="Skip writing module.pyi next to module.py.",
|
|
400
|
+
)
|
|
401
|
+
args = ap.parse_args()
|
|
402
|
+
|
|
403
|
+
write_adjacent: bool = not args.no_adjacent
|
|
404
|
+
out_roots: list[Path] = _parse_out_args(args.out) if args.out else []
|
|
405
|
+
infos = _find_literal_enums(args.root)
|
|
406
|
+
|
|
407
|
+
by_module: dict[str, list[EnumInfo]] = {}
|
|
408
|
+
for e in infos:
|
|
409
|
+
by_module.setdefault(e.module, []).append(e)
|
|
410
|
+
|
|
411
|
+
written = 0
|
|
412
|
+
for module, enums in by_module.items():
|
|
413
|
+
# Adjacent stubs (module.pyi next to module.py) — default
|
|
414
|
+
if write_adjacent:
|
|
415
|
+
adjacent_text = _render_adjacent_preserving_stub(module, enums)
|
|
416
|
+
adj_path = _module_to_adjacent_stub_path(module)
|
|
417
|
+
if adj_path is not None:
|
|
418
|
+
_ensure_parent(adj_path)
|
|
419
|
+
adj_path.write_text(adjacent_text, encoding="utf-8")
|
|
420
|
+
written += 1
|
|
421
|
+
|
|
422
|
+
# Overlay stubs (e.g. typings/) — only when --out is given
|
|
423
|
+
if out_roots:
|
|
424
|
+
overlay_text = _render_overlay_stub_module(enums)
|
|
425
|
+
for stub_root in out_roots:
|
|
426
|
+
out_path = _module_to_stub_path(stub_root, module)
|
|
427
|
+
_ensure_parent(out_path)
|
|
428
|
+
out_path.write_text(overlay_text, encoding="utf-8")
|
|
429
|
+
written += 1
|
|
430
|
+
|
|
431
|
+
print(f"Wrote {written} stub file(s) for {len(infos)} LiteralEnum subclasses.")
|
|
432
|
+
return 0
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
if __name__ == "__main__":
|
|
438
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: literalenum
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A Python typing construct that provides namespaced literal constants with advanced typing features.
|
|
5
|
+
Project-URL: Homepage, https://github.com/modularizer/literalenum
|
|
6
|
+
Project-URL: Repository, https://github.com/modularizer/literalenum
|
|
7
|
+
Project-URL: Issues, https://github.com/modularizer/literalenum/issues
|
|
8
|
+
Author-email: Torin Halsted <modularizer@gmail.com>
|
|
9
|
+
License: Unlicense
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: python,typehinting
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# LiteralEnum
|
|
16
|
+
|
|
17
|
+
**LiteralEnum** is an experiment/prototype for a proposed Python typing construct:
|
|
18
|
+
a *finite, named set of runtime literals* (usually strings) that type checkers can treat as
|
|
19
|
+
an **exhaustive `Literal[...]` union**.
|
|
20
|
+
|
|
21
|
+
> Status: Prototype / exploration for typing-sig discussion. Not an accepted PEP.
|
|
22
|
+
|
|
23
|
+
## Why this exists
|
|
24
|
+
In typed Python today you often have to pick one:
|
|
25
|
+
|
|
26
|
+
- `Enum` / `StrEnum`: great runtime namespace, but APIs want callers to pass enum members instead of raw strings
|
|
27
|
+
- `Literal[...]`: great static checking, but no runtime namespace/iteration/validation
|
|
28
|
+
|
|
29
|
+
So people duplicate values or accept `str` and validate at runtime.
|
|
30
|
+
|
|
31
|
+
LiteralEnum aims to make the common case a single source of truth.
|
|
32
|
+
|
|
33
|
+
It’s designed for “protocol token” style values—HTTP methods, event names, command identifiers, config keys—where you want:
|
|
34
|
+
- **plain literals at runtime** (e.g. `"GET"`),
|
|
35
|
+
- **namespaced constants** (e.g. `HttpMethod.GET`), and
|
|
36
|
+
- **static exhaustiveness checking** (i.e. the type is equivalent to `Literal["GET", "POST", ...]`).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
## Table of Contents
|
|
40
|
+
- [Typing discussion](https://discuss.python.org/t/proposal-literalenum-runtime-literals-with-static-exhaustiveness/106000) with the Python community
|
|
41
|
+
- [PEP.md](/PEP.md) is a draft PEP
|
|
42
|
+
- [LITMUS.md](/LITMUS.md) describes the project goals
|
|
43
|
+
- [TYPING_DISCUSSION.md](/TYPING_DISCUSSION.md) is shows drafts from the discussion
|
|
44
|
+
- [src/typing_literalenum.py](/src/typing_literalenum.py) is a the draft of the core runtime functionality (proposed to become `typing.LiteralEnum` or `typing_extensions.LiteralEnum`)
|
|
45
|
+
- [src/literalenum](/src/literalenum) is the full proposed PyPi module
|
|
46
|
+
- [src/literalenum/mypy_plugin.py](/src/literalenum/mypy_plugin.py) is an experimental **mypy plugin**
|
|
47
|
+
- [src/literalenum/samples](/src/literalenum/samples) shows sample usage
|
|
48
|
+
- [src/literalenum/stubgen.py](/src/literalenum/stubgen.py) provides tools for generating stubs, usable through CLI tool `lestub`
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
This repo is currently set up as a package under `src/`.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
#python -m venv .venv
|
|
60
|
+
#source .venv/bin/activate
|
|
61
|
+
pip install literalenum
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from literalenum import LiteralEnum
|
|
66
|
+
|
|
67
|
+
class HttpMethod(LiteralEnum):
|
|
68
|
+
GET = "GET"
|
|
69
|
+
POST = "POST"
|
|
70
|
+
DELETE = "DELETE"
|
|
71
|
+
|
|
72
|
+
def handle(method: HttpMethod) -> None:
|
|
73
|
+
print(f"{method=}")
|
|
74
|
+
|
|
75
|
+
handle("GET") # ✅ should type-check
|
|
76
|
+
handle(HttpMethod.GET) # ✅ should type-check
|
|
77
|
+
handle("git") # ❌ should be rejected by a type checker
|
|
78
|
+
|
|
79
|
+
assert HttpMethod.GET == "GET"
|
|
80
|
+
assert list(HttpMethod) == ["GET", "POST", "DELETE"]
|
|
81
|
+
assert "GET" in HttpMethod
|
|
82
|
+
print(HttpMethod.keys())
|
|
83
|
+
print(HttpMethod.values())
|
|
84
|
+
print(HttpMethod.mapping)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Contributing / discussion
|
|
90
|
+
|
|
91
|
+
Actively looking for feedback!
|
|
92
|
+
Please comment at https://discuss.python.org/t/proposal-literalenum-runtime-literals-with-static-exhaustiveness/106000
|
|
93
|
+
|
|
94
|
+
It would be especially helpful if you are familiar with mypy/pright/pylance and have suggestions on how
|
|
95
|
+
a future Python version could support the type hinting goals.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
This project is released into the **public domain** under **The Unlicense**.
|
|
102
|
+
|
|
103
|
+
You are free to copy, modify, publish, use, compile, sell, or distribute this software,
|
|
104
|
+
either in source code form or as a compiled binary, for any purpose, commercial or non-commercial,
|
|
105
|
+
and by any means.
|
|
106
|
+
|
|
107
|
+
See the `LICENSE` file for full details.
|
|
108
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
literalenum/__init__.py,sha256=Cle3alhK2TPqkjKZ6yEFB3_2yv-y0EwtdYUnbkRG_-s,477
|
|
2
|
+
literalenum/literal_enum.py,sha256=OfB73GCHeZ5G1cAz3EA99zzZsmQ6jtL3xx3AKK-XP-0,3907
|
|
3
|
+
literalenum/mypy_plugin.py,sha256=JExHoi8NP_QJI0eESRed3TdR3DxEVrj7qA6q-bhj3KU,11020
|
|
4
|
+
literalenum/stubgen.py,sha256=U-Ch7-KyAoJXMmGFW5sLRNNszYnMd0T_3mjXHtAOWEo,14456
|
|
5
|
+
literalenum/compatibility_extensions/__init__.py,sha256=nIY32idsLpo9VyXtDREWMzLAfYUU7K7Zm3Cl69mWR0k,551
|
|
6
|
+
literalenum/compatibility_extensions/annotated.py,sha256=68TxWaCzMdcuFgnyWfWHUtzUClP5r99BMydSufdgcRA,146
|
|
7
|
+
literalenum/compatibility_extensions/bare_class.py,sha256=LXhT5BfIBJmfA8ZQmHfAVyF4IpM9911oUcj4xtonLFM,75
|
|
8
|
+
literalenum/compatibility_extensions/base_model.py,sha256=3-QWUnGLhL9R5-bEW8xpsRMaTo3tdLE7Lqmwnm8xmMg,667
|
|
9
|
+
literalenum/compatibility_extensions/click_choice.py,sha256=oaceRBUDXeOOpRkil-9R2pfBQCF7F5BN_G_x-s4VNeY,74
|
|
10
|
+
literalenum/compatibility_extensions/django_choices.py,sha256=hZVrZLhbmpHL5g12xlzKBbErQXLZjwOImfwCrumCrCQ,75
|
|
11
|
+
literalenum/compatibility_extensions/enum.py,sha256=VvWsWab8gjNKsNCGoBFr-Mj_fSWCY7ZHjUqwbZ1WGro,154
|
|
12
|
+
literalenum/compatibility_extensions/graphene_enum.py,sha256=cwovO1C82YxXXk1kRacZKGJKj44loYSFa_1jKgUiRY4,522
|
|
13
|
+
literalenum/compatibility_extensions/int_enum.py,sha256=JNs3BYZdnLxkcR7JaqSGwtr-01Wqfec8b6XUd2-JuSg,326
|
|
14
|
+
literalenum/compatibility_extensions/json_schema.py,sha256=hnIk8ACNXO3jqw1qu0RaVZPXvUGIBFTjwA4ish4tc3g,5197
|
|
15
|
+
literalenum/compatibility_extensions/literal.py,sha256=3R25lT4mlUzRdICgjjGyTdQy87PdbiwdWEP9sKUUPOc,223
|
|
16
|
+
literalenum/compatibility_extensions/random_choice.py,sha256=49-oSH6ebQJv0GIcYMDq_N9piI2kRG0RCY9bvWt7Q_s,88
|
|
17
|
+
literalenum/compatibility_extensions/regex.py,sha256=4XDtC8VlehMlcZkcE5L9nXbKkR4w2MXk411Q0OdfCyw,441
|
|
18
|
+
literalenum/compatibility_extensions/sqlalchemy_enum.py,sha256=DF_ce3Q7YNp98mTFXPsyWagQVzTTaczI-dRklOPh_LY,237
|
|
19
|
+
literalenum/compatibility_extensions/str_enum.py,sha256=y-VWFyaEDXmNicgyqzdEpsx-ImUdnydbF_YWhzvEVq0,329
|
|
20
|
+
literalenum/compatibility_extensions/strawberry_enum.py,sha256=mjg3Pyy0w_AggRRB_UtuWQiOguiG66eTN5E_rpwd7cg,526
|
|
21
|
+
typing_literalenum.py,sha256=ZoM0rbr-7Hp72uqohJ6enm-eIhUwAHTGBsb-FsQmXJs,24837
|
|
22
|
+
literalenum/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
literalenum-0.1.1.dist-info/METADATA,sha256=ZO4NgWWrqRpFZgexLjkyMX5wBVYmn2UMfS5KSdfXuiA,3957
|
|
24
|
+
literalenum-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
25
|
+
literalenum-0.1.1.dist-info/entry_points.txt,sha256=AK8je10LoLMOXuWLjXSqHV2rsTxPW9HlG7YTJGfwkCY,46
|
|
26
|
+
literalenum-0.1.1.dist-info/licenses/LICENSE,sha256=tQZYOMusRS38hVum5uAxSBrSxoQG9w0h6tkyE3RlPmw,1212
|
|
27
|
+
literalenum-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org/>
|