From 0ec3dad8d0c40d6119d4c20b8e081fcb3d0f180e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 22 Nov 2021 23:50:50 +0200 Subject: [PATCH] Fixed Dict-derived classes being mistaken for TypedDict on Python 3.10+ Fixes #216. --- docs/versionhistory.rst | 2 ++ src/typeguard/__init__.py | 15 ++++++++++++++- tests/test_typeguard_py36.py | 14 +++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index a20200e7..ff40dac7 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -6,6 +6,8 @@ This library adheres to `Semantic Versioning 2.0 = (3, 10): + from typing import is_typeddict +elif sys.version_info >= (3, 9): + def is_typeddict(tp) -> bool: + from typing import _TypedDictMeta + + return isinstance(tp, _TypedDictMeta) +else: + def is_typeddict(tp) -> bool: + from typing_extensions import _TypedDictMeta + + return isinstance(tp, _TypedDictMeta) + if TYPE_CHECKING: _F = TypeVar("_F") @@ -750,7 +763,7 @@ def check_type(argname: str, value, expected_type, memo: Optional[_TypeCheckMemo check_typevar(argname, value, expected_type, memo) elif issubclass(expected_type, IO): check_io(argname, value, expected_type) - elif issubclass(expected_type, dict) and hasattr(expected_type, '__annotations__'): + elif is_typeddict(expected_type): check_typed_dict(argname, value, expected_type, memo) elif getattr(expected_type, '_is_protocol', False): check_protocol(argname, value, expected_type) diff --git a/tests/test_typeguard_py36.py b/tests/test_typeguard_py36.py index bdddfaa0..80dc793f 100644 --- a/tests/test_typeguard_py36.py +++ b/tests/test_typeguard_py36.py @@ -1,5 +1,5 @@ import warnings -from typing import AsyncGenerator, AsyncIterable, AsyncIterator, Callable +from typing import Any, AsyncGenerator, AsyncIterable, AsyncIterator, Callable, Dict import pytest from typing_extensions import Protocol, runtime_checkable @@ -112,6 +112,18 @@ def foo(arg: ChildDict): foo({'x': 1}) pytest.raises(TypeError, foo, {'y': 1}) + def test_mapping_is_not_typeddict(self): + """Regression test for #216.""" + + class Foo(Dict[str, Any]): + pass + + @typechecked + def foo(arg: Foo): + pass + + foo(Foo({'x': 1})) + async def asyncgenfunc() -> AsyncGenerator[int, None]: yield 1