From 465bf009f30584c7206bdca1d5a9e8b3e82d050d Mon Sep 17 00:00:00 2001 From: Roy Williams Date: Thu, 29 Dec 2016 18:07:46 -0800 Subject: [PATCH 1/2] Allow fields on a TypedDict to be subtypes of their declared types. TypedDicts appear to have explicitly decided not to accept subtypes on fields, but this behavior is counter intuitive. This made it so TypedDicts didn't respect `Any` and caused problems with what should have been ducktype compatible. This also brings TypedDicts more in line with other container types and with how fields on classes behave. ```python from typing import Dict def foo() -> Dict[float, object]: return { 1: 32 } ``` This fixes https://github.com/python/mypy/issues/2610 --- mypy/subtypes.py | 2 +- test-data/unit/check-typeddict.test | 39 +++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 43f28c18d029..2b3dd387cc01 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -211,7 +211,7 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool: if not left.names_are_wider_than(right): return False for (_, l, r) in left.zip(right): - if not is_equivalent(l, r, self.check_type_parameter): + if not is_subtype(l, r, self.check_type_parameter): return False # (NOTE: Fallbacks don't matter.) return True diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 424c8b2b84e0..8b4eebfc0f3d 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -88,14 +88,6 @@ def convert(op: ObjectPoint) -> Point: return op # E: Incompatible return value type (got "ObjectPoint", expected "Point") [builtins fixtures/dict.pyi] -[case testCannotConvertTypedDictToSimilarTypedDictWithWiderItemTypes] -from mypy_extensions import TypedDict -Point = TypedDict('Point', {'x': int, 'y': int}) -ObjectPoint = TypedDict('ObjectPoint', {'x': object, 'y': object}) -def convert(p: Point) -> ObjectPoint: - return p # E: Incompatible return value type (got "Point", expected "ObjectPoint") -[builtins fixtures/dict.pyi] - [case testCannotConvertTypedDictToSimilarTypedDictWithIncompatibleItemTypes] from mypy_extensions import TypedDict Point = TypedDict('Point', {'x': int, 'y': int}) @@ -136,6 +128,37 @@ def as_mapping(p: Point) -> Mapping[str, str]: return p # E: Incompatible return value type (got "Point", expected Mapping[str, str]) [builtins fixtures/dict.pyi] +[case testTypedDictAcceptsIntForFloatDuckTypes] +from mypy_extensions import TypedDict +from typing import Any, Mapping +Point = TypedDict('Point', {'x': float, 'y': float}) +def create_point() -> Point: + return Point(x=1, y=2) +[builtins fixtures/dict.pyi] + +[case testTypedDictDoesNotAcceptsFloatForInt] +from mypy_extensions import TypedDict +from typing import Any, Mapping +Point = TypedDict('Point', {'x': int, 'y': int}) +def create_point() -> Point: + return Point(x=1.2, y=2.5) +[out] +main:5: error: Incompatible return value type (got "TypedDict(x=float, y=float)", expected "Point") +main:5: error: Incompatible types (expression has type "float", TypedDict item "x" has type "int") +main:5: error: Incompatible types (expression has type "float", TypedDict item "y" has type "int") +[builtins fixtures/dict.pyi] + +[case testTypedDictAcceptsAnyType] +from mypy_extensions import TypedDict +from typing import Any, Mapping +Point = TypedDict('Point', {'x': float, 'y': float}) +def create_point(something: Any) -> Point: + return Point({ + 'x': something.x, + 'y': something.y + }) +[builtins fixtures/dict.pyi] + -- TODO: Fix mypy stubs so that the following passes in the test suite --[case testCanConvertTypedDictToAnySuperclassOfMapping] --from mypy_extensions import TypedDict From 0ca0ae608b293c67c20db1a06180d6336fe29ee7 Mon Sep 17 00:00:00 2001 From: Roy Williams Date: Fri, 6 Jan 2017 15:29:32 -0800 Subject: [PATCH 2/2] Take suggestion from Jukka to have calls to typed dicts always result in exact declared type See https://github.com/python/mypy/pull/2621#issuecomment-270125052 --- mypy/checkexpr.py | 4 ++-- mypy/subtypes.py | 2 +- test-data/unit/check-typeddict.test | 17 ++++++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 22e582e4a993..84b9c2d07be3 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -246,12 +246,12 @@ def check_typeddict_call_with_kwargs(self, callee: TypedDictType, for (item_name, item_expected_type) in callee.items.items(): item_value = kwargs[item_name] - item_actual_type = self.chk.check_simple_assignment( + self.chk.check_simple_assignment( lvalue_type=item_expected_type, rvalue=item_value, context=item_value, msg=messages.INCOMPATIBLE_TYPES, lvalue_name='TypedDict item "{}"'.format(item_name), rvalue_name='expression') - items[item_name] = item_actual_type + items[item_name] = item_expected_type mapping_value_type = join.join_type_list(list(items.values())) fallback = self.chk.named_generic_type('typing.Mapping', diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2b3dd387cc01..43f28c18d029 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -211,7 +211,7 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool: if not left.names_are_wider_than(right): return False for (_, l, r) in left.zip(right): - if not is_subtype(l, r, self.check_type_parameter): + if not is_equivalent(l, r, self.check_type_parameter): return False # (NOTE: Fallbacks don't matter.) return True diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 8b4eebfc0f3d..20bca470ab2d 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -88,6 +88,14 @@ def convert(op: ObjectPoint) -> Point: return op # E: Incompatible return value type (got "ObjectPoint", expected "Point") [builtins fixtures/dict.pyi] +[case testCannotConvertTypedDictToSimilarTypedDictWithWiderItemTypes] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +ObjectPoint = TypedDict('ObjectPoint', {'x': object, 'y': object}) +def convert(p: Point) -> ObjectPoint: + return p # E: Incompatible return value type (got "Point", expected "ObjectPoint") +[builtins fixtures/dict.pyi] + [case testCannotConvertTypedDictToSimilarTypedDictWithIncompatibleItemTypes] from mypy_extensions import TypedDict Point = TypedDict('Point', {'x': int, 'y': int}) @@ -143,7 +151,6 @@ Point = TypedDict('Point', {'x': int, 'y': int}) def create_point() -> Point: return Point(x=1.2, y=2.5) [out] -main:5: error: Incompatible return value type (got "TypedDict(x=float, y=float)", expected "Point") main:5: error: Incompatible types (expression has type "float", TypedDict item "x" has type "int") main:5: error: Incompatible types (expression has type "float", TypedDict item "y" has type "int") [builtins fixtures/dict.pyi] @@ -214,9 +221,9 @@ CellWithObject = TypedDict('CellWithObject', {'value': object, 'meta': object}) c1 = CellWithInt(value=1, meta=42) c2 = CellWithObject(value=2, meta='turtle doves') joined_cells = [c1, c2] -reveal_type(c1) # E: Revealed type is 'TypedDict(value=builtins.int, meta=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' -reveal_type(c2) # E: Revealed type is 'TypedDict(value=builtins.int, meta=builtins.str, _fallback=typing.Mapping[builtins.str, builtins.object])' -reveal_type(joined_cells) # E: Revealed type is 'builtins.list[TypedDict(value=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])]' +reveal_type(c1) # E: Revealed type is 'TypedDict(value=builtins.object, meta=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.object])' +reveal_type(c2) # E: Revealed type is 'TypedDict(value=builtins.object, meta=builtins.object, _fallback=typing.Mapping[builtins.str, builtins.object])' +reveal_type(joined_cells) # E: Revealed type is 'builtins.list[TypedDict(value=builtins.object, _fallback=typing.Mapping[builtins.str, builtins.object])]' [builtins fixtures/dict.pyi] [case testJoinOfDisjointTypedDictsIsEmptyTypedDict] @@ -227,7 +234,7 @@ d1 = Point(x=0, y=0) d2 = Cell(value='pear tree') joined_dicts = [d1, d2] reveal_type(d1) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' -reveal_type(d2) # E: Revealed type is 'TypedDict(value=builtins.str, _fallback=typing.Mapping[builtins.str, builtins.str])' +reveal_type(d2) # E: Revealed type is 'TypedDict(value=builtins.object, _fallback=typing.Mapping[builtins.str, builtins.object])' reveal_type(joined_dicts) # E: Revealed type is 'builtins.list[TypedDict(_fallback=typing.Mapping[builtins.str, builtins.None])]' [builtins fixtures/dict.pyi]