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.
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from .literal_enum import LiteralEnum, LiteralEnumMeta
4
+ from .stubgen import main as lestub
5
+
6
+ import typing_literalenum as core
7
+
8
+ __all__ = [
9
+ "LiteralEnum", "LiteralEnumMeta",
10
+ "plugin",
11
+ "compatibility_extensions",
12
+ "lestub",
13
+ "core"
14
+ ]
15
+
16
+
17
+ def __getattr__(name: str):
18
+ if name == "plugin":
19
+ from .mypy_plugin import plugin
20
+ return plugin
21
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,15 @@
1
+ from .annotated import annotated
2
+ from .base_model import base_model
3
+ from .enum import enum
4
+ from .graphene_enum import graphene_enum
5
+ from .int_enum import int_enum
6
+ from .json_schema import json_schema
7
+ from .literal import literal
8
+ from .regex import regex_str, regex_pattern
9
+ from .sqlalchemy_enum import sqlalchemy_enum
10
+ from .str_enum import str_enum
11
+ from .strawberry_enum import strawberry_enum
12
+ from .bare_class import bare_class
13
+ from .click_choice import click_choice
14
+ from .django_choices import django_choices
15
+ from .random_choice import random_choice
@@ -0,0 +1,6 @@
1
+ from typing import Any
2
+
3
+
4
+ def annotated(cls, *metadata: Any):
5
+ from typing import Annotated
6
+ return Annotated[cls.runtime_literal, *metadata]
@@ -0,0 +1,2 @@
1
+ def bare_class(cls):
2
+ return type(cls.__name__, (), dict(cls._members_))
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+
6
+ def base_model(
7
+ enum_cls: type,
8
+ *,
9
+ model_name: str | None = None,
10
+ field_name: str = "value",
11
+ description: str | None = None,
12
+ ) -> type["BaseModel"]:
13
+ """
14
+ Create a pydantic BaseModel with a single field that validates against enum_cls.literal.
15
+ """
16
+ from pydantic import BaseModel, create_model, Field
17
+ ann = Literal[*enum_cls.values()] # <-- Literal["GET","POST",...]
18
+ default = Field(..., description=description) if description else ...
19
+
20
+ return create_model(
21
+ model_name or f"{enum_cls.__name__}Model",
22
+ **{field_name: (ann, default)},
23
+ )
@@ -0,0 +1,3 @@
1
+ def click_choice(cls):
2
+ import click
3
+ return click.Choice(list(cls))
@@ -0,0 +1,2 @@
1
+ def django_choices(cls):
2
+ return [(v, name) for name, v in cls.items()]
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ import typing_literalenum as core
4
+
5
+
6
+ def enum(cls: core.LiteralEnumMeta) -> Enum:
7
+ return Enum(cls.__name__, dict(cls._members_))
@@ -0,0 +1,18 @@
1
+ def graphene_enum(cls) -> type:
2
+ """Convert this LiteralEnum to a Graphene GraphQL enum type.
3
+
4
+ Requires ``graphene`` to be installed. Uses canonical members
5
+ only (aliases are excluded)::
6
+
7
+ class Color(LiteralEnum):
8
+ RED = "red"
9
+ GREEN = "green"
10
+
11
+ # use in a Graphene schema
12
+ ColorEnum = Color.graphene_enum
13
+ """
14
+ import enum as _enum
15
+ import graphene
16
+
17
+ PyEnum = _enum.Enum(cls.__name__, dict(cls.unique_mapping))
18
+ return graphene.Enum.from_enum(PyEnum)
@@ -0,0 +1,9 @@
1
+ from enum import IntEnum
2
+
3
+ import typing_literalenum as core
4
+
5
+
6
+ def int_enum(cls: core.LiteralEnumMeta) -> IntEnum:
7
+ if not all(isinstance(v, int) for v in cls._ordered_values_ if v is not None):
8
+ raise TypeError("int_enum only works on a int-valued LiteralEnum")
9
+ return IntEnum(cls.__name__, dict(cls._members_))
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ JsonSchema = Dict[str, Any]
6
+
7
+
8
+
9
+ def json_schema(
10
+ enum_cls: type,
11
+ *,
12
+ title: str | None = None,
13
+ description: str | None = None,
14
+ nullable: bool | None = None,
15
+ openapi: bool = False,
16
+ ) -> JsonSchema:
17
+ """
18
+ Build a JSON Schema (and OpenAPI-friendly) schema for a LiteralEnumMeta class.
19
+
20
+ Expects `enum_cls` to have:
21
+ - _ordered_values_: list/tuple of literal values in order
22
+ - _members_: dict[str, value]
23
+
24
+ Supports literals: str, int, bool, None, bytes.
25
+ For mixed types, emits a union schema (oneOf / anyOf).
26
+
27
+ Params:
28
+ - nullable:
29
+ * None (default): inferred from presence of None in values
30
+ * True/False: force nullable behavior
31
+ - openapi:
32
+ * If True: emits OpenAPI 3.0-friendly shape (uses nullable: true)
33
+ * If False: emits JSON Schema 2020-12-friendly shape (uses type: "null" or oneOf)
34
+ """
35
+ values: List[Any] = list(getattr(enum_cls, "_ordered_values_", ()))
36
+ if not values:
37
+ raise ValueError(f"{enum_cls!r} has no _ordered_values_")
38
+
39
+ # JSON Schema can't represent bytes directly; common convention is base64 string.
40
+ def _json_type(v: Any) -> str:
41
+ if v is None:
42
+ return "null"
43
+ if isinstance(v, bool):
44
+ return "boolean"
45
+ if isinstance(v, int) and not isinstance(v, bool):
46
+ return "integer"
47
+ if isinstance(v, str):
48
+ return "string"
49
+ if isinstance(v, (bytes, bytearray, memoryview)):
50
+ return "string"
51
+ # float literal support not listed in your metaclass, but easy to add if you want:
52
+ if isinstance(v, float):
53
+ return "number"
54
+ raise TypeError(f"Unsupported LiteralEnum value {v!r} (type {type(v).__name__})")
55
+
56
+ # Normalize bytes -> base64-ish string representation? (You can swap this.)
57
+ def _normalize(v: Any) -> Any:
58
+ if isinstance(v, (bytes, bytearray, memoryview)):
59
+ # JSON can't carry bytes. Convention: base64 string.
60
+ # If you prefer "binary" format in OpenAPI, keep as string and add format.
61
+ return bytes(v).decode("base64", errors="strict")
62
+ return v
63
+
64
+ normalized_values = [_normalize(v) for v in values]
65
+ types = [_json_type(v) for v in values]
66
+ unique_types = sorted(set(types))
67
+
68
+ has_null = "null" in unique_types
69
+ inferred_nullable = has_null
70
+ if nullable is None:
71
+ nullable = inferred_nullable
72
+
73
+ # Remove nulls from enum list if we represent null via nullable/type: null separately.
74
+ enum_no_null = [v for v, t in zip(normalized_values, types) if t != "null"]
75
+
76
+ schema: JsonSchema = {}
77
+ schema["title"] = title or getattr(enum_cls, "__name__", "LiteralEnum")
78
+ if description:
79
+ schema["description"] = description
80
+
81
+ # If only one non-null type, simplest representation.
82
+ non_null_types = [t for t in unique_types if t != "null"]
83
+
84
+ def _apply_nullable_oas(s: JsonSchema) -> JsonSchema:
85
+ if nullable:
86
+ s["nullable"] = True
87
+ return s
88
+
89
+ # Helper: JSON Schema style nullability (2020-12-ish)
90
+ def _apply_nullable_jsonschema(s: JsonSchema) -> JsonSchema:
91
+ if not nullable:
92
+ return s
93
+ # If already unioned, add {"type": "null"} via oneOf
94
+ if "oneOf" in s:
95
+ s["oneOf"].append({"type": "null"})
96
+ return s
97
+ # If type is a single string, allow null via type array
98
+ t = s.get("type")
99
+ if isinstance(t, str):
100
+ s["type"] = [t, "null"]
101
+ elif isinstance(t, list) and "null" not in t:
102
+ t.append("null")
103
+ else:
104
+ # Fallback: union with null
105
+ s = {"oneOf": [s, {"type": "null"}], **{k: v for k, v in s.items() if k not in ("type", "enum")}}
106
+ return s
107
+
108
+ # Build
109
+ if len(non_null_types) == 1:
110
+ t = non_null_types[0]
111
+ schema["type"] = t
112
+ if enum_no_null:
113
+ schema["enum"] = enum_no_null
114
+
115
+ # bytes convention: if any bytes present, add format hint
116
+ if any(isinstance(v, (bytes, bytearray, memoryview)) for v in values):
117
+ schema.setdefault("format", "byte") # OpenAPI convention for base64
118
+
119
+ if openapi:
120
+ schema = _apply_nullable_oas(schema)
121
+ else:
122
+ schema = _apply_nullable_jsonschema(schema)
123
+
124
+ return schema
125
+
126
+ # Mixed types: use union
127
+ # In OpenAPI 3.0, "oneOf" is supported; "anyOf" also works but oneOf is clearer.
128
+ alts: List[JsonSchema] = []
129
+ for t in sorted(set(non_null_types)):
130
+ vals_for_t = [v for v, tt in zip(normalized_values, types) if tt == t]
131
+ alt: JsonSchema = {"type": t, "enum": vals_for_t}
132
+ if t == "string" and any(isinstance(v, (bytes, bytearray, memoryview)) for v in values):
133
+ # Only add if those strings are actually from bytes;
134
+ # leaving this hint in case you mix bytes + str.
135
+ alt.setdefault("format", "byte")
136
+ alts.append(alt)
137
+
138
+ schema["oneOf"] = alts
139
+
140
+ if openapi:
141
+ schema = _apply_nullable_oas(schema)
142
+ else:
143
+ schema = _apply_nullable_jsonschema(schema)
144
+
145
+ return schema
@@ -0,0 +1,9 @@
1
+ from typing import Literal, TypeVar, Any
2
+
3
+ import typing_literalenum as core
4
+
5
+ def literal(cls: core.LiteralEnumMeta) -> TypeVar:
6
+ try:
7
+ return Literal[*cls._ordered_values_]
8
+ except TypeError:
9
+ return Any
@@ -0,0 +1,3 @@
1
+ def random_choice(cls):
2
+ import random
3
+ return random.choice(cls._ordered_values_)
@@ -0,0 +1,10 @@
1
+ def regex_str(cls) -> str:
2
+ if not all(isinstance(v, str) for v in cls._ordered_values_ if v is not None):
3
+ raise TypeError("regex is only valid for string-valued LiteralEnum")
4
+ import re
5
+ vals = [v for v in cls._ordered_values_ if isinstance(v, str)]
6
+ return "^(?:" + "|".join(re.escape(v) for v in vals) + ")$"
7
+
8
+ def regex_pattern(cls, flags=0) -> "re.Pattern":
9
+ import re
10
+ return re.compile(cls.regex_str(), flags)
@@ -0,0 +1,6 @@
1
+ def sqlalchemy_enum(cls):
2
+ try:
3
+ from sqlalchemy import Enum
4
+ except ImportError as e:
5
+ raise RuntimeError("Install sqlalchemy to use .sqlalchemy_enum") from e
6
+ return Enum(*cls._ordered_values_, name=cls.__name__)
@@ -0,0 +1,9 @@
1
+ from enum import StrEnum
2
+
3
+ import typing_literalenum as core
4
+
5
+
6
+ def str_enum(cls: core.LiteralEnumMeta) -> StrEnum:
7
+ if not all(isinstance(v, str) for v in cls._ordered_values_ if v is not None):
8
+ raise TypeError("str_enum only works on a string-valued LiteralEnum")
9
+ return StrEnum(cls.__name__, dict(cls._members_))
@@ -0,0 +1,18 @@
1
+ def strawberry_enum(cls) -> type:
2
+ """Convert this LiteralEnum to a Strawberry GraphQL enum type.
3
+
4
+ Requires ``strawberry`` to be installed. Uses canonical members
5
+ only (aliases are excluded)::
6
+
7
+ class Color(LiteralEnum):
8
+ RED = "red"
9
+ GREEN = "green"
10
+
11
+ # use in a Strawberry schema
12
+ ColorEnum = Color.strawberry_enum
13
+ """
14
+ import enum as _enum
15
+ import strawberry
16
+
17
+ PyEnum = _enum.Enum(cls.__name__, dict(cls.unique_mapping))
18
+ return strawberry.enum(PyEnum)
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Never, NoReturn
4
+
5
+ import typing_literalenum as core
6
+ from literalenum import compatibility_extensions as compat
7
+
8
+
9
+ class LiteralEnumMeta(core.LiteralEnumMeta):
10
+ def literal(cls):
11
+ return compat.literal(cls)
12
+
13
+ def enum(cls):
14
+ return compat.enum(cls)
15
+
16
+ def str_enum(cls):
17
+ return compat.str_enum(cls)
18
+
19
+ def int_enum(cls):
20
+ return compat.int_enum(cls)
21
+
22
+ def json_schema(cls):
23
+ return compat.json_schema(cls)
24
+
25
+ def base_model(cls):
26
+ return compat.base_model(cls)
27
+
28
+ def sqlalchemy_enum(cls):
29
+ return compat.sqlalchemy_enum(cls)
30
+
31
+ def strawberry_enum(cls):
32
+ return compat.strawberry_enum(cls)
33
+
34
+ def graphene_enum(cls):
35
+ return compat.graphene_enum(cls)
36
+
37
+ def regex_str(cls):
38
+ return compat.regex_str(cls)
39
+
40
+ def regex_pattern(cls, flags=0):
41
+ return compat.regex_pattern(cls, flags)
42
+
43
+ def annotated(cls):
44
+ return compat.annotated(cls)
45
+
46
+ def django_choices(cls):
47
+ return compat.django_choices(cls)
48
+
49
+ def click_choice(cls):
50
+ return compat.click_choice(cls)
51
+
52
+ def random_choice(cls):
53
+ return compat.random_choice(cls)
54
+
55
+ def bare_class(cls):
56
+ return compat.bare_class(cls)
57
+
58
+ def set(cls):
59
+ return set(cls)
60
+
61
+ def list(cls):
62
+ return list(cls)
63
+
64
+ def frozenset(cls):
65
+ return frozenset(cls)
66
+
67
+ def dict(cls):
68
+ return dict(cls.mapping)
69
+
70
+ def tuple(cls):
71
+ return tuple(cls)
72
+
73
+ def str(cls):
74
+ return "|".join(f'"{v}"' if isinstance(v, str) else repr(v) for v in cls)
75
+
76
+ def stub(cls):
77
+ from literalenum.stubgen import stub_for
78
+ return stub_for(cls)
79
+
80
+ @property
81
+ def T_(cls):
82
+ return cls.literal()
83
+
84
+ class LiteralEnum(metaclass=LiteralEnumMeta):
85
+ """Base class for defining a set of named literal values.
86
+
87
+ Subclass ``LiteralEnum`` and assign literal values as class attributes::
88
+
89
+ class Color(LiteralEnum):
90
+ RED = "red"
91
+ GREEN = "green"
92
+ BLUE = "blue"
93
+
94
+ At runtime:
95
+
96
+ * ``Color.RED`` evaluates to ``"red"`` (a plain ``str``).
97
+ * ``list(Color)`` returns ``["red", "green", "blue"]``.
98
+ * ``"red" in Color`` returns ``True``.
99
+ * ``Color.validate(x)`` returns *x* if valid or raises ``ValueError``.
100
+
101
+ At type-check time, ``Color`` is intended to be equivalent to
102
+ ``Literal["red", "green", "blue"]``.
103
+
104
+ ``LiteralEnum`` is **not instantiable** — its values are plain scalars,
105
+ not wrapper objects. Use ``validate()`` or ``is_valid()`` instead.
106
+
107
+ Duplicate values are permitted; the first declared name is canonical
108
+ and later names are aliases::
109
+
110
+ class Method(LiteralEnum):
111
+ GET = "GET"
112
+ get = "GET" # alias for GET
113
+
114
+ Method.names("GET") # ("GET", "get")
115
+ Method.canonical_name("GET") # "GET"
116
+ list(Method) # ["GET"] (aliases not yielded)
117
+
118
+ To extend an existing LiteralEnum, pass ``extend=True``::
119
+
120
+ class ExtendedColor(Color, extend=True):
121
+ YELLOW = "yellow"
122
+ """
123
+
124
+ def __new__(cls, value: Never) -> NoReturn | core.LE:
125
+ """Signal to type checkers that LiteralEnum is not instantiable.
126
+
127
+ At runtime, the metaclass ``__call__`` intercepts before this is
128
+ reached — either validating (``call_to_validate=True``) or raising
129
+ ``TypeError``. This method exists solely so that type checkers
130
+ flag ``HttpMethod("GET")`` as an error by default.
131
+ """
132
+ if cls._call_to_validate_:
133
+ return core.validate_is_member(cls, value)
134
+ raise TypeError(
135
+ f"{cls.__name__} is not instantiable; "
136
+ f"use {cls.__name__}.validate(x) or x in {cls.__name__}"
137
+ )