Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 98 additions & 22 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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",
)

#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand All @@ -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(
Expand All @@ -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__")
Expand All @@ -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

Expand All @@ -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,
)

Expand All @@ -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,
)
Expand All @@ -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,
)
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
41 changes: 37 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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")
42 changes: 42 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2999,3 +3040,4 @@ class C(A):
def f(self, y: int | str) -> str: pass
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]

Loading