Fix #947, #1483, #1247: allow inner type vars to be present in parent generic classes (#1989)

* 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:
Davis Kirkendall
2020-12-30 19:23:55 +01:00
committed by GitHub
parent aacf592040
commit 6b8d2babaa
6 changed files with 496 additions and 74 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.'