From 5f1bbdd9565e7ae514912e81080db12a27a4a485 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 3 May 2023 09:14:41 +0100 Subject: [PATCH 1/3] Make dict expression inference more consistent --- mypy/checkexpr.py | 74 ++++++++++----------------- mypy/messages.py | 20 +++++++- test-data/unit/check-expressions.test | 19 +++---- test-data/unit/check-generics.test | 10 ++++ test-data/unit/check-python38.test | 15 ++++++ test-data/unit/fine-grained.test | 2 +- test-data/unit/pythoneval.test | 2 +- 7 files changed, 77 insertions(+), 65 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index fccbad7bb87e1..fce43fb686692 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4319,12 +4319,19 @@ def visit_dict_expr(self, e: DictExpr) -> Type: if dt: return dt + # Define type variables (used in constructors below). + kt = TypeVarType("KT", "KT", -1, [], self.object_type()) + vt = TypeVarType("VT", "VT", -2, [], self.object_type()) + # Collect function arguments, watching out for **expr. - args: list[Expression] = [] # Regular "key: value" - stargs: list[Expression] = [] # For "**expr" + args: list[Expression] = [] + expected_types: list[Type] = [] for key, value in e.items: if key is None: - stargs.append(value) + args.append(value) + expected_types.append( + self.chk.named_generic_type("_typeshed.SupportsKeysAndGetItem", [kt, vt]) + ) else: tup = TupleExpr([key, value]) if key.line >= 0: @@ -4333,52 +4340,23 @@ def visit_dict_expr(self, e: DictExpr) -> Type: else: tup.line = value.line tup.column = value.column + tup.end_line = value.end_line + tup.end_column = value.end_column args.append(tup) - # Define type variables (used in constructors below). - kt = TypeVarType("KT", "KT", -1, [], self.object_type()) - vt = TypeVarType("VT", "VT", -2, [], self.object_type()) - rv = None - # Call dict(*args), unless it's empty and stargs is not. - if args or not stargs: - # The callable type represents a function like this: - # - # def (*v: Tuple[kt, vt]) -> Dict[kt, vt]: ... - constructor = CallableType( - [TupleType([kt, vt], self.named_type("builtins.tuple"))], - [nodes.ARG_STAR], - [None], - self.chk.named_generic_type("builtins.dict", [kt, vt]), - self.named_type("builtins.function"), - name="", - variables=[kt, vt], - ) - rv = self.check_call(constructor, args, [nodes.ARG_POS] * len(args), e)[0] - else: - # dict(...) will be called below. - pass - # Call rv.update(arg) for each arg in **stargs, - # except if rv isn't set yet, then set rv = dict(arg). - if stargs: - for arg in stargs: - if rv is None: - constructor = CallableType( - [ - self.chk.named_generic_type( - "_typeshed.SupportsKeysAndGetItem", [kt, vt] - ) - ], - [nodes.ARG_POS], - [None], - self.chk.named_generic_type("builtins.dict", [kt, vt]), - self.named_type("builtins.function"), - name="", - variables=[kt, vt], - ) - rv = self.check_call(constructor, [arg], [nodes.ARG_POS], arg)[0] - else: - self.check_method_call_by_name("update", rv, [arg], [nodes.ARG_POS], arg) - assert rv is not None - return rv + expected_types.append(TupleType([kt, vt], self.named_type("builtins.tuple"))) + + # The callable type represents a function like this (except we adjust for **expr): + # def (*v: Tuple[kt, vt]) -> Dict[kt, vt]: ... + constructor = CallableType( + expected_types, + [nodes.ARG_POS] * len(expected_types), + [None] * len(expected_types), + self.chk.named_generic_type("builtins.dict", [kt, vt]), + self.named_type("builtins.function"), + name="", + variables=[kt, vt], + ) + return self.check_call(constructor, args, [nodes.ARG_POS] * len(args), e)[0] def find_typeddict_context( self, context: Type | None, dict_expr: DictExpr diff --git a/mypy/messages.py b/mypy/messages.py index eb0b5448efdfb..486c5dd9593c2 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -679,11 +679,13 @@ def incompatible_argument( name.title(), n, actual_type_str, expected_type_str ) code = codes.LIST_ITEM - elif callee_name == "": + elif callee_name == "" and isinstance( + get_proper_type(callee.arg_types[n - 1]), TupleType + ): name = callee_name[1:-1] n -= 1 key_type, value_type = cast(TupleType, arg_type).items - expected_key_type, expected_value_type = cast(TupleType, callee.arg_types[0]).items + expected_key_type, expected_value_type = cast(TupleType, callee.arg_types[n]).items # don't increase verbosity unless there is need to do so if is_subtype(key_type, expected_key_type): @@ -710,6 +712,14 @@ def incompatible_argument( expected_value_type_str, ) code = codes.DICT_ITEM + elif callee_name == "": + value_type_str, expected_value_type_str = format_type_distinctly( + arg_type, callee.arg_types[n - 1], options=self.options + ) + msg = "Unpacked dict entry {} has incompatible type {}; expected {}".format( + n - 1, value_type_str, expected_value_type_str + ) + code = codes.DICT_ITEM elif callee_name == "": actual_type_str, expected_type_str = map( strip_quotes, @@ -1301,6 +1311,12 @@ def could_not_infer_type_arguments( callee_name = callable_name(callee_type) if callee_name is not None and n > 0: self.fail(f"Cannot infer type argument {n} of {callee_name}", context) + if callee_name == "": + # Invariance in key type causes more of these errors than we would want. + self.note( + "Try assigning the literal to a variable annotated as dict[, ]", + context, + ) else: self.fail("Cannot infer function type argument", context) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 43bf28e519c6b..1fa551f6a2e4e 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1800,11 +1800,12 @@ a = {'a': 1} b = {'z': 26, **a} c = {**b} d = {**a, **b, 'c': 3} -e = {1: 'a', **a} # E: Argument 1 to "update" of "dict" has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, str]" -f = {**b} # type: Dict[int, int] # E: List item 0 has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, int]" +e = {1: 'a', **a} # E: Cannot infer type argument 1 of \ + # N: Try assigning the literal to a variable annotated as dict[, ] +f = {**b} # type: Dict[int, int] # E: Unpacked dict entry 0 has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, int]" g = {**Thing()} h = {**a, **Thing()} -i = {**Thing()} # type: Dict[int, int] # E: List item 0 has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, int]" \ +i = {**Thing()} # type: Dict[int, int] # E: Unpacked dict entry 0 has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, int]" \ # N: Following member(s) of "Thing" have conflicts: \ # N: Expected: \ # N: def __getitem__(self, int, /) -> int \ @@ -1814,16 +1815,8 @@ i = {**Thing()} # type: Dict[int, int] # E: List item 0 has incompatible type # N: def keys(self) -> Iterable[int] \ # N: Got: \ # N: def keys(self) -> Iterable[str] -j = {1: 'a', **Thing()} # E: Argument 1 to "update" of "dict" has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, str]" \ - # N: Following member(s) of "Thing" have conflicts: \ - # N: Expected: \ - # N: def __getitem__(self, int, /) -> str \ - # N: Got: \ - # N: def __getitem__(self, str, /) -> int \ - # N: Expected: \ - # N: def keys(self) -> Iterable[int] \ - # N: Got: \ - # N: def keys(self) -> Iterable[str] +j = {1: 'a', **Thing()} # E: Cannot infer type argument 1 of \ + # N: Try assigning the literal to a variable annotated as dict[, ] [builtins fixtures/dict.pyi] [typing fixtures/typing-medium.pyi] diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 79df350f810e7..ee1a35f93efb5 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2711,3 +2711,13 @@ class G(Generic[T]): def g(self, x: S) -> Union[S, T]: ... f(lambda x: x.g(0)) # E: Cannot infer type argument 1 of "f" + +[case testDictStarInference] +class B: ... +class C1(B): ... +class C2(B): ... + +dict1 = {"a": C1()} +dict2 = {"a": C2(), **dict1} +reveal_type(dict2) # N: Revealed type is "builtins.dict[builtins.str, __main__.B]" +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test index bb1768d1d17d3..550c328b3b78a 100644 --- a/test-data/unit/check-python38.test +++ b/test-data/unit/check-python38.test @@ -790,3 +790,18 @@ if sys.version_info < (3, 6): else: 42 # type: ignore # E: Unused "type: ignore" comment [builtins fixtures/ops.pyi] + +[case testDictExpressionErrorLocations] +# flags: --pretty +from typing import Dict + +other: Dict[str, str] +dct: Dict[str, int] = {"a": "b", **other} +[builtins fixtures/dict.pyi] +[out] +main:5: error: Dict entry 0 has incompatible type "str": "str"; expected "str": "int" + dct: Dict[str, int] = {"a": "b", **other} + ^~~~~~~~ +main:5: error: Unpacked dict entry 1 has incompatible type "Dict[str, str]"; expected "SupportsKeysAndGetItem[str, int]" + dct: Dict[str, int] = {"a": "b", **other} + ^~~~~ diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 7e20a1fb688ea..1e8b9c87a5fb6 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7546,7 +7546,7 @@ def d() -> Dict[int, int]: pass [builtins fixtures/dict.pyi] [out] == -main:5: error: Argument 1 to "update" of "dict" has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]" +main:5: error: Argument 2 to has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]" [case testAwaitAndAsyncDef-only_when_nocache] from a import g diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 915d9b4921a21..034c2190dd5e3 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1350,7 +1350,7 @@ def f() -> Dict[int, str]: def d() -> Dict[int, int]: return {} [out] -_testDictWithStarStarSpecialCase.py:4: error: Argument 1 to "update" of "MutableMapping" has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]" +_testDictWithStarStarSpecialCase.py:4: error: Unpacked dict entry 1 has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]" [case testLoadsOfOverloads] from typing import overload, Any, TypeVar, Iterable, List, Dict, Callable, Union From ef21fdd67c4e69de80ff45339a09c744e8df1f7c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 3 May 2023 09:31:06 +0100 Subject: [PATCH 2/3] Update error message --- test-data/unit/fine-grained.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 1e8b9c87a5fb6..88a11be31f340 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7546,7 +7546,7 @@ def d() -> Dict[int, int]: pass [builtins fixtures/dict.pyi] [out] == -main:5: error: Argument 2 to has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]" +main:5: error: Unpacked dict entry 1 has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]" [case testAwaitAndAsyncDef-only_when_nocache] from a import g From 4cf10e82567194bca3ff852b5d2c8c713eb4a369 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 4 May 2023 23:51:51 +0100 Subject: [PATCH 3/3] Add a test case with Any --- test-data/unit/check-generics.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index ee1a35f93efb5..06b80be85096b 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2721,3 +2721,15 @@ dict1 = {"a": C1()} dict2 = {"a": C2(), **dict1} reveal_type(dict2) # N: Revealed type is "builtins.dict[builtins.str, __main__.B]" [builtins fixtures/dict.pyi] + +[case testDictStarAnyKeyJoinValue] +from typing import Any + +class B: ... +class C1(B): ... +class C2(B): ... + +dict1: Any +dict2 = {"a": C1(), **{x: C2() for x in dict1}} +reveal_type(dict2) # N: Revealed type is "builtins.dict[Any, __main__.B]" +[builtins fixtures/dict.pyi]