literalenum 0.3.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.3.0 → literalenum-0.4.0}/PKG-INFO +1 -1
- {literalenum-0.3.0 → literalenum-0.4.0}/pyproject.toml +1 -1
- {literalenum-0.3.0 → literalenum-0.4.0}/src/typing_literalenum.py +42 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/tests/test_typing_literalenum.py +30 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/.github/workflows/publish.yml +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/.gitignore +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/LICENSE +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/LITMUS.md +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/PEP.md +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/README.md +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/TYPING_DISCUSSION.md +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/__init__.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/__init__.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/annotated.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/bare_class.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/base_model.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/click_choice.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/django_choices.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/graphene_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/int_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/json_schema.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/literal.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/random_choice.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/regex.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/sqlalchemy_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/str_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/strawberry_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/literal_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/mypy_plugin.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/py.typed +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/samples/__init__.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/samples/http.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/samples/http.pyi +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/stubgen.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/__init__.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/a_strenum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/b_str_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/c_enum.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/d_literal.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/e_literal_plus_namespace.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/f_literal_hack.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/g_custom_type.py +0 -0
- {literalenum-0.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/h_custom_literal_namespace.py +0 -0
- {literalenum-0.3.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
|
|
@@ -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"
|
|
@@ -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(
|
|
@@ -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
|
|
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.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/__init__.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/annotated.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/bare_class.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/base_model.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/click_choice.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/django_choices.py
RENAMED
|
File without changes
|
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/graphene_enum.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/int_enum.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/json_schema.py
RENAMED
|
File without changes
|
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/random_choice.py
RENAMED
|
File without changes
|
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/sqlalchemy_enum.py
RENAMED
|
File without changes
|
{literalenum-0.3.0 → literalenum-0.4.0}/src/literalenum/compatibility_extensions/str_enum.py
RENAMED
|
File without changes
|
{literalenum-0.3.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
|
|
File without changes
|
{literalenum-0.3.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.3.0 → literalenum-0.4.0}/src/sample_str_enum_solutions/h_custom_literal_namespace.py
RENAMED
|
File without changes
|
|
File without changes
|