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.
- literalenum-0.4.0/CONTRIBUTING.md +129 -0
- literalenum-0.4.0/PITCH.md +32 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/PKG-INFO +1 -3
- {literalenum-0.2.0 → literalenum-0.4.0}/README.md +0 -2
- {literalenum-0.2.0 → literalenum-0.4.0}/pyproject.toml +1 -1
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/stubgen.py +58 -18
- {literalenum-0.2.0 → literalenum-0.4.0}/src/typing_literalenum.py +98 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/tests/test_typing_literalenum.py +127 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/.github/workflows/publish.yml +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/.gitignore +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/LICENSE +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/LITMUS.md +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/PEP.md +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/TYPING_DISCUSSION.md +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/__init__.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/__init__.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/annotated.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/bare_class.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/base_model.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/click_choice.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/django_choices.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/graphene_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/int_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/json_schema.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/literal.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/random_choice.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/regex.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/sqlalchemy_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/str_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/strawberry_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/literal_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/mypy_plugin.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/py.typed +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/samples/__init__.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/samples/http.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/samples/http.pyi +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/__init__.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/a_strenum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/b_str_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/c_enum.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/d_literal.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/e_literal_plus_namespace.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/f_literal_hack.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/g_custom_type.py +0 -0
- {literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/h_custom_literal_namespace.py +0 -0
- {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.
|
|
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:
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "literalenum"
|
|
8
|
-
version = "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
|
-
|
|
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"{
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
-
out.append(
|
|
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: {
|
|
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) -> {
|
|
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
|
|
206
|
-
out.append("from
|
|
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
|
-
|
|
216
|
-
|
|
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", "
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/__init__.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/annotated.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/bare_class.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/base_model.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/click_choice.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/django_choices.py
RENAMED
|
File without changes
|
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/graphene_enum.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/int_enum.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/json_schema.py
RENAMED
|
File without changes
|
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/random_choice.py
RENAMED
|
File without changes
|
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/sqlalchemy_enum.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/str_enum.py
RENAMED
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/strawberry_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/e_literal_plus_namespace.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{literalenum-0.2.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/h_custom_literal_namespace.py
RENAMED
|
File without changes
|
|
File without changes
|