diff --git a/mypy/checker.py b/mypy/checker.py index b8b85be3fbe8c..b8d22b639b474 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1394,8 +1394,8 @@ def check_default_args(self, item: FuncItem, body_is_trivial: bool) -> None: arg.initializer, context=arg.initializer, msg=ErrorMessage(msg, code=codes.ASSIGNMENT), - lvalue_name="argument", - rvalue_name="default", + lvalue_name="argument has type", + rvalue_name="default has type", notes=notes, ) @@ -2659,8 +2659,8 @@ def check_import(self, node: ImportBase) -> None: assign.rvalue, node, msg=message, - lvalue_name="local name", - rvalue_name="imported name", + lvalue_name="local name has type", + rvalue_name="imported name has type", ) # @@ -2870,7 +2870,7 @@ def check_assignment( ): # Ignore member access to modules instance_type = self.expr_checker.accept(lvalue.expr) rvalue_type, lvalue_type, infer_lvalue_type = self.check_member_assignment( - instance_type, lvalue_type, rvalue, context=rvalue + instance_type, lvalue_type, lvalue, rvalue, context=rvalue ) else: # Hacky special case for assigning a literal None @@ -3127,7 +3127,8 @@ def check_compatibility_super( rvalue, message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, "expression has type", - f'base class "{base.name}" defined the type as', + message_registry.INCOMPATIBLE_TYPES_BASECLASS_MISMATCH.format(base.name), + # f'base class "{base.name}" defined the type as', ) return True @@ -3938,8 +3939,8 @@ def check_simple_assignment( rvalue: Expression, context: Context, msg: ErrorMessage = message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, - lvalue_name: str = "variable", - rvalue_name: str = "expression", + lvalue_name: str = "variable has type", + rvalue_name: str = "expression has type", *, notes: list[str] | None = None, ) -> Type: @@ -3988,14 +3989,19 @@ def check_simple_assignment( lvalue_type, context, msg, - f"{rvalue_name} has type", - f"{lvalue_name} has type", + rvalue_name, + lvalue_name, notes=notes, ) return rvalue_type def check_member_assignment( - self, instance_type: Type, attribute_type: Type, rvalue: Expression, context: Context + self, + instance_type: Type, + attribute_type: Type, + lvalue: MemberExpr, + rvalue: Expression, + context: Context, ) -> tuple[Type, Type, bool]: """Type member assignment. @@ -4011,15 +4017,20 @@ def check_member_assignment( instance_type = get_proper_type(instance_type) attribute_type = get_proper_type(attribute_type) # Descriptors don't participate in class-attribute access - if (isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance( - instance_type, TypeType - ): - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + if isinstance(instance_type, CallableType) and instance_type.is_type_obj(): + rvalue_type = self.check_parent_member_assignment( + instance_type.ret_type, attribute_type, lvalue, rvalue, context=context + ) + return rvalue_type, attribute_type, True + elif isinstance(instance_type, TypeType): + rvalue_type = self.check_parent_member_assignment( + instance_type.item, attribute_type, lvalue, rvalue, context=context + ) return rvalue_type, attribute_type, True if not isinstance(attribute_type, Instance): # TODO: support __set__() for union types. - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context=context) return rvalue_type, attribute_type, True mx = MemberContext( @@ -4038,7 +4049,9 @@ def check_member_assignment( # the return type of __get__. This doesn't match the python semantics, # (which allow you to override the descriptor with any value), but preserves # the type of accessing the attribute (even after the override). - rvalue_type = self.check_simple_assignment(get_type, rvalue, context) + rvalue_type = self.check_parent_member_assignment( + instance_type, get_type, lvalue, rvalue, context=context + ) return rvalue_type, get_type, True dunder_set = attribute_type.type.get_method("__set__") @@ -4047,7 +4060,7 @@ def check_member_assignment( message_registry.DESCRIPTOR_SET_NOT_CALLABLE.format( attribute_type.str_with_options(self.options) ), - context, + context=context, ) return AnyType(TypeOfAny.from_error), get_type, False @@ -4068,7 +4081,7 @@ def check_member_assignment( dunder_set_type, [TempNode(instance_type, context=context), rvalue], [nodes.ARG_POS, nodes.ARG_POS], - context, + context=context, object_type=attribute_type, ) @@ -4080,7 +4093,7 @@ def check_member_assignment( dunder_set_type, [TempNode(instance_type, context=context), type_context], [nodes.ARG_POS, nodes.ARG_POS], - context, + context=context, object_type=attribute_type, callable_name=callable_name, ) @@ -4094,7 +4107,7 @@ def check_member_assignment( dunder_set_type, [TempNode(instance_type, context=context), type_context], [nodes.ARG_POS, nodes.ARG_POS], - context, + context=context, object_type=attribute_type, callable_name=callable_name, ) @@ -4110,10 +4123,73 @@ def check_member_assignment( # and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects. - rvalue_type = self.check_simple_assignment(set_type, rvalue, context) + rvalue_type = self.check_simple_assignment(set_type, rvalue, context=context) infer = is_subtype(rvalue_type, get_type) and is_subtype(get_type, set_type) return rvalue_type if infer else set_type, get_type, infer + def check_parent_member_assignment( + self, + lvalue_base_type: Type, + lvalue_type: Type, + lvalue: MemberExpr, + rvalue: Expression, + context: Context, + ) -> Type: + """Exactly the same as check_simple_assignment(). + + It adds better error message to indicate the class where the attribute originally defined if possible. + """ + lvalue_base_type = get_proper_type(lvalue_base_type) + if ( + not isinstance(lvalue_base_type, Instance) + or lvalue.name in lvalue_base_type.type.names + ): + return self.check_simple_assignment(lvalue_type, rvalue, context=context) + + lvalue_warn = message_registry.INCOMPATIBLE_TYPES_BASECLASS_MISMATCH + + metaclass = lvalue_base_type.type.metaclass_type + if metaclass and metaclass.type.has_readable_member(lvalue.name): + if lvalue.name in metaclass.type.names: + meta_attribute = metaclass.type.names[lvalue.name].node + if not (isinstance(meta_attribute, Var) and meta_attribute.info.is_generic()): + return self.check_simple_assignment( + lvalue_type, + rvalue, + context=context, + msg=message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, + lvalue_name=message_registry.INCOMPATIBLE_TYPES_METACLASS_MISMATCH.format( + metaclass.type.defn.name + ), + ) + else: + # attribute is defined further up in the metaclass MRO + # better error message for later check + lvalue_warn = 'base class "{{}}" of {}'.format( + message_registry.INCOMPATIBLE_TYPES_METACLASS_MISMATCH.format( + metaclass.type.defn.name + ) + ) + lvalue_base_type = metaclass + + if lvalue_base_type.type.has_readable_member(lvalue.name): + for base in lvalue_base_type.type.bases: + if lvalue.name in base.type.names: + parent_context = base.type.names[lvalue.name].node + if isinstance(parent_context, Var) and parent_context.info.is_generic(): + # Attribute may have been defined in this class but its type is + # probably defined elsewhere, possibly in the instance itself. + continue + return self.check_simple_assignment( + lvalue_type, + rvalue, + context=context, + msg=message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, + lvalue_name=lvalue_warn.format(base.type.defn.name), + ) + + return self.check_simple_assignment(lvalue_type, rvalue, context=context) + def check_indexed_assignment( self, lvalue: IndexExpr, rvalue: Expression, context: Context ) -> None: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f9fbd53866da5..29b4eed780945 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -852,8 +852,8 @@ def check_typeddict_call_with_kwargs( msg=ErrorMessage( message_registry.INCOMPATIBLE_TYPES.value, code=codes.TYPEDDICT_ITEM ), - lvalue_name=f'TypedDict item "{item_name}"', - rvalue_name="expression", + lvalue_name=f'TypedDict item "{item_name}" has type', + rvalue_name="expression has type", ) return orig_ret_type diff --git a/mypy/message_registry.py b/mypy/message_registry.py index b32edc06571a8..4fe1ec9fe0c4e 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -73,6 +73,9 @@ def with_additional_msg(self, info: str) -> ErrorMessage: 'Incompatible types in "async with" for "__aexit__"' ) INCOMPATIBLE_TYPES_IN_ASYNC_FOR: Final = 'Incompatible types in "async for"' +INCOMPATIBLE_TYPES_BASECLASS_MISMATCH: Final = 'base class "{}" defined the type as' +INCOMPATIBLE_TYPES_METACLASS_MISMATCH: Final = 'metaclass "{}" defined the type as' + INVALID_TYPE_FOR_SLOTS: Final = 'Invalid type for "__slots__"' ASYNC_FOR_OUTSIDE_COROUTINE: Final = '"async for" outside async function' diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 6476ad1566dcd..9f7752d00aba8 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -234,7 +234,7 @@ class A: self.x = 0 class B(A): def f(self) -> None: - self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [targets __main__, __main__.A.f, __main__.B.f] [case testAssignmentToAttributeInMultipleMethods] @@ -4132,7 +4132,7 @@ class B(A): def __init__(self) -> None: self.a = "a" [out] -main:5: error: Incompatible types in assignment (expression has type "str", variable has type "int") +main:5: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [case testVariableSubclassTypeOverwrite] class A: @@ -6704,7 +6704,7 @@ from b import C class D(C): def g(self) -> None: - self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self.x = '' # E: Incompatible types in assignment (expression has type "str", base class "C" defined the type as "int") def f(self) -> None: reveal_type(self.x) # N: Revealed type is "builtins.int" @@ -6735,7 +6735,7 @@ class C: class E(C): def g(self) -> None: - self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self.x = '' # E: Incompatible types in assignment (expression has type "str", base class "C" defined the type as "int") def f(self) -> None: reveal_type(self.x) # N: Revealed type is "builtins.int" @@ -7859,3 +7859,36 @@ f5(1) # E: Argument 1 to "f5" has incompatible type "int"; expected "Integral" # N: Types from "numbers" aren't supported for static type checking \ # N: See https://peps.python.org/pep-0484/#the-numeric-tower \ # N: Consider using a protocol instead, such as typing.SupportsFloat + +[case testMetaclassParentAttribute] +import submod +class A(metaclass=submod.M): + pass +A.x = "" +A.x = 1 +[file submod.py] +class N(type): + x: str +class M(N): + pass +[out] +main:5: error: Incompatible types in assignment (expression has type "int", base class "N" of metaclass "M" defined the type as "str") + +[case testParentMetaclassAttribute] +import submod +class A(metaclass=submod.M): + pass +class B(A): + pass +class C(B): + pass +B.x = "" +B.x = 1 +C.x = "" +C.x = 1 +[file submod.py] +class M(type): + x: str +[out] +main:9: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") +main:11: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 4bad19af539ce..2bfa758a3599c 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -2725,6 +2725,47 @@ f: Callable[[Sequence[TI]], None] g: Callable[[Union[Sequence[TI], Sequence[TS]]], None] f = g +[case testFactoryWithInheritance] +from typing import Type +class A: + x = "foo" +class B(A): + pass +def f() -> Type[B]: + return B +def g() -> B: + return B() + +f().x = "bar" +f().x = 0 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") +f()().x = "baz" +f()().x = 1 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") +g().x = "" +g().x = 2 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") + +[case testMultiLayerCallable] +class A: + y = 0 # Type: int +class B(A): + def __init__(self, z: int) -> None: + self.z = z +def foo(x: int) -> B: + return B(x) +def bar(x: int) -> B: + return foo(x) + +a = bar(1) +a.z = 2 +a.z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +a.y = 2 +a.y = "" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") + +b = foo(3) +a.z = 4 +a.z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +a.y = 4 +a.y = "" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") + [case explicitOverride] # flags: --python-version 3.12 from typing import override @@ -2999,3 +3040,4 @@ class C(A): def f(self, y: int | str) -> str: pass [typing fixtures/typing-full.pyi] [builtins fixtures/tuple.pyi] + diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index d03f2e35e1c46..cff4279e70349 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -44,9 +44,9 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") c.y = '' -c.y = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +c.y = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") [case testSimpleMultipleInheritanceAndInstanceVariableInClassBody] import typing @@ -57,9 +57,9 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") c.y = '' -c.y = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +c.y = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") [case testSimpleMultipleInheritanceAndClassVariable] import typing @@ -69,9 +69,9 @@ class B: y = '' class C(A, B): pass C.x = 1 -C.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +C.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") C.y = '' -C.y = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +C.y = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") -- Name collisions @@ -100,7 +100,7 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [case testClassVarNameOverlapInMultipleInheritanceWithCompatibleTypes] import typing @@ -111,9 +111,9 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") C.x = 1 -C.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +C.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [case testMethodNameCollisionInMultipleInheritanceWithIncompatibleSigs] import typing diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 05a03ecaf7b09..ea25d1b93cbcb 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -934,8 +934,8 @@ Parent.NormalExplicit = 4 # E: Incompatible types in assignment (expression has Parent.SpecialImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "") Parent.SpecialExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "") -Child.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]") -Child.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]") +Child.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", base class "Parent" defined the type as "Type[Foo]") +Child.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", base class "Parent" defined the type as "Type[Foo]") Child.SpecialImplicit = 4 Child.SpecialExplicit = 4 diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 88a11be31f340..7be7a17b46bf2 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1106,7 +1106,7 @@ class A: [out] == main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") -main:9: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:9: error: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") main:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") [case testChangeBaseClassAttributeType] @@ -1124,7 +1124,7 @@ class A: self.x = 'a' [out] == -main:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:4: error: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") [case testRemoveAttributeInBaseClass] import m @@ -2845,7 +2845,7 @@ class M(type): x: int [out] == -a.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +a.py:4: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") == a.py:4: error: "Type[C]" has no attribute "x" == @@ -2874,7 +2874,7 @@ class M(type): x: int [out] == -a.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") +a.py:3: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") == a.py:3: error: "Type[C]" has no attribute "x" == @@ -4425,7 +4425,7 @@ class C: y: str [out] == -main:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:4: error: Incompatible types in assignment (expression has type "int", base class "C" defined the type as "str") [case testNewTypeDependencies2] from a import N