diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 728915622d71..cec2ef86b94b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3334,13 +3334,14 @@ def check_lst_expr(self, items: List[Expression], fullname: str, self.chk.named_generic_type(fullname, [tv]), self.named_type('builtins.function'), name=tag, - variables=[tv]) - out = self.check_call(constructor, - [(i.expr if isinstance(i, StarExpr) else i) - for i in items], - [(nodes.ARG_STAR if isinstance(i, StarExpr) else nodes.ARG_POS) - for i in items], - context)[0] + variables=[tv], + ) + out = self.check_call( + constructor, + [(i.expr if isinstance(i, StarExpr) else i) for i in items], + [(nodes.ARG_STAR if isinstance(i, StarExpr) else nodes.ARG_POS) for i in items], + context, + )[0] return remove_instance_last_known_values(out) def visit_tuple_expr(self, e: TupleExpr) -> Type: @@ -3503,7 +3504,7 @@ def visit_dict_expr(self, e: DictExpr) -> Type: else: # dict(...) will be called below. pass - # Call rv.update(arg) for each arg in **stargs, + # Re-infer types for each `**` unpack in dict args, # except if rv isn't set yet, then set rv = dict(arg). if stargs: for arg in stargs: @@ -3518,10 +3519,68 @@ def visit_dict_expr(self, e: DictExpr) -> Type: 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) + rv = self.dict_unpack(rv, arg.accept(self), arg) assert rv is not None return rv + def dict_unpack(self, dict_obj: Type, other: Type, context: Context) -> ProperType: + """Joins two dict-like objects. + + For example, `dict[str, int]` and `dict[int, int]` + will become `dict[object, int]`. + """ + dict_obj = get_proper_type(dict_obj) + + # Since this function is only used when two dicts are joined like: + # `{**other, 'a': 1}`, we require `dict_obj` to be an `Instance` + # of `builtins.dict``. This should not happen, unless someone else + # will call this function from other places. + assert isinstance(dict_obj, Instance) + assert dict_obj.type.fullname == 'builtins.dict' + assert len(dict_obj.args) == 2 + + key_type, value_type = dict_obj.args + new_key_type, new_value_type = self.extract_key_value_types(other, context) + return dict_obj.copy_modified(args=[ + join.join_types(key_type, new_key_type), + join.join_types(value_type, new_value_type), + ]) + + def extract_key_value_types(self, typ: Type, context: Context) -> Tuple[Type, Type]: + mapping_type = self.named_type('typing.Mapping') + + typ = get_proper_type(typ) + if (isinstance(typ, Instance) + and is_subtype(typ, mapping_type, ignore_type_params=True)): + mapping = map_instance_to_supertype(typ, mapping_type.type) + if len(mapping.args) >= 2: + return mapping.args[0], mapping.args[1] + elif isinstance(typ, TypedDictType): + return ( + self.named_type('builtins.str'), + join.join_type_list(list(typ.items.values())), + ) + elif isinstance(typ, UnionType): + keys = [] + values = [] + for item in typ.relevant_items(): + key, value = self.extract_key_value_types(item, context) + keys.append(key) + values.append(value) + return join.join_type_list(keys), join.join_type_list(values) + elif isinstance(typ, AnyType): + return ( + AnyType(TypeOfAny.implementation_artifact), + AnyType(TypeOfAny.implementation_artifact), + ) + + # All other types are not allowed to be unpacked. + self.chk.fail('Cannot unpack "{}" type with "**"'.format(typ), context) + return ( + AnyType(TypeOfAny.from_error), + AnyType(TypeOfAny.from_error), + ) + def find_typeddict_context(self, context: Optional[Type], dict_expr: DictExpr) -> Optional[TypedDictType]: context = get_proper_type(context) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 70fda50d617e..9aca7072de69 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1781,11 +1781,170 @@ 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 "Mapping[int, str]" +e = {1: 'a', **a} f = {**b} # type: Dict[int, int] # E: List item 0 has incompatible type "Dict[str, int]"; expected "Mapping[int, int]" [builtins fixtures/dict.pyi] [typing fixtures/typing-medium.pyi] +[case testDictExprWithStarAdvanced] +# Same type: +foo = {1: "a"} + +reveal_type({**foo}) # N: Revealed type is "builtins.dict[builtins.int*, builtins.str*]" +reveal_type({**foo, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.int, builtins.str]" +reveal_type({**foo, 2: 'b', 3: 'c'}) # N: Revealed type is "builtins.dict[builtins.int*, builtins.str*]" +reveal_type({2: 'b', **foo}) # N: Revealed type is "builtins.dict[builtins.int, builtins.str]" +reveal_type({2: 'b', 3: 'c', **foo}) # N: Revealed type is "builtins.dict[builtins.int*, builtins.str*]" +reveal_type({2: 'b', **foo, 3: 'c'}) # N: Revealed type is "builtins.dict[builtins.int*, builtins.str*]" +reveal_type({2: 'b', **foo, 3: 'c', **foo}) # N: Revealed type is "builtins.dict[builtins.int*, builtins.str*]" + + +# Changing type: +bar = {'a': 1} + +reveal_type({**bar, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" +reveal_type({**bar, 2: 'b', 3: 'c'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" +reveal_type({2: 'b', **bar}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" +reveal_type({2: 'b', 3: 'c', **bar}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" +reveal_type({2: 'b', **bar, 3: 'c'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" +reveal_type({2: 'b', **bar, 3: 'c', **bar}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + + +# With subtypes: +class A: pass +class B(A): pass + +parent_dict = {A(): A()} +child_dict = {B(): B()} +mixed_dict = {A(): B()} + +reveal_type({**parent_dict, A(): A()}) # N: Revealed type is "builtins.dict[__main__.A*, __main__.A*]" +reveal_type({**parent_dict, B(): B()}) # N: Revealed type is "builtins.dict[__main__.A, __main__.A]" +reveal_type({A(): B(), **parent_dict}) # N: Revealed type is "builtins.dict[__main__.A, __main__.A]" + +reveal_type({**child_dict, A(): A()}) # N: Revealed type is "builtins.dict[__main__.A, __main__.A]" +reveal_type({**child_dict, B(): B()}) # N: Revealed type is "builtins.dict[__main__.B*, __main__.B*]" +reveal_type({**child_dict, A(): B()}) # N: Revealed type is "builtins.dict[__main__.A, __main__.B]" + +reveal_type({**mixed_dict, A(): A()}) # N: Revealed type is "builtins.dict[__main__.A, __main__.A]" +reveal_type({**mixed_dict, B(): B()}) # N: Revealed type is "builtins.dict[__main__.A, __main__.B]" +reveal_type({**mixed_dict, A(): B()}) # N: Revealed type is "builtins.dict[__main__.A*, __main__.B*]" +reveal_type({**mixed_dict, B(): A()}) # N: Revealed type is "builtins.dict[__main__.A, __main__.A]" +[builtins fixtures/dict.pyi] + + +[case testDictUnpackDifferentTypes] +from typing import Any, Union, Dict, Mapping, TypeVar, Generic, TypedDict, NoReturn + +K = TypeVar('K') +V = TypeVar('V') + +class MyClass: pass +class MyGeneric1(Generic[K]): pass +class MyGeneric2(Generic[K, V]): pass +class MyDict(Dict[K, V]): pass +class MyMapping(Mapping[K, V]): pass +class MyMapping2(Generic[K, V], Mapping[V, K]): pass +class MyMapping3(Mapping[str, int]): pass + +class MySimpleTypedDict(TypedDict): + arg: int + +class MyComplexTypedDict(TypedDict): + arg: int + t: type + + +# Correct types: + +d = {'a': 1} + +md: MyDict[str, int] +mm: MyMapping[str, int] +mm2: MyMapping2[int, str] # reversed +mm3: MyMapping3 +mstd: MySimpleTypedDict +mctd: MyComplexTypedDict + +reveal_type({**md}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.int*]" +reveal_type({**md, 'b': 2}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({**md, 2: 2}) # N: Revealed type is "builtins.dict[builtins.object, builtins.int]" +reveal_type({**md, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" +reveal_type({**md, **d}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({**d, **md}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" + +reveal_type({**mm}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.int*]" +reveal_type({**mm, 'b': 2}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({**mm, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + +reveal_type({**mm2}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.int*]" +reveal_type({**mm2, 'b': 2}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({**mm2, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + +reveal_type({**mm3}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.int*]" +reveal_type({**mm3, 'b': 2}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({**mm3, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + +# TODO: this type for `**mstd` does not seem correct, but this issue is not related to +# https://github.com/python/mypy/pull/11693 +reveal_type({**mstd}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.object*]" +reveal_type({**mstd, 'b': 2}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({**mstd, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + +reveal_type({**mctd}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.object*]" +reveal_type({**mctd, 'b': 2}) # N: Revealed type is "builtins.dict[builtins.str, builtins.object]" +reveal_type({**mctd, 2: 'b'}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + +a: Any +reveal_type({**a}) # N: Revealed type is "builtins.dict[Any, Any]" +reveal_type({**a, **d}) # N: Revealed type is "builtins.dict[Any, Any]" +reveal_type({'a': 1, **a}) # N: Revealed type is "builtins.dict[Any, Any]" + +u1: Union[MyDict[str, int], MyMapping[str, int], Dict[str, int]] +reveal_type({**u1}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.int*]" +reveal_type({**u1, **d}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +reveal_type({'a': 1, **d}) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" + +u2: Union[MyDict[str, int], MyDict[int, str]] +reveal_type({'a': 1, **u2}) # N: Revealed type is "builtins.dict[builtins.object, builtins.object]" + +u3: Union[MyComplexTypedDict, MySimpleTypedDict] +reveal_type({**u3}) # N: Revealed type is "builtins.dict[builtins.str*, builtins.object*]" +reveal_type({'a': 1, **u3}) # N: Revealed type is "builtins.dict[builtins.str, builtins.object]" + + +# Wrong types: + +mc: MyClass +mg1: MyGeneric1[str] +mg2: MyGeneric2[str, int] +never: NoReturn +un1: Union[MyClass, Dict[str, int]] +un2: Union[MyClass, MyGeneric1[str]] + + +reveal_type({1: 'a', **mc}) # N: Revealed type is "builtins.dict[Any, Any]" \ + # E: Cannot unpack "__main__.MyClass" type with "**" + +reveal_type({1: 'a', **mg1}) # N: Revealed type is "builtins.dict[Any, Any]" \ + # E: Cannot unpack "__main__.MyGeneric1[builtins.str]" type with "**" + +reveal_type({1: 'a', **mg2}) # N: Revealed type is "builtins.dict[Any, Any]" \ + # E: Cannot unpack "__main__.MyGeneric2[builtins.str, builtins.int]" type with "**" + +reveal_type({1: 'a', **never}) # N: Revealed type is "builtins.dict[Any, Any]" \ + # E: Cannot unpack "" type with "**" + +reveal_type({1: 'a', **un1}) # N: Revealed type is "builtins.dict[Any, Any]" \ + # E: Cannot unpack "__main__.MyClass" type with "**" + +reveal_type({1: 'a', **un2}) # N: Revealed type is "builtins.dict[Any, Any]" \ + # E: Cannot unpack "__main__.MyClass" type with "**" \ + # E: Cannot unpack "__main__.MyGeneric1[builtins.str]" type with "**" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testDictIncompatibleTypeErrorMessage] from typing import Dict, Callable diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 18310e172a9c..0fa40e495476 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7395,7 +7395,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 "Mapping[int, str]" +main:5: error: Incompatible return value type (got "Dict[int, object]", expected "Dict[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 90588a86d8bf..a3ea8f6c678e 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1328,7 +1328,7 @@ def f() -> Dict[int, str]: def d() -> Dict[int, int]: return {} [out] -_testDictWithStarStarSpecialCase.py:4: error: Argument 1 to "update" of "dict" has incompatible type "Dict[int, int]"; expected "Mapping[int, str]" +_testDictWithStarStarSpecialCase.py:4: error: Incompatible return value type (got "Dict[int, object]", expected "Dict[int, str]") [case testLoadsOfOverloads] from typing import overload, Any, TypeVar, Iterable, List, Dict, Callable, Union