import sys from collections import namedtuple from typing import Any, Callable as TypingCallable, Dict, ForwardRef, List, NamedTuple, NewType, Union # noqa: F401 import pytest from typing_extensions import Annotated # noqa: F401 from pydantic import Field # noqa: F401 from pydantic.typing import Literal, convert_generics, is_namedtuple, is_none_type, is_typeddict try: from typing import TypedDict as typing_TypedDict except ImportError: typing_TypedDict = None try: from typing_extensions import TypedDict as typing_extensions_TypedDict except ImportError: typing_extensions_TypedDict = None try: from mypy_extensions import TypedDict as mypy_extensions_TypedDict except ImportError: mypy_extensions_TypedDict = None ALL_TYPEDDICT_KINDS = (typing_TypedDict, typing_extensions_TypedDict, mypy_extensions_TypedDict) def test_is_namedtuple(): class Employee(NamedTuple): name: str id: int = 3 assert is_namedtuple(namedtuple('Point', 'x y')) is True assert is_namedtuple(Employee) is True assert is_namedtuple(NamedTuple('Employee', [('name', str), ('id', int)])) is True class Other(tuple): name: str id: int assert is_namedtuple(Other) is False @pytest.mark.parametrize('TypedDict', (t for t in ALL_TYPEDDICT_KINDS if t is not None)) def test_is_typeddict_typing(TypedDict): class Employee(TypedDict): name: str id: int assert is_typeddict(Employee) is True assert is_typeddict(TypedDict('Employee', {'name': str, 'id': int})) is True class Other(dict): name: str id: int assert is_typeddict(Other) is False def test_is_none_type(): assert is_none_type(Literal[None]) is True assert is_none_type(None) is True assert is_none_type(type(None)) is True assert is_none_type(6) is False assert is_none_type({}) is False # WARNING: It's important to test `typing.Callable` not # `collections.abc.Callable` (even with python >= 3.9) as they behave # differently assert is_none_type(TypingCallable) is False class Hero: pass class Team: pass @pytest.mark.skipif(sys.version_info < (3, 9), reason='PEP585 generics only supported for python 3.9 and above.') @pytest.mark.parametrize( ['type_', 'expectations'], [ ('int', 'int'), ('Union[list["Hero"], int]', 'Union[list[ForwardRef("Hero")], int]'), ('list["Hero"]', 'list[ForwardRef("Hero")]'), ('dict["Hero", "Team"]', 'dict[ForwardRef("Hero"), ForwardRef("Team")]'), ('dict["Hero", list["Team"]]', 'dict[ForwardRef("Hero"), list[ForwardRef("Team")]]'), ('dict["Hero", List["Team"]]', 'dict[ForwardRef("Hero"), List[ForwardRef("Team")]]'), ('Dict["Hero", list["Team"]]', 'Dict[ForwardRef("Hero"), list[ForwardRef("Team")]]'), ( 'Annotated[list["Hero"], Field(min_length=2)]', 'Annotated[list[ForwardRef("Hero")], Field(min_length=2)]', ), ], ) def test_convert_generics(type_, expectations): assert str(convert_generics(eval(type_))) == str(eval(expectations)) @pytest.mark.skipif(sys.version_info < (3, 10), reason='NewType class was added in python 3.10.') def test_convert_generics_unsettable_args(): class User(NewType): __origin__ = type(list[str]) __args__ = (list['Hero'],) def __init__(self, name: str, tp: type) -> None: super().__init__(name, tp) def __setattr__(self, __name: str, __value: Any) -> None: if __name == '__args__': raise AttributeError # will be thrown during the generics conversion return super().__setattr__(__name, __value) # tests that convert_generics will not throw an exception even if __args__ isn't settable assert convert_generics(User('MyUser', str)).__args__ == (list['Hero'],) @pytest.mark.skipif(sys.version_info < (3, 10), reason='PEP604 unions only supported for python 3.10 and above.') def test_convert_generics_pep604(): assert ( convert_generics(dict['Hero', list['Team']] | int) == dict[ForwardRef('Hero'), list[ForwardRef('Team')]] | int )