mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
* Fix #947, fix #1483, fix #1247 allow inner type vars to be present in parent generic classes - Rename generics._is_type to _is_generic in response to comment: https://github.com/samuelcolvin/pydantic/pull/1989#discussion_r503400391 - Add more explicit type assertion in generics test - Add generics tests and unify naming - Move deep generic tests all into same place in code - Unify naming convention in deep generic tests using naming of existing tests - Add tests for multiple inheritance and multiple type vars - Add generic tests for type hint resolution cases - Fix edge cases for handling parameters in generic models - Resolve parameters correctly - Add tests for special cases like callables - Add returning generic type directly if parameters and arguments are identical. * Apply review comments - Add docstrings - Small refactor `generics.__concrete_name__` - Small refactor of `generics.resolve_type_hints` - Change `is_identity_typevars_map` to more generic and clearer `all_identical` and move into utils. - rename "resolve_type_hint" to "replace_types" so as to not get confused with "resolve_annotations" from `pydantic.typing` * Fix generics test coverage * Update pydantic/generics.py, remove unneeded annotation mypy understands list comprehensions as they are now Co-authored-by: Eric Jolibois <em.jolibois@gmail.com> Co-authored-by: Eric Jolibois <em.jolibois@gmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Fix bug where generic models with fields where the typevar is nested in another type `a: List[T]` are considered to be concrete. This allows these models to be subclassed and composed as expected.
|
||||
+119
-35
@@ -1,10 +1,14 @@
|
||||
import sys
|
||||
import typing
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
ClassVar,
|
||||
Dict,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
@@ -17,8 +21,8 @@ from typing import (
|
||||
from .class_validators import gather_all_validators
|
||||
from .fields import FieldInfo, ModelField
|
||||
from .main import BaseModel, create_model
|
||||
from .typing import get_origin
|
||||
from .utils import lenient_issubclass
|
||||
from .typing import display_as_type, get_args, get_origin, typing_base
|
||||
from .utils import all_identical, lenient_issubclass
|
||||
|
||||
_generic_types_cache: Dict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]] = {}
|
||||
GenericModelT = TypeVar('GenericModelT', bound='GenericModel')
|
||||
@@ -37,6 +41,16 @@ class GenericModel(BaseModel):
|
||||
|
||||
# Setting the return type as Type[Any] instead of Type[BaseModel] prevents PyCharm warnings
|
||||
def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[Type[Any], ...]]) -> Type[Any]:
|
||||
"""Instantiates a new class from a generic class `cls` and type variables `params`.
|
||||
|
||||
:param params: Tuple of types the class . Given a generic class
|
||||
`Model` with 2 type variables and a concrete model `Model[str, int]`,
|
||||
the value `(str, int)` would be passed to `params`.
|
||||
:return: New model class inheriting from `cls` with instantiated
|
||||
types described by `params`. If no parameters are given, `cls` is
|
||||
returned as is.
|
||||
|
||||
"""
|
||||
cached = _generic_types_cache.get((cls, params))
|
||||
if cached is not None:
|
||||
return cached
|
||||
@@ -50,16 +64,24 @@ class GenericModel(BaseModel):
|
||||
raise TypeError(f'Type {cls.__name__} must inherit from typing.Generic before being parameterized')
|
||||
|
||||
check_parameters_count(cls, params)
|
||||
# Build map from generic typevars to passed params
|
||||
typevars_map: Dict[TypeVarType, Type[Any]] = dict(zip(cls.__parameters__, params))
|
||||
if all_identical(typevars_map.keys(), typevars_map.values()) and typevars_map:
|
||||
return cls # if arguments are equal to parameters it's the same object
|
||||
|
||||
# Recursively walk class type hints and replace generic typevars
|
||||
# with concrete types that were passed.
|
||||
type_hints = get_type_hints(cls).items()
|
||||
instance_type_hints = {k: v for k, v in type_hints if get_origin(v) is not ClassVar}
|
||||
concrete_type_hints: Dict[str, Type[Any]] = {
|
||||
k: resolve_type_hint(v, typevars_map) for k, v in instance_type_hints.items()
|
||||
k: replace_types(v, typevars_map) for k, v in instance_type_hints.items()
|
||||
}
|
||||
|
||||
# Create new model with original model as parent inserting fields with
|
||||
# updated type hints.
|
||||
model_name = cls.__concrete_name__(params)
|
||||
validators = gather_all_validators(cls)
|
||||
fields = _build_generic_fields(cls.__fields__, concrete_type_hints, typevars_map)
|
||||
fields = _build_generic_fields(cls.__fields__, concrete_type_hints)
|
||||
model_module, called_globally = get_caller_frame_info()
|
||||
created_model = cast(
|
||||
Type[GenericModel], # casting ensures mypy is aware of the __concrete__ and __parameters__ attributes
|
||||
@@ -82,12 +104,20 @@ class GenericModel(BaseModel):
|
||||
reference_name += '_'
|
||||
|
||||
created_model.Config = cls.Config
|
||||
concrete = all(not _is_typevar(v) for v in concrete_type_hints.values())
|
||||
created_model.__concrete__ = concrete
|
||||
if not concrete:
|
||||
parameters = tuple(v for v in concrete_type_hints.values() if _is_typevar(v))
|
||||
parameters = tuple({k: None for k in parameters}.keys()) # get unique params while maintaining order
|
||||
created_model.__parameters__ = parameters
|
||||
|
||||
# Find any typevars that are still present in the model.
|
||||
# If none are left, the model is fully "concrete", otherwise the new
|
||||
# class is a generic class as well taking the found typevars as
|
||||
# parameters.
|
||||
new_params = tuple(
|
||||
{param: None for param in iter_contained_typevars(typevars_map.values())}
|
||||
) # use dict as ordered set
|
||||
created_model.__concrete__ = not new_params
|
||||
if new_params:
|
||||
created_model.__parameters__ = new_params
|
||||
|
||||
# Save created model in cache so we don't end up creating duplicate
|
||||
# models that should be identical.
|
||||
_generic_types_cache[(cls, params)] = created_model
|
||||
if len(params) == 1:
|
||||
_generic_types_cache[(cls, params[0])] = created_model
|
||||
@@ -95,19 +125,74 @@ class GenericModel(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
|
||||
"""Compute class name for child classes.
|
||||
|
||||
:param params: Tuple of types the class . Given a generic class
|
||||
`Model` with 2 type variables and a concrete model `Model[str, int]`,
|
||||
the value `(str, int)` would be passed to `params`.
|
||||
:return: String representing a the new class where `params` are
|
||||
passed to `cls` as type variables.
|
||||
|
||||
This method can be overridden to achieve a custom naming scheme for GenericModels.
|
||||
"""
|
||||
This method can be overridden to achieve a custom naming scheme for GenericModels
|
||||
"""
|
||||
param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
|
||||
param_names = [display_as_type(param) for param in params]
|
||||
params_component = ', '.join(param_names)
|
||||
return f'{cls.__name__}[{params_component}]'
|
||||
|
||||
|
||||
def resolve_type_hint(type_: Any, typevars_map: Dict[Any, Any]) -> Type[Any]:
|
||||
if get_origin(type_) and getattr(type_, '__parameters__', None):
|
||||
concrete_type_args = tuple([typevars_map[x] for x in type_.__parameters__])
|
||||
return type_[concrete_type_args]
|
||||
return typevars_map.get(type_, type_)
|
||||
def replace_types(type_: Any, type_map: Dict[Any, Any]) -> Any:
|
||||
"""Return type with all occurances of `type_map` keys recursively replaced with their values.
|
||||
|
||||
:param type_: Any type, class or generic alias
|
||||
:type_map: Mapping from `TypeVar` instance to concrete types.
|
||||
:return: New type representing the basic structure of `type_` with all
|
||||
`typevar_map` keys recursively replaced.
|
||||
|
||||
>>> replace_types(Tuple[str, Union[List[str], float]], {str: int})
|
||||
Tuple[int, Union[List[int], float]]
|
||||
|
||||
"""
|
||||
if not type_map:
|
||||
return type_
|
||||
|
||||
type_args = get_args(type_)
|
||||
origin_type = get_origin(type_)
|
||||
|
||||
# Having type args is a good indicator that this is a typing module
|
||||
# class instantiation or a generic alias of some sort.
|
||||
if type_args:
|
||||
resolved_type_args = tuple(replace_types(arg, type_map) for arg in type_args)
|
||||
if all_identical(type_args, resolved_type_args):
|
||||
# If all arguments are the same, there is no need to modify the
|
||||
# type or create a new object at all
|
||||
return type_
|
||||
if origin_type is not None and isinstance(type_, typing_base) and not isinstance(origin_type, typing_base):
|
||||
# In python < 3.9 generic aliases don't exist so any of these like `list`,
|
||||
# `type` or `collections.abc.Callable` need to be translated.
|
||||
# See: https://www.python.org/dev/peps/pep-0585
|
||||
origin_type = getattr(typing, type_._name)
|
||||
return origin_type[resolved_type_args]
|
||||
|
||||
# We handle pydantic generic models separately as they don't have the same
|
||||
# semantics as "typing" classes or generic aliases
|
||||
if not origin_type and lenient_issubclass(type_, GenericModel) and not type_.__concrete__:
|
||||
type_args = type_.__parameters__
|
||||
resolved_type_args = tuple(replace_types(t, type_map) for t in type_args)
|
||||
if all_identical(type_args, resolved_type_args):
|
||||
return type_
|
||||
return type_[resolved_type_args]
|
||||
|
||||
# Handle special case for typehints that can have lists as arguments.
|
||||
# `typing.Callable[[int, str], int]` is an example for this.
|
||||
if isinstance(type_, (List, list)):
|
||||
resolved_list = list(replace_types(element, type_map) for element in type_)
|
||||
if all_identical(type_, resolved_list):
|
||||
return type_
|
||||
return resolved_list
|
||||
|
||||
# If all else fails, we try to resolve the type directly and otherwise just
|
||||
# return the input with no modifications.
|
||||
return type_map.get(type_, type_)
|
||||
|
||||
|
||||
def check_parameters_count(cls: Type[GenericModel], parameters: Tuple[Any, ...]) -> None:
|
||||
@@ -118,27 +203,26 @@ def check_parameters_count(cls: Type[GenericModel], parameters: Tuple[Any, ...])
|
||||
raise TypeError(f'Too {description} parameters for {cls.__name__}; actual {actual}, expected {expected}')
|
||||
|
||||
|
||||
def iter_contained_typevars(v: Any) -> Iterator[TypeVarType]:
|
||||
"""Recursively iterate through all subtypes and type args of `v` and yield any typevars that are found."""
|
||||
if isinstance(v, TypeVar):
|
||||
yield v
|
||||
elif hasattr(v, '__parameters__') and not get_origin(v) and lenient_issubclass(v, GenericModel):
|
||||
yield from v.__parameters__
|
||||
elif isinstance(v, Iterable):
|
||||
for var in v:
|
||||
yield from iter_contained_typevars(var)
|
||||
else:
|
||||
args = get_args(v)
|
||||
for arg in args:
|
||||
yield from iter_contained_typevars(arg)
|
||||
|
||||
|
||||
def _build_generic_fields(
|
||||
raw_fields: Dict[str, ModelField],
|
||||
concrete_type_hints: Dict[str, Type[Any]],
|
||||
typevars_map: Dict[TypeVarType, Type[Any]],
|
||||
) -> Dict[str, Tuple[Type[Any], FieldInfo]]:
|
||||
return {
|
||||
k: (_parameterize_generic_field(v, typevars_map), raw_fields[k].field_info)
|
||||
for k, v in concrete_type_hints.items()
|
||||
if k in raw_fields
|
||||
}
|
||||
|
||||
|
||||
def _parameterize_generic_field(field_type: Type[Any], typevars_map: Dict[TypeVarType, Type[Any]]) -> Type[Any]:
|
||||
if lenient_issubclass(field_type, GenericModel) and not field_type.__concrete__:
|
||||
parameters = tuple(typevars_map.get(param, param) for param in field_type.__parameters__)
|
||||
field_type = field_type[parameters]
|
||||
return field_type
|
||||
|
||||
|
||||
def _is_typevar(v: Any) -> bool:
|
||||
return isinstance(v, TypeVar)
|
||||
return {k: (v, raw_fields[k].field_info) for k, v in concrete_type_hints.items() if k in raw_fields}
|
||||
|
||||
|
||||
def get_caller_frame_info() -> Tuple[Optional[str], bool]:
|
||||
|
||||
+40
-4
@@ -25,6 +25,12 @@ try:
|
||||
except ImportError:
|
||||
from typing import _Final as typing_base # type: ignore
|
||||
|
||||
try:
|
||||
from typing import GenericAlias # type: ignore
|
||||
except ImportError:
|
||||
# python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on)
|
||||
GenericAlias = ()
|
||||
|
||||
|
||||
if sys.version_info < (3, 7):
|
||||
if TYPE_CHECKING:
|
||||
@@ -74,7 +80,7 @@ else:
|
||||
AnyCallable = TypingCallable[..., Any]
|
||||
NoArgAnyCallable = TypingCallable[[], Any]
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
if sys.version_info < (3, 8): # noqa: C901
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
else: # due to different mypy warnings raised during CI for python 3.7 and 3.8
|
||||
@@ -83,8 +89,34 @@ if sys.version_info < (3, 8):
|
||||
except ImportError:
|
||||
Literal = None
|
||||
|
||||
def get_args(t: Type[Any]) -> Tuple[Any, ...]:
|
||||
return getattr(t, '__args__', ())
|
||||
if sys.version_info < (3, 7):
|
||||
|
||||
def get_args(t: Type[Any]) -> Tuple[Any, ...]:
|
||||
"""Simplest get_args compatability layer possible.
|
||||
|
||||
The Python 3.6 typing module does not have `_GenericAlias` so
|
||||
this won't work for everything. In particular this will not
|
||||
support the `generics` module (we don't support generic models in
|
||||
python 3.6).
|
||||
|
||||
"""
|
||||
return getattr(t, '__args__', ())
|
||||
|
||||
else:
|
||||
from typing import _GenericAlias
|
||||
|
||||
def get_args(t: Type[Any]) -> Tuple[Any, ...]:
|
||||
"""Compatability version of get_args for python 3.7.
|
||||
|
||||
Mostly compatible with the python 3.8 `typing` module version
|
||||
and able to handle almost all use cases.
|
||||
"""
|
||||
if isinstance(t, _GenericAlias):
|
||||
res = t.__args__
|
||||
if t.__origin__ is Callable and res and res[0] is not Ellipsis:
|
||||
res = (list(res[:-1]), res[-1])
|
||||
return res
|
||||
return getattr(t, '__args__', ())
|
||||
|
||||
def get_origin(t: Type[Any]) -> Optional[Type[Any]]:
|
||||
return getattr(t, '__origin__', None)
|
||||
@@ -179,7 +211,7 @@ NoneType = None.__class__
|
||||
|
||||
|
||||
def display_as_type(v: Type[Any]) -> str:
|
||||
if not isinstance(v, typing_base) and not isinstance(v, type):
|
||||
if not isinstance(v, typing_base) and not isinstance(v, GenericAlias) and not isinstance(v, type):
|
||||
v = v.__class__
|
||||
|
||||
if isinstance(v, type) and issubclass(v, Enum):
|
||||
@@ -190,6 +222,10 @@ def display_as_type(v: Type[Any]) -> str:
|
||||
else:
|
||||
return 'enum'
|
||||
|
||||
if isinstance(v, GenericAlias):
|
||||
# Generic alias are constructs like `list[int]`
|
||||
return str(v).replace('typing.', '')
|
||||
|
||||
try:
|
||||
return v.__name__
|
||||
except AttributeError:
|
||||
|
||||
+20
-1
@@ -2,7 +2,7 @@ import warnings
|
||||
import weakref
|
||||
from collections import OrderedDict, defaultdict, deque
|
||||
from copy import deepcopy
|
||||
from itertools import islice
|
||||
from itertools import islice, zip_longest
|
||||
from types import BuiltinFunctionType, CodeType, FunctionType, GeneratorType, LambdaType, ModuleType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -11,6 +11,7 @@ from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
@@ -639,3 +640,21 @@ def is_valid_private_name(name: str) -> bool:
|
||||
'__orig_bases__',
|
||||
'__qualname__',
|
||||
}
|
||||
|
||||
|
||||
_EMPTY = object()
|
||||
|
||||
|
||||
def all_identical(left: Iterable[Any], right: Iterable[Any]) -> bool:
|
||||
"""Check that the items of `left` are the same objects as those in `right`.
|
||||
|
||||
>>> a, b = object(), object()
|
||||
>>> all_identical([a, b, a], [a, b, a])
|
||||
True
|
||||
>>> all_identical([a, b, [a]], [a, b, [a]]) # new list object, while "equal" is not "identical"
|
||||
False
|
||||
"""
|
||||
for left_item, right_item in zip_longest(left, right, fillvalue=_EMPTY):
|
||||
if left_item is not right_item:
|
||||
return False
|
||||
return True
|
||||
|
||||
+292
-33
@@ -1,11 +1,11 @@
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union
|
||||
from typing import Any, Callable, ClassVar, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError, root_validator, validator
|
||||
from pydantic.generics import GenericModel, _generic_types_cache
|
||||
from pydantic.generics import GenericModel, _generic_types_cache, iter_contained_typevars, replace_types
|
||||
|
||||
skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above')
|
||||
|
||||
@@ -17,7 +17,10 @@ def test_generic_name():
|
||||
class Result(GenericModel, Generic[data_type]):
|
||||
data: data_type
|
||||
|
||||
assert Result[List[int]].__name__ == 'Result[typing.List[int]]'
|
||||
if sys.version_info >= (3, 9):
|
||||
assert Result[list[int]].__name__ == 'Result[list[int]]'
|
||||
assert Result[List[int]].__name__ == 'Result[List[int]]'
|
||||
assert Result[int].__name__ == 'Result[int]'
|
||||
|
||||
|
||||
@skip_36
|
||||
@@ -248,35 +251,6 @@ def test_generic_config():
|
||||
result.data = 2
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic():
|
||||
T = TypeVar('T')
|
||||
S = TypeVar('S')
|
||||
R = TypeVar('R')
|
||||
|
||||
class OuterModel(GenericModel, Generic[T, S, R]):
|
||||
a: Dict[R, Optional[List[T]]]
|
||||
b: Optional[Union[S, R]]
|
||||
c: R
|
||||
d: float
|
||||
|
||||
class InnerModel(GenericModel, Generic[T, R]):
|
||||
c: T
|
||||
d: R
|
||||
|
||||
class NormalModel(BaseModel):
|
||||
e: int
|
||||
f: str
|
||||
|
||||
inner_model = InnerModel[int, str]
|
||||
generic_model = OuterModel[inner_model, NormalModel, int]
|
||||
|
||||
inner_models = [inner_model(c=1, d='a')]
|
||||
generic_model(a={1: inner_models, 2: None}, b=None, c=1, d=1.5)
|
||||
generic_model(a={}, b=NormalModel(e=1, f='a'), c=1, d=1.5)
|
||||
generic_model(a={}, b=1, c=1, d=1.5)
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_enum_generic():
|
||||
T = TypeVar('T')
|
||||
@@ -498,6 +472,26 @@ def test_partial_specification():
|
||||
]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_partial_specification_with_inner_typevar():
|
||||
AT = TypeVar('AT')
|
||||
BT = TypeVar('BT')
|
||||
|
||||
class Model(GenericModel, Generic[AT, BT]):
|
||||
a: List[AT]
|
||||
b: List[BT]
|
||||
|
||||
partial_model = Model[str, BT]
|
||||
assert partial_model.__concrete__ is False
|
||||
concrete_model = partial_model[int]
|
||||
assert concrete_model.__concrete__ is True
|
||||
|
||||
# nested resolution of partial models should work as expected
|
||||
nested_resolved = concrete_model(a=[123], b=['456'])
|
||||
assert nested_resolved.a == ['123']
|
||||
assert nested_resolved.b == [456]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_partial_specification_name():
|
||||
AT = TypeVar('AT')
|
||||
@@ -681,7 +675,11 @@ def test_generic_model_from_function_pickle_fail(create_module):
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_generic_model_redefined_without_cache_fail(create_module):
|
||||
def test_generic_model_redefined_without_cache_fail(create_module, monkeypatch):
|
||||
|
||||
# match identity checker otherwise we never get to the redefinition check
|
||||
monkeypatch.setattr('pydantic.generics.all_identical', lambda left, right: False)
|
||||
|
||||
@create_module
|
||||
def module():
|
||||
from typing import Generic, TypeVar
|
||||
@@ -756,3 +754,264 @@ def test_get_caller_frame_info_when_sys_getframe_undefined():
|
||||
assert get_caller_frame_info() == (None, False)
|
||||
finally: # just to make sure we always setting original attribute back
|
||||
sys._getframe = getframe
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_iter_contained_typevars():
|
||||
T = TypeVar('T')
|
||||
T2 = TypeVar('T2')
|
||||
|
||||
class Model(GenericModel, Generic[T]):
|
||||
a: T
|
||||
|
||||
assert list(iter_contained_typevars(Model[T])) == [T]
|
||||
assert list(iter_contained_typevars(Optional[List[Union[str, Model[T]]]])) == [T]
|
||||
assert list(iter_contained_typevars(Optional[List[Union[str, Model[int]]]])) == []
|
||||
assert list(iter_contained_typevars(Optional[List[Union[str, Model[T], Callable[[T2, T], str]]]])) == [T, T2, T]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_nested_identity_parameterization():
|
||||
T = TypeVar('T')
|
||||
T2 = TypeVar('T2')
|
||||
|
||||
class Model(GenericModel, Generic[T]):
|
||||
a: T
|
||||
|
||||
assert Model[T][T][T] is Model
|
||||
assert Model[T] is Model
|
||||
assert Model[T2] is not Model
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_replace_types():
|
||||
T = TypeVar('T')
|
||||
|
||||
class Model(GenericModel, Generic[T]):
|
||||
a: T
|
||||
|
||||
assert replace_types(T, {T: int}) is int
|
||||
assert replace_types(List[Union[str, list, T]], {T: int}) == List[Union[str, list, int]]
|
||||
assert replace_types(Callable, {T: int}) == Callable
|
||||
assert replace_types(Callable[[int, str, T], T], {T: int}) == Callable[[int, str, int], int]
|
||||
assert replace_types(T, {}) is T
|
||||
assert replace_types(Model[List[T]], {T: int}) == Model[List[T]][int]
|
||||
assert replace_types(T, {}) is T
|
||||
assert replace_types(Type[T], {T: int}) == Type[int]
|
||||
assert replace_types(Model[T], {T: T}) == Model[T]
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
# Check generic aliases (subscripted builtin types) to make sure they
|
||||
# resolve correctly (don't get translated to typing versions for
|
||||
# example)
|
||||
assert replace_types(list[Union[str, list, T]], {T: int}) == list[Union[str, list, int]]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_replace_types_identity_on_unchanged():
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
|
||||
type_ = List[Union[str, Callable[[list], Optional[str]], U]]
|
||||
assert replace_types(type_, {T: int}) is type_
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic():
|
||||
T = TypeVar('T')
|
||||
S = TypeVar('S')
|
||||
R = TypeVar('R')
|
||||
|
||||
class OuterModel(GenericModel, Generic[T, S, R]):
|
||||
a: Dict[R, Optional[List[T]]]
|
||||
b: Optional[Union[S, R]]
|
||||
c: R
|
||||
d: float
|
||||
|
||||
class InnerModel(GenericModel, Generic[T, R]):
|
||||
c: T
|
||||
d: R
|
||||
|
||||
class NormalModel(BaseModel):
|
||||
e: int
|
||||
f: str
|
||||
|
||||
inner_model = InnerModel[int, str]
|
||||
generic_model = OuterModel[inner_model, NormalModel, int]
|
||||
|
||||
inner_models = [inner_model(c=1, d='a')]
|
||||
generic_model(a={1: inner_models, 2: None}, b=None, c=1, d=1.5)
|
||||
generic_model(a={}, b=NormalModel(e=1, f='a'), c=1, d=1.5)
|
||||
generic_model(a={}, b=1, c=1, d=1.5)
|
||||
|
||||
assert InnerModel.__concrete__ is False
|
||||
assert inner_model.__concrete__ is True
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic_with_inner_typevar():
|
||||
T = TypeVar('T')
|
||||
|
||||
class OuterModel(GenericModel, Generic[T]):
|
||||
a: List[T]
|
||||
|
||||
class InnerModel(OuterModel[T], Generic[T]):
|
||||
pass
|
||||
|
||||
assert InnerModel[int].__concrete__ is True
|
||||
assert InnerModel.__concrete__ is False
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
InnerModel[int](a=['wrong'])
|
||||
assert InnerModel[int](a=['1']).a == [1]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic_with_referenced_generic():
|
||||
T = TypeVar('T')
|
||||
R = TypeVar('R')
|
||||
|
||||
class ReferencedModel(GenericModel, Generic[R]):
|
||||
a: R
|
||||
|
||||
class OuterModel(GenericModel, Generic[T]):
|
||||
a: ReferencedModel[T]
|
||||
|
||||
class InnerModel(OuterModel[T], Generic[T]):
|
||||
pass
|
||||
|
||||
assert InnerModel[int].__concrete__ is True
|
||||
assert InnerModel.__concrete__ is False
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
InnerModel[int](a={'a': 'wrong'})
|
||||
assert InnerModel[int](a={'a': 1}).a.a == 1
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic_with_referenced_inner_generic():
|
||||
T = TypeVar('T')
|
||||
|
||||
class ReferencedModel(GenericModel, Generic[T]):
|
||||
a: T
|
||||
|
||||
class OuterModel(GenericModel, Generic[T]):
|
||||
a: Optional[List[Union[ReferencedModel[T], str]]]
|
||||
|
||||
class InnerModel(OuterModel[T], Generic[T]):
|
||||
pass
|
||||
|
||||
assert InnerModel[int].__concrete__ is True
|
||||
assert InnerModel.__concrete__ is False
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
InnerModel[int](a=['s', {'a': 'wrong'}])
|
||||
assert InnerModel[int](a=['s', {'a': 1}]).a[1].a == 1
|
||||
|
||||
assert InnerModel[int].__fields__['a'].outer_type_ == List[Union[ReferencedModel[int], str]]
|
||||
assert (InnerModel[int].__fields__['a'].sub_fields[0].sub_fields[0].outer_type_.__fields__['a'].outer_type_) == int
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic_with_multiple_typevars():
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
|
||||
class OuterModel(GenericModel, Generic[T]):
|
||||
data: List[T]
|
||||
|
||||
class InnerModel(OuterModel[T], Generic[U, T]):
|
||||
extra: U
|
||||
|
||||
ConcreteInnerModel = InnerModel[int, float]
|
||||
assert ConcreteInnerModel.__fields__['data'].outer_type_ == List[float]
|
||||
assert ConcreteInnerModel.__fields__['extra'].outer_type_ == int
|
||||
|
||||
assert ConcreteInnerModel(data=['1'], extra='2').dict() == {'data': [1.0], 'extra': 2}
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_deep_generic_with_multiple_inheritance():
|
||||
K = TypeVar('K')
|
||||
V = TypeVar('V')
|
||||
T = TypeVar('T')
|
||||
|
||||
class OuterModelA(GenericModel, Generic[K, V]):
|
||||
data: Dict[K, V]
|
||||
|
||||
class OuterModelB(GenericModel, Generic[T]):
|
||||
stuff: List[T]
|
||||
|
||||
class InnerModel(OuterModelA[K, V], OuterModelB[T], Generic[K, V, T]):
|
||||
extra: int
|
||||
|
||||
ConcreteInnerModel = InnerModel[int, float, str]
|
||||
|
||||
assert ConcreteInnerModel.__fields__['data'].outer_type_ == Dict[int, float]
|
||||
assert ConcreteInnerModel.__fields__['stuff'].outer_type_ == List[str]
|
||||
assert ConcreteInnerModel.__fields__['extra'].outer_type_ == int
|
||||
|
||||
ConcreteInnerModel(data={1.1: '5'}, stuff=[123], extra=5).dict() == {
|
||||
'data': {1: 5},
|
||||
'stuff': ['123'],
|
||||
'extra': 5,
|
||||
}
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_generic_with_referenced_generic_type_1():
|
||||
T = TypeVar('T')
|
||||
|
||||
class ModelWithType(GenericModel, Generic[T]):
|
||||
# Type resolves to type origin of "type" which is non-subscriptible for
|
||||
# python < 3.9 so we want to make sure it works for other versions
|
||||
some_type: Type[T]
|
||||
|
||||
class ReferenceModel(GenericModel, Generic[T]):
|
||||
abstract_base_with_type: ModelWithType[T]
|
||||
|
||||
ReferenceModel[int]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_generic_with_referenced_nested_typevar():
|
||||
T = TypeVar('T')
|
||||
|
||||
class ModelWithType(GenericModel, Generic[T]):
|
||||
# Type resolves to type origin of "collections.abc.Sequence" which is
|
||||
# non-subscriptible for
|
||||
# python < 3.9 so we want to make sure it works for other versions
|
||||
some_type: Sequence[T]
|
||||
|
||||
class ReferenceModel(GenericModel, Generic[T]):
|
||||
abstract_base_with_type: ModelWithType[T]
|
||||
|
||||
ReferenceModel[int]
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_generic_with_callable():
|
||||
T = TypeVar('T')
|
||||
|
||||
class Model(GenericModel, Generic[T]):
|
||||
# Callable is a test for any type that accepts a list as an argument
|
||||
some_callable: Callable[[Optional[int], T], None]
|
||||
|
||||
Model[str].__concrete__ is True
|
||||
Model.__concrete__ is False
|
||||
|
||||
|
||||
@skip_36
|
||||
def test_generic_with_partial_callable():
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
|
||||
class Model(GenericModel, Generic[T, U]):
|
||||
t: T
|
||||
u: U
|
||||
# Callable is a test for any type that accepts a list as an argument
|
||||
some_callable: Callable[[Optional[int], str], None]
|
||||
|
||||
Model[str, U].__concrete__ is False
|
||||
Model[str, U].__parameters__ == [U]
|
||||
Model[str, int].__concrete__ is False
|
||||
|
||||
+24
-1
@@ -27,6 +27,7 @@ from pydantic.utils import (
|
||||
BUILTIN_COLLECTIONS,
|
||||
ClassAttribute,
|
||||
ValueItems,
|
||||
all_identical,
|
||||
deep_update,
|
||||
get_model,
|
||||
import_string,
|
||||
@@ -60,11 +61,18 @@ def test_import_no_attr():
|
||||
assert exc_info.value.args[0] == 'Module "os" does not define a "foobar" attribute'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value,expected', ((str, 'str'), ('string', 'str'), (Union[str, int], 'Union[str, int]')))
|
||||
@pytest.mark.parametrize(
|
||||
'value,expected', ((str, 'str'), ('string', 'str'), (Union[str, int], 'Union[str, int]'), (list, 'list'))
|
||||
)
|
||||
def test_display_as_type(value, expected):
|
||||
assert display_as_type(value) == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 9), reason='generic aliases are not available in python < 3.9')
|
||||
def test_display_as_type_generic_alias():
|
||||
assert display_as_type(list[[Union[str, int]]]) == 'list[[Union[str, int]]]'
|
||||
|
||||
|
||||
def test_display_as_type_enum():
|
||||
class SubField(Enum):
|
||||
a = 1
|
||||
@@ -446,3 +454,18 @@ def test_resolve_annotations_no_module():
|
||||
# TODO: is there a better test for this, can this case really happen?
|
||||
fr = ForwardRef('Foo')
|
||||
assert resolve_annotations({'Foo': ForwardRef('Foo')}, None) == {'Foo': fr}
|
||||
|
||||
|
||||
def test_all_identical():
|
||||
a, b = object(), object()
|
||||
c = [b]
|
||||
assert all_identical([a, b], [a, b]) is True
|
||||
assert all_identical([a, b], [a, b]) is True
|
||||
assert all_identical([a, b, b], [a, b, b]) is True
|
||||
assert all_identical([a, c, b], [a, c, b]) is True
|
||||
|
||||
assert all_identical([], [a]) is False, 'Expected iterables with different lengths to evaluate to `False`'
|
||||
assert all_identical([a], []) is False, 'Expected iterables with different lengths to evaluate to `False`'
|
||||
assert (
|
||||
all_identical([a, [b], b], [a, [b], b]) is False
|
||||
), 'New list objects are different objects and should therefor not be identical.'
|
||||
|
||||
Reference in New Issue
Block a user