8000 Recursively unpack `Literal` values if using PEP 695 type aliases · pydantic/pydantic@b2f4844 · GitHub
[go: up one dir, main page]

Skip to content

Commit b2f4844

Browse files
committed
Recursively unpack Literal values if using PEP 695 type aliases
1 parent d5f4bde commit b2f4844

File tree

2 files changed

+31
-4
lines changed

2 files changed

+31
-4
lines changed

pydantic/_internal/_typing_extra.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,27 @@ def is_literal(tp: Any, /) -> bool:
8585
return _is_typing_name(get_origin(tp), name='Literal')
8686

8787

88-
# TODO remove and replace with `get_args` when we drop support for Python 3.8
89-
# (see https://docs.python.org/3/whatsnew/3.9.html#id4).
9088
def literal_values(tp: Any, /) -> list[Any]:
91-
"""Return the values contained in the provided `Literal` special form."""
89+
"""Return the values contained in the provided `Literal` special form.
90+
91+
If one of the literal values is a PEP 695 type alias, recursively parse
92+
the type alias' `__value__` to unpack literal values as well. This function
93+
*doesn't* check that the type alias is referencing a `Literal` special form,
94+
so unexpected values could be unpacked.
95+
"""
96+
# TODO When we drop support for Python 3.8, there's no need to check of `is_literal`
97+
# here, as Python unpacks nested `Literal` forms in 3.9+.
98+
# (see https://docs.python.org/3/whatsnew/3.9.html#id4).
9299
if not is_literal(tp):
100+
# Note: we could also check for generic aliases with a type alias as an origin.
101+
# However, it is very unlikely that this happens as type variables can't appear in
102+
# `Literal` forms, so the only valid (but unnecessary) use case would be something like:
103+
# `type Test[T] = Literal['a']` (and then use `Test[SomeType]`).
104+
if is_type_alias_type(tp):
105+
# Note: accessing `__value__` could raise a `NameError`, but we just let
106+
# the exception be raised as there's not much we can do if this happens.
107+
return literal_values(tp.__value__)
108+
93109
return [tp]
94110

95111
values = get_args(tp)

tests/test_json_schema.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from packaging.version import Version
3838
from pydantic_core import CoreSchema, SchemaValidator, core_schema, to_jsonable_python
3939
from pydantic_core.core_schema import ValidatorFunctionWrapHandler
40-
from typing_extensions import Annotated, Literal, Self, TypedDict, deprecated
40+
from typing_extensions import Annotated, Literal, Self, TypeAliasType, TypedDict, deprecated
4141

4242
import pydantic
4343
from pydantic import (
@@ -2378,6 +2378,17 @@ class Model(BaseModel):
23782378
}
23792379

23802380

2381+
def test_literal_schema_type_aliases() -> None:
2382+
TestType0 = TypeAliasType('TestType0', Literal['a'])
2383+
TestType1 = TypeAliasType('TestType1', Literal[TestType0, 'b'])
2384+
TestType2 = TypeAliasType('TestType2', Literal[TestType1, 'c'])
2385+
2386+
assert TypeAdapter(TestType2).json_schema() == {
2387+
'enum': ['a', 'b', 'c'],
2388+
'type': 'string',
2389+
}
2390+
2391+
23812392
def test_literal_enum():
23822393
class MyEnum(str, Enum):
23832394
FOO = 'foo'

0 commit comments

Comments
 (0)
0