Skip to content

Commit

Permalink
Change TypedDict fallback to Mapping[str, object] (#5933)
Browse files Browse the repository at this point in the history
The previous fallback mapping item type was join of the value
types, which is unsafe because of structural subtyping.

Fixes #5927.
  • Loading branch information
JukkaL authored Nov 22, 2018
1 parent 69d03ac commit 1428513
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 56 deletions.
10 changes: 0 additions & 10 deletions mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,16 +283,6 @@ def build_typeddict_typeinfo(self, name: str, items: List[str],
info = self.api.basic_new_typeinfo(name, fallback)
info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), required_keys,
fallback)

def patch() -> None:
# Calculate the correct value type for the fallback Mapping.
assert info.typeddict_type, "TypedDict type deleted before calling the patch"
fallback.args[1] = join.join_type_list(list(info.typeddict_type.items.values()))

# We can't calculate the complete fallback type until after semantic
# analysis, since otherwise MROs might be incomplete. Postpone a callback
# function that patches the fallback.
self.api.schedule_patch(PRIORITY_FALLBACKS, patch)
return info

# Helpers
Expand Down
4 changes: 3 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4514,7 +4514,9 @@ class Bar(TypedDict):

def foo(node: NodeType) -> int:
x = node
return x['x']
# TODO: This is incorrect (https://github.com/python/mypy/issues/5930), but ensure that it
# doesn't crash at least
return x['x'] # E: Incompatible return value type (got "object", expected "int")
[builtins fixtures/isinstancelist.pyi]
[out]

Expand Down
74 changes: 33 additions & 41 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Point = TypedDict('Point', {'x': int, 'y': int})
p = Point(x=42, y=1337)
reveal_type(p) # E: Revealed type is 'TypedDict('__main__.Point', {'x': builtins.int, 'y': builtins.int})'
# Use values() to check fallback value type.
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.int*]'
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]'
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

Expand All @@ -16,7 +16,7 @@ Point = TypedDict('Point', {'x': int, 'y': int})
p = Point(dict(x=42, y=1337))
reveal_type(p) # E: Revealed type is 'TypedDict('__main__.Point', {'x': builtins.int, 'y': builtins.int})'
# Use values() to check fallback value type.
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.int*]'
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]'
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

Expand All @@ -26,7 +26,7 @@ Point = TypedDict('Point', {'x': int, 'y': int})
p = Point({'x': 42, 'y': 1337})
reveal_type(p) # E: Revealed type is 'TypedDict('__main__.Point', {'x': builtins.int, 'y': builtins.int})'
# Use values() to check fallback value type.
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.int*]'
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]'
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

Expand All @@ -36,7 +36,7 @@ from mypy_extensions import TypedDict
EmptyDict = TypedDict('EmptyDict', {})
p = EmptyDict()
reveal_type(p) # E: Revealed type is 'TypedDict('__main__.EmptyDict', {})'
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[<nothing>]'
reveal_type(p.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]'
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

Expand Down Expand Up @@ -286,16 +286,16 @@ def widen(p: Point) -> Point3D:
from mypy_extensions import TypedDict
from typing import Mapping
Point = TypedDict('Point', {'x': int, 'y': int})
def as_mapping(p: Point) -> Mapping[str, int]:
def as_mapping(p: Point) -> Mapping[str, object]:
return p
[builtins fixtures/dict.pyi]

[case testCannotConvertTypedDictToCompatibleMapping]
[case testCannotConvertTypedDictToIncompatibleMapping]
from mypy_extensions import TypedDict
from typing import Mapping
Point = TypedDict('Point', {'x': int, 'y': int})
def as_mapping(p: Point) -> Mapping[str, str]:
return p # E: Incompatible return value type (got "Point", expected "Mapping[str, str]")
def as_mapping(p: Point) -> Mapping[str, int]:
return p # E: Incompatible return value type (got "Point", expected "Mapping[str, int]")
[builtins fixtures/dict.pyi]

[case testTypedDictAcceptsIntForFloatDuckTypes]
Expand Down Expand Up @@ -342,8 +342,8 @@ from typing import Dict, MutableMapping
Point = TypedDict('Point', {'x': int, 'y': int})
def as_dict(p: Point) -> Dict[str, int]:
return p # E: Incompatible return value type (got "Point", expected "Dict[str, int]")
def as_mutable_mapping(p: Point) -> MutableMapping[str, int]:
return p # E: Incompatible return value type (got "Point", expected "MutableMapping[str, int]")
def as_mutable_mapping(p: Point) -> MutableMapping[str, object]:
return p # E: Incompatible return value type (got "Point", expected "MutableMapping[str, object]")
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

Expand Down Expand Up @@ -377,25 +377,29 @@ f(ll) # E: Argument 1 to "f" has incompatible type "List[TypedDict({'x': int, 'z
from typing_extensions import Protocol
from mypy_extensions import TypedDict

class StrObjectMap(Protocol):
def __getitem__(self, key: str) -> object: ...
class StrIntMap(Protocol):
def __getitem__(self, key: str) -> int: ...

A = TypedDict('A', {'x': int, 'y': int})
B = TypedDict('B', {'x': int, 'y': str})

def fun(arg: StrIntMap) -> None: ...
def fun(arg: StrObjectMap) -> None: ...
def fun2(arg: StrIntMap) -> None: ...
a: A
b: B
fun(a)
fun(b) # Error
fun(b)
fun2(a) # Error
[builtins fixtures/dict.pyi]
[out]
main:14: error: Argument 1 to "fun" has incompatible type "B"; expected "StrIntMap"
main:14: note: Following member(s) of "B" have conflicts:
main:14: note: Expected:
main:14: note: def __getitem__(self, str) -> int
main:14: note: Got:
main:14: note: def __getitem__(self, str) -> object
main:18: error: Argument 1 to "fun2" has incompatible type "A"; expected "StrIntMap"
main:18: note: Following member(s) of "A" have conflicts:
main:18: note: Expected:
main:18: note: def __getitem__(self, str) -> int
main:18: note: Got:
main:18: note: def __getitem__(self, str) -> object

[case testTypedDictWithSimpleProtocolInference]
from typing_extensions import Protocol
Expand All @@ -415,7 +419,7 @@ def fun(arg: StrMap[T]) -> T:
return arg['whatever']
a: A
b: B
reveal_type(fun(a)) # E: Revealed type is 'builtins.int*'
reveal_type(fun(a)) # E: Revealed type is 'builtins.object*'
reveal_type(fun(b)) # E: Revealed type is 'builtins.object*'
[builtins fixtures/dict.pyi]
[out]
Expand All @@ -430,7 +434,7 @@ p1 = TaggedPoint(type='2d', x=0, y=0)
p2 = Point3D(x=1, y=1, z=1)
joined_points = [p1, p2][0]
reveal_type(p1.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]'
reveal_type(p2.values()) # E: Revealed type is 'typing.Iterable[builtins.int*]'
reveal_type(p2.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]'
reveal_type(joined_points) # E: Revealed type is 'TypedDict({'x': builtins.int, 'y': builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])'
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]
Expand Down Expand Up @@ -467,8 +471,8 @@ left = Cell(value=42)
right = {'score': 999} # type: Mapping[str, int]
joined1 = [left, right]
joined2 = [right, left]
reveal_type(joined1) # E: Revealed type is 'builtins.list[typing.Mapping*[builtins.str, builtins.int]]'
reveal_type(joined2) # E: Revealed type is 'builtins.list[typing.Mapping*[builtins.str, builtins.int]]'
reveal_type(joined1) # E: Revealed type is 'builtins.list[typing.Mapping*[builtins.str, builtins.object]]'
reveal_type(joined2) # E: Revealed type is 'builtins.list[typing.Mapping*[builtins.str, builtins.object]]'
[builtins fixtures/dict.pyi]

[case testJoinOfTypedDictWithCompatibleMappingSupertypeIsSupertype]
Expand All @@ -484,18 +488,6 @@ reveal_type(joined2) # E: Revealed type is 'builtins.list[typing.Sized*]'
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

[case testJoinOfTypedDictWithIncompatibleMappingIsObject]
from mypy_extensions import TypedDict
from typing import Mapping
Cell = TypedDict('Cell', {'value': int})
left = Cell(value=42)
right = {'score': 'zero'} # type: Mapping[str, str]
joined1 = [left, right]
joined2 = [right, left]
reveal_type(joined1) # E: Revealed type is 'builtins.list[builtins.object*]'
reveal_type(joined2) # E: Revealed type is 'builtins.list[builtins.object*]'
[builtins fixtures/dict.pyi]

[case testJoinOfTypedDictWithIncompatibleTypeIsObject]
from mypy_extensions import TypedDict
from typing import Mapping
Expand Down Expand Up @@ -795,13 +787,13 @@ def u(x: T, y: S) -> Union[S, T]: pass
C = TypedDict('C', {'a': int, 'b': int})

c = C(a=1, b=1)
m_s_i: Mapping[str, int]
m_s_o: Mapping[str, object]
m_s_s: Mapping[str, str]
m_i_i: Mapping[int, int]
m_s_a: Mapping[str, Any]

reveal_type(u(c, m_s_i)) # E: Revealed type is 'typing.Mapping*[builtins.str, builtins.int]'
reveal_type(u(m_s_i, c)) # E: Revealed type is 'typing.Mapping*[builtins.str, builtins.int]'
reveal_type(u(c, m_s_o)) # E: Revealed type is 'typing.Mapping*[builtins.str, builtins.object]'
reveal_type(u(m_s_o, c)) # E: Revealed type is 'typing.Mapping*[builtins.str, builtins.object]'
reveal_type(u(c, m_s_s)) # E: Revealed type is 'Union[typing.Mapping*[builtins.str, builtins.str], TypedDict('__main__.C', {'a': builtins.int, 'b': builtins.int})]'
reveal_type(u(c, m_i_i)) # E: Revealed type is 'Union[typing.Mapping*[builtins.int, builtins.int], TypedDict('__main__.C', {'a': builtins.int, 'b': builtins.int})]'
reveal_type(u(c, m_s_a)) # E: Revealed type is 'Union[typing.Mapping*[builtins.str, Any], TypedDict('__main__.C', {'a': builtins.int, 'b': builtins.int})]'
Expand Down Expand Up @@ -1297,8 +1289,8 @@ class B: pass
class C(B): pass
x: X
reveal_type(x) # E: Revealed type is 'TypedDict('__main__.X', {'b': __main__.B, 'c': __main__.C})'
m1: Mapping[str, B] = x
m2: Mapping[str, C] = x # E: Incompatible types in assignment (expression has type "X", variable has type "Mapping[str, C]")
m1: Mapping[str, object] = x
m2: Mapping[str, B] = x # E: Incompatible types in assignment (expression has type "X", variable has type "Mapping[str, B]")
[builtins fixtures/dict.pyi]

[case testForwardReferenceInClassTypedDict]
Expand All @@ -1311,8 +1303,8 @@ class B: pass
class C(B): pass
x: X
reveal_type(x) # E: Revealed type is 'TypedDict('__main__.X', {'b': __main__.B, 'c': __main__.C})'
m1: Mapping[str, B] = x
m2: Mapping[str, C] = x # E: Incompatible types in assignment (expression has type "X", variable has type "Mapping[str, C]")
m1: Mapping[str, object] = x
m2: Mapping[str, B] = x # E: Incompatible types in assignment (expression has type "X", variable has type "Mapping[str, B]")
[builtins fixtures/dict.pyi]

[case testForwardReferenceToTypedDictInTypedDict]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/deps-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ class I: pass
<m.P> -> m.P
<mod.I.__init__> -> m
<mod.I.__new__> -> m
<mod.I> -> <m.P>, m, m.P
<mod.I> -> <m.P>, m
<mypy_extensions.TypedDict> -> m

[case testAliasDepsTypedDictFunctional]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1072,8 +1072,8 @@ _testTypedDictMappingMethods.py:6: error: Revealed type is 'typing.Iterator[buil
_testTypedDictMappingMethods.py:7: error: Revealed type is 'builtins.int'
_testTypedDictMappingMethods.py:8: error: Revealed type is 'builtins.bool'
_testTypedDictMappingMethods.py:9: error: Revealed type is 'typing.AbstractSet[builtins.str*]'
_testTypedDictMappingMethods.py:10: error: Revealed type is 'typing.AbstractSet[Tuple[builtins.str*, builtins.int*]]'
_testTypedDictMappingMethods.py:11: error: Revealed type is 'typing.ValuesView[builtins.int*]'
_testTypedDictMappingMethods.py:10: error: Revealed type is 'typing.AbstractSet[Tuple[builtins.str*, builtins.object*]]'
_testTypedDictMappingMethods.py:11: error: Revealed type is 'typing.ValuesView[builtins.object*]'

[case testCrashOnComplexCheckWithNamedTupleNext]
from typing import NamedTuple
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/semanal-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ MypyFile:1(
ClassDef:2(
A
BaseType(
typing.Mapping[builtins.str, builtins.str])
typing.Mapping[builtins.str, builtins.object])
ExpressionStmt:3(
StrExpr(foo))
AssignmentStmt:4(
Expand Down

0 comments on commit 1428513

Please sign in to comment.