From d0a6404b27805eb7030aaa698b2eeafe1536176c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 17 Oct 2020 14:53:59 +0300 Subject: [PATCH] Added support for TypedDict inheritance This is problematic due to CPython bug 42059, but is easily worked around by using class-based declaration instead. Fixes #101. --- docs/userguide.rst | 13 +++++++++---- docs/versionhistory.rst | 2 ++ tests/test_typeguard.py | 31 ++++++++++++++++++++----------- tests/test_typeguard_py36.py | 19 +++++++++++++++++++ typeguard/__init__.py | 19 +++++++++++-------- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/docs/userguide.rst b/docs/userguide.rst index d8e69c4d..87d29acb 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -199,17 +199,22 @@ Type Notes ``Literal`` ``NamedTuple`` Field values are typechecked ``NoReturn`` -``Protocol`` Run-time protocols are checked with :func:`isinstance`, others are - ignored +``Protocol`` Run-time protocols are checked with :func:`isinstance`, + others are ignored ``Set`` Contents are typechecked ``Sequence`` Contents are typechecked ``Tuple`` Contents are typechecked ``Type`` -``TypedDict`` Contents are typechecked; ``total`` from superclasses is not - respected (see `#101`_ for more information) +``TypedDict`` Contents are typechecked; On Python 3.8 and earlier, + ``total`` from superclasses is not respected (see `#101`_ for + more information); On Python 3.9.0 or ``typing_extensions`` + <= 3.7.4.3, false positives can happen when constructing + ``TypedDict`` classes using old-style syntax (see + `issue 42059`_) ``TypeVar`` Constraints, bound types and co/contravariance are supported but custom generic types are not (due to type erasure) ``Union`` =============== ============================================================= .. _#101: https://github.com/agronholm/typeguard/issues/101 +.. _issue 42059: https://bugs.python.org/issue42059 diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 8c0f8d44..ecd4c25e 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -7,6 +7,8 @@ This library adheres to `Semantic Versioning 2.0 AsyncGenerator[int, None]: foo() + def test_typeddict_inherited(self): + class ParentDict(TypedDict): + x: int + + class ChildDict(ParentDict, total=False): + y: int + + @typechecked + def foo(arg: ChildDict): + pass + + foo({'x': 1}) + pytest.raises(TypeError, foo, {'y': 1}) + async def asyncgenfunc() -> AsyncGenerator[int, None]: yield 1 diff --git a/typeguard/__init__.py b/typeguard/__init__.py index 27702086..f441bf28 100644 --- a/typeguard/__init__.py +++ b/typeguard/__init__.py @@ -311,19 +311,22 @@ def check_dict(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None def check_typed_dict(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - expected_keys = frozenset(expected_type.__annotations__) - existing_keys = frozenset(value) + declared_keys = frozenset(expected_type.__annotations__) + if hasattr(expected_type, '__required_keys__'): + required_keys = expected_type.__required_keys__ + else: # py3.8 and lower + required_keys = declared_keys if expected_type.__total__ else frozenset() - extra_keys = existing_keys - expected_keys + existing_keys = frozenset(value) + extra_keys = existing_keys - declared_keys if extra_keys: keys_formatted = ', '.join('"{}"'.format(key) for key in sorted(extra_keys)) raise TypeError('extra key(s) ({}) in {}'.format(keys_formatted, argname)) - if expected_type.__total__: - missing_keys = expected_keys - existing_keys - if missing_keys: - keys_formatted = ', '.join('"{}"'.format(key) for key in sorted(missing_keys)) - raise TypeError('required key(s) ({}) missing from {}'.format(keys_formatted, argname)) + missing_keys = required_keys - existing_keys + if missing_keys: + keys_formatted = ', '.join('"{}"'.format(key) for key in sorted(missing_keys)) + raise TypeError('required key(s) ({}) missing from {}'.format(keys_formatted, argname)) for key, argtype in get_type_hints(expected_type).items(): argvalue = value.get(key, _missing)