Skip to content

Commit 541639e

Browse files
authored
Make dict expression inference more consistent (#15174)
Fixes #12977 IMO current dict expression inference logic is quite arbitrary: we only take the non-star items to infer resulting type, then enforce it on the remaining (star) items. In this PR I simplify the logic to simply put all expressions as arguments into the same call. This has following benefits: * Makes everything more consistent/predictable. * Fixes one of top upvoted bugs * Makes dict item indexes correct (previously we reshuffled them causing wrong indexes for non-star items after star items) * No more weird wordings like `List item <n>` or `Argument <n> to "update" of "dict"` * I also fix the end position of generated expressions to show correct spans in errors The only downside is that we will see `Cannot infer type argument` error instead of `Incompatible type` more often. This is because `SupportsKeysAndGetItem` (used for star items) is invariant in key type. I think this is fine however, since: * This only affects key types, that are mixed much less often than value types (they are usually just strings), and for latter we use joins. * I added a dedicated note for this case
1 parent fea5c93 commit 541639e

File tree

7 files changed

+89
-65
lines changed

7 files changed

+89
-65
lines changed

mypy/checkexpr.py

Lines changed: 26 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4319,12 +4319,19 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
43194319
if dt:
43204320
return dt
43214321

4322+
# Define type variables (used in constructors below).
4323+
kt = TypeVarType("KT", "KT", -1, [], self.object_type())
4324+
vt = TypeVarType("VT", "VT", -2, [], self.object_type())
4325+
43224326
# Collect function arguments, watching out for **expr.
4323-
args: list[Expression] = [] # Regular "key: value"
4324-
stargs: list[Expression] = [] # For "**expr"
4327+
args: list[Expression] = []
4328+
expected_types: list[Type] = []
43254329
for key, value in e.items:
43264330
if key is None:
4327-
stargs.append(value)
4331+
args.append(value)
4332+
expected_types.append(
4333+
self.chk.named_generic_type("_typeshed.SupportsKeysAndGetItem", [kt, vt])
4334+
)
43284335
else:
43294336
tup = TupleExpr([key, value])
43304337
if key.line >= 0:
@@ -4333,52 +4340,23 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
43334340
else:
43344341
tup.line = value.line
43354342
tup.column = value.column
4343+
tup.end_line = value.end_line
4344+
tup.end_column = value.end_column
43364345
args.append(tup)
4337-
# Define type variables (used in constructors below).
4338-
kt = TypeVarType("KT", "KT", -1, [], self.object_type())
4339-
vt = TypeVarType("VT", "VT", -2, [], self.object_type())
4340-
rv = None
4341-
# Call dict(*args), unless it's empty and stargs is not.
4342-
if args or not stargs:
4343-
# The callable type represents a function like this:
4344-
#
4345-
# def <unnamed>(*v: Tuple[kt, vt]) -> Dict[kt, vt]: ...
4346-
constructor = CallableType(
4347-
[TupleType([kt, vt], self.named_type("builtins.tuple"))],
4348-
[nodes.ARG_STAR],
4349-
[None],
4350-
self.chk.named_generic_type("builtins.dict", [kt, vt]),
4351-
self.named_type("builtins.function"),
4352-
name="<dict>",
4353-
variables=[kt, vt],
4354-
)
4355-
rv = self.check_call(constructor, args, [nodes.ARG_POS] * len(args), e)[0]
4356-
else:
4357-
# dict(...) will be called below.
4358-
pass
4359-
# Call rv.update(arg) for each arg in **stargs,
4360-
# except if rv isn't set yet, then set rv = dict(arg).
4361-
if stargs:
4362-
for arg in stargs:
4363-
if rv is None:
4364-
constructor = CallableType(
4365-
[
4366-
self.chk.named_generic_type(
4367-
"_typeshed.SupportsKeysAndGetItem", [kt, vt]
4368-
)
4369-
],
4370-
[nodes.ARG_POS],
4371-
[None],
4372-
self.chk.named_generic_type("builtins.dict", [kt, vt]),
4373-
self.named_type("builtins.function"),
4374-
name="<list>",
4375-
variables=[kt, vt],
4376-
)
4377-
rv = self.check_call(constructor, [arg], [nodes.ARG_POS], arg)[0]
4378-
else:
4379-
self.check_method_call_by_name("update", rv, [arg], [nodes.ARG_POS], arg)
4380-
assert rv is not None
4381-
return rv
4346+
expected_types.append(TupleType([kt, vt], self.named_type("builtins.tuple")))
4347+
4348+
# The callable type represents a function like this (except we adjust for **expr):
4349+
# def <dict>(*v: Tuple[kt, vt]) -> Dict[kt, vt]: ...
4350+
constructor = CallableType(
4351+
expected_types,
4352+
[nodes.ARG_POS] * len(expected_types),
4353+
[None] * len(expected_types),
4354+
self.chk.named_generic_type("builtins.dict", [kt, vt]),
4355+
self.named_type("builtins.function"),
4356+
name="<dict>",
4357+
variables=[kt, vt],
4358+
)
4359+
return self.check_call(constructor, args, [nodes.ARG_POS] * len(args), e)[0]
43824360

43834361
def find_typeddict_context(
43844362
self, context: Type | None, dict_expr: DictExpr

mypy/messages.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -679,11 +679,13 @@ def incompatible_argument(
679679
name.title(), n, actual_type_str, expected_type_str
680680
)
681681
code = codes.LIST_ITEM
682-
elif callee_name == "<dict>":
682+
elif callee_name == "<dict>" and isinstance(
683+
get_proper_type(callee.arg_types[n - 1]), TupleType
684+
):
683685
name = callee_name[1:-1]
684686
n -= 1
685687
key_type, value_type = cast(TupleType, arg_type).items
686-
expected_key_type, expected_value_type = cast(TupleType, callee.arg_types[0]).items
688+
expected_key_type, expected_value_type = cast(TupleType, callee.arg_types[n]).items
687689

688690
# don't increase verbosity unless there is need to do so
689691
if is_subtype(key_type, expected_key_type):
@@ -710,6 +712,14 @@ def incompatible_argument(
710712
expected_value_type_str,
711713
)
712714
code = codes.DICT_ITEM
715+
elif callee_name == "<dict>":
716+
value_type_str, expected_value_type_str = format_type_distinctly(
717+
arg_type, callee.arg_types[n - 1], options=self.options
718+
)
719+
msg = "Unpacked dict entry {} has incompatible type {}; expected {}".format(
720+
n - 1, value_type_str, expected_value_type_str
721+
)
722+
code = codes.DICT_ITEM
713723
elif callee_name == "<list-comprehension>":
714724
actual_type_str, expected_type_str = map(
715725
strip_quotes,
@@ -1301,6 +1311,12 @@ def could_not_infer_type_arguments(
13011311
callee_name = callable_name(callee_type)
13021312
if callee_name is not None and n > 0:
13031313
self.fail(f"Cannot infer type argument {n} of {callee_name}", context)
1314+
if callee_name == "<dict>":
1315+
# Invariance in key type causes more of these errors than we would want.
1316+
self.note(
1317+
"Try assigning the literal to a variable annotated as dict[<key>, <val>]",
1318+
context,
1319+
)
13041320
else:
13051321
self.fail("Cannot infer function type argument", context)
13061322

test-data/unit/check-expressions.test

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,11 +1800,12 @@ a = {'a': 1}
18001800
b = {'z': 26, **a}
18011801
c = {**b}
18021802
d = {**a, **b, 'c': 3}
1803-
e = {1: 'a', **a} # E: Argument 1 to "update" of "dict" has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, str]"
1804-
f = {**b} # type: Dict[int, int] # E: List item 0 has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, int]"
1803+
e = {1: 'a', **a} # E: Cannot infer type argument 1 of <dict> \
1804+
# N: Try assigning the literal to a variable annotated as dict[<key>, <val>]
1805+
f = {**b} # type: Dict[int, int] # E: Unpacked dict entry 0 has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, int]"
18051806
g = {**Thing()}
18061807
h = {**a, **Thing()}
1807-
i = {**Thing()} # type: Dict[int, int] # E: List item 0 has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, int]" \
1808+
i = {**Thing()} # type: Dict[int, int] # E: Unpacked dict entry 0 has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, int]" \
18081809
# N: Following member(s) of "Thing" have conflicts: \
18091810
# N: Expected: \
18101811
# N: def __getitem__(self, int, /) -> int \
@@ -1814,16 +1815,8 @@ i = {**Thing()} # type: Dict[int, int] # E: List item 0 has incompatible type
18141815
# N: def keys(self) -> Iterable[int] \
18151816
# N: Got: \
18161817
# N: def keys(self) -> Iterable[str]
1817-
j = {1: 'a', **Thing()} # E: Argument 1 to "update" of "dict" has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, str]" \
1818-
# N: Following member(s) of "Thing" have conflicts: \
1819-
# N: Expected: \
1820-
# N: def __getitem__(self, int, /) -> str \
1821-
# N: Got: \
1822-
# N: def __getitem__(self, str, /) -> int \
1823-
# N: Expected: \
1824-
# N: def keys(self) -> Iterable[int] \
1825-
# N: Got: \
1826-
# N: def keys(self) -> Iterable[str]
1818+
j = {1: 'a', **Thing()} # E: Cannot infer type argument 1 of <dict> \
1819+
# N: Try assigning the literal to a variable annotated as dict[<key>, <val>]
18271820
[builtins fixtures/dict.pyi]
18281821
[typing fixtures/typing-medium.pyi]
18291822

test-data/unit/check-generics.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,3 +2711,25 @@ class G(Generic[T]):
27112711
def g(self, x: S) -> Union[S, T]: ...
27122712

27132713
f(lambda x: x.g(0)) # E: Cannot infer type argument 1 of "f"
2714+
2715+
[case testDictStarInference]
2716+
class B: ...
2717+
class C1(B): ...
2718+
class C2(B): ...
2719+
2720+
dict1 = {"a": C1()}
2721+
dict2 = {"a": C2(), **dict1}
2722+
reveal_type(dict2) # N: Revealed type is "builtins.dict[builtins.str, __main__.B]"
2723+
[builtins fixtures/dict.pyi]
2724+
2725+
[case testDictStarAnyKeyJoinValue]
2726+
from typing import Any
2727+
2728+
class B: ...
2729+
class C1(B): ...
2730+
class C2(B): ...
2731+
2732+
dict1: Any
2733+
dict2 = {"a": C1(), **{x: C2() for x in dict1}}
2734+
reveal_type(dict2) # N: Revealed type is "builtins.dict[Any, __main__.B]"
2735+
[builtins fixtures/dict.pyi]

test-data/unit/check-python38.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,3 +812,18 @@ if sys.version_info < (3, 6):
812812
else:
813813
42 # type: ignore # E: Unused "type: ignore" comment
814814
[builtins fixtures/ops.pyi]
815+
816+
[case testDictExpressionErrorLocations]
817+
# flags: --pretty
818+
from typing import Dict
819+
820+
other: Dict[str, str]
821+
dct: Dict[str, int] = {"a": "b", **other}
822+
[builtins fixtures/dict.pyi]
823+
[out]
824+
main:5: error: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"
825+
dct: Dict[str, int] = {"a": "b", **other}
826+
^~~~~~~~
827+
main:5: error: Unpacked dict entry 1 has incompatible type "Dict[str, str]"; expected "SupportsKeysAndGetItem[str, int]"
828+
dct: Dict[str, int] = {"a": "b", **other}
829+
^~~~~

test-data/unit/fine-grained.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7546,7 +7546,7 @@ def d() -> Dict[int, int]: pass
75467546
[builtins fixtures/dict.pyi]
75477547
[out]
75487548
==
7549-
main:5: error: Argument 1 to "update" of "dict" has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]"
7549+
main:5: error: Unpacked dict entry 1 has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]"
75507550

75517551
[case testAwaitAndAsyncDef-only_when_nocache]
75527552
from a import g

test-data/unit/pythoneval.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1350,7 +1350,7 @@ def f() -> Dict[int, str]:
13501350
def d() -> Dict[int, int]:
13511351
return {}
13521352
[out]
1353-
_testDictWithStarStarSpecialCase.py:4: error: Argument 1 to "update" of "MutableMapping" has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]"
1353+
_testDictWithStarStarSpecialCase.py:4: error: Unpacked dict entry 1 has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]"
13541354

13551355
[case testLoadsOfOverloads]
13561356
from typing import overload, Any, TypeVar, Iterable, List, Dict, Callable, Union

0 commit comments

Comments
 (0)