Skip to content

Commit

Permalink
Added support for TypedDict
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Oct 22, 2019
1 parent 97487f2 commit 5eebb4b
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 11 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ Type Notes
``Sequence`` Contents are typechecked
``Tuple`` Contents are typechecked
``Type``
``TypedDict`` Contents are typechecked
``TypeVar`` Constraints, bound types and co/contravariance are supported
but custom generic types are not (due to type erasure)
``Union``
Expand Down
1 change: 1 addition & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This library adheres to `Semantic Versioning 2.0 <https://semver.org/#semantic-v
**UNRELEASED**

- Added a :pep:`302` import hook for annotating functions and classes with ``@typechecked``
- Added support for ``typing.TypedDict``
- Deprecated ``TypeChecker`` (will be removed in v3.0)

**2.5.1** (2019-09-26)
Expand Down
10 changes: 1 addition & 9 deletions tests/test_typeguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from typeguard import (
typechecked, check_argument_types, qualified_name, TypeChecker, TypeWarning, function_name,
check_type, Literal, TypeHintWarning, ForwardRefPolicy, check_return_type)
check_type, TypeHintWarning, ForwardRefPolicy, check_return_type)

try:
from typing import Type
Expand Down Expand Up @@ -624,14 +624,6 @@ def foo(a: TextIO):
with tmpdir.join('testfile').open('w') as f:
foo(f)

@pytest.mark.skipif(Literal is None, reason='typing.Literal could not be imported')
def test_literal(self):
def foo(a: Literal[1, 6, 8]):
assert check_argument_types()

foo(6)
pytest.raises(TypeError, foo, 4).match(r'must be one of \(1, 6, 8\); got 4 instead$')


class TestTypeChecked:
def test_typechecked(self):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_typeguard_py38.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Literal, TypedDict

import pytest

from typeguard import typechecked


def test_literal():
@typechecked
def foo(a: Literal[1, 6, 8]):
pass

foo(6)
pytest.raises(TypeError, foo, 4).match(r'must be one of \(1, 6, 8\); got 4 instead$')


@pytest.mark.parametrize('value, error_re', [
({'x': 6, 'y': 'foo'}, None),
({'y': 'foo'}, None),
({'y': 3}, 'type of item "y" for argument "arg" must be str; got int instead'),
({}, 'the required key "y" is missing for argument "arg"')
], ids=['correct', 'missing_x', 'wrong_type', 'missing_y'])
def test_typed_dict(value, error_re):
class DummyDict(TypedDict):
x: int = 0
y: str

@typechecked
def foo(arg: DummyDict):
pass

if error_re:
pytest.raises(TypeError, foo, value).match(error_re)
else:
foo(value)
17 changes: 15 additions & 2 deletions typeguard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
from weakref import WeakKeyDictionary, WeakValueDictionary

try:
from typing import Literal
from typing import Literal, TypedDict
except ImportError:
Literal = None
Literal = TypedDict = None

try:
from typing import AsyncGenerator
Expand All @@ -42,6 +42,7 @@ def isasyncgenfunction(func):

_type_hints_map = WeakKeyDictionary() # type: Dict[FunctionType, Dict[str, Any]]
_functions_map = WeakValueDictionary() # type: Dict[CodeType, FunctionType]
_missing = object()

T_CallableOrType = TypeVar('T_CallableOrType', Callable, Type[Any])

Expand Down Expand Up @@ -260,6 +261,16 @@ def check_dict(argname: str, value, expected_type, memo: Optional[_CallMemo]) ->
check_type('{}[{!r}]'.format(argname, k), v, value_type, memo)


def check_typed_dict(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
for key, argtype in expected_type.__annotations__.items():
argvalue = value.get(key, _missing)
if argvalue is not _missing:
check_type('dict item "{}" for {}'.format(key, argname), argvalue, argtype)
elif not hasattr(expected_type, key):
raise TypeError('the required key "{}" is missing for {}'
.format(key, argname)) from None


def check_list(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
if not isinstance(value, list):
raise TypeError('type of {} must be a list; got {} instead'.
Expand Down Expand Up @@ -517,6 +528,8 @@ def check_type(argname: str, value, expected_type, memo: Optional[_CallMemo] = N
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__'):
check_typed_dict(argname, value, expected_type, memo)
else:
expected_type = (getattr(expected_type, '__extra__', None) or origin_type or
expected_type)
Expand Down

0 comments on commit 5eebb4b

Please sign in to comment.