Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use checkmember.py to check variable overrides #18847

Merged
merged 5 commits into from
Mar 31, 2025
Merged
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
273 changes: 133 additions & 140 deletions mypy/checker.py

Large diffs are not rendered by default.

85 changes: 47 additions & 38 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
@@ -93,11 +93,12 @@ def __init__(
original_type: Type,
context: Context,
chk: mypy.checker.TypeChecker,
self_type: Type | None,
self_type: Type | None = None,
module_symbol_table: SymbolTable | None = None,
no_deferral: bool = False,
is_self: bool = False,
rvalue: Expression | None = None,
suppress_errors: bool = False,
) -> None:
self.is_lvalue = is_lvalue
self.is_super = is_super
@@ -113,13 +114,18 @@ def __init__(
if rvalue is not None:
assert is_lvalue
self.rvalue = rvalue
self.suppress_errors = suppress_errors

def named_type(self, name: str) -> Instance:
return self.chk.named_type(name)

def not_ready_callback(self, name: str, context: Context) -> None:
self.chk.handle_cannot_determine_type(name, context)

def fail(self, msg: str) -> None:
if not self.suppress_errors:
self.msg.fail(msg, self.context)

def copy_modified(
self,
*,
@@ -138,6 +144,7 @@ def copy_modified(
module_symbol_table=self.module_symbol_table,
no_deferral=self.no_deferral,
rvalue=self.rvalue,
suppress_errors=self.suppress_errors,
)
if self_type is not None:
mx.self_type = self_type
@@ -165,6 +172,7 @@ def analyze_member_access(
no_deferral: bool = False,
is_self: bool = False,
rvalue: Expression | None = None,
suppress_errors: bool = False,
) -> Type:
"""Return the type of attribute 'name' of 'typ'.
@@ -191,6 +199,11 @@ def analyze_member_access(
'rvalue' can be provided optionally to infer better setter type when is_lvalue is True,
most notably this helps for descriptors with overloaded __set__() method.
'suppress_errors' will skip any logic that is only needed to generate error messages.
Note that this more of a performance optimization, one should not rely on this to not
show any messages, as some may be show e.g. by callbacks called here,
use msg.filter_errors(), if needed.
"""
mx = MemberContext(
is_lvalue=is_lvalue,
@@ -204,6 +217,7 @@ def analyze_member_access(
no_deferral=no_deferral,
is_self=is_self,
rvalue=rvalue,
suppress_errors=suppress_errors,
)
result = _analyze_member_access(name, typ, mx, override_info)
possible_literal = get_proper_type(result)
@@ -251,7 +265,8 @@ def _analyze_member_access(
)
return _analyze_member_access(name, typ.upper_bound, mx, override_info)
elif isinstance(typ, DeletedType):
mx.msg.deleted_as_rvalue(typ, mx.context)
if not mx.suppress_errors:
mx.msg.deleted_as_rvalue(typ, mx.context)
return AnyType(TypeOfAny.from_error)
return report_missing_attribute(mx.original_type, typ, name, mx)

@@ -280,6 +295,8 @@ def report_missing_attribute(
mx: MemberContext,
override_info: TypeInfo | None = None,
) -> Type:
if mx.suppress_errors:
return AnyType(TypeOfAny.from_error)
error_code = mx.msg.has_no_attr(original_type, typ, name, mx.context, mx.module_symbol_table)
if not mx.msg.prefer_simple_messages():
if may_be_awaitable_attribute(name, typ, mx, override_info):
@@ -297,7 +314,7 @@ def analyze_instance_member_access(
if name == "__init__" and not mx.is_super:
# Accessing __init__ in statically typed code would compromise
# type safety unless used via super().
mx.msg.fail(message_registry.CANNOT_ACCESS_INIT, mx.context)
mx.fail(message_registry.CANNOT_ACCESS_INIT)
return AnyType(TypeOfAny.from_error)

# The base object has an instance type.
@@ -310,13 +327,14 @@ def analyze_instance_member_access(
state.find_occurrences
and info.name == state.find_occurrences[0]
and name == state.find_occurrences[1]
and not mx.suppress_errors
):
mx.msg.note("Occurrence of '{}.{}'".format(*state.find_occurrences), mx.context)

# Look up the member. First look up the method dictionary.
method = info.get_method(name)
if method and not isinstance(method, Decorator):
if mx.is_super:
if mx.is_super and not mx.suppress_errors:
validate_super_call(method, mx)

if method.is_property:
@@ -327,7 +345,7 @@ def analyze_instance_member_access(
mx.chk.warn_deprecated(items[1], mx.context)
return analyze_var(name, getter.var, typ, mx)

if mx.is_lvalue:
if mx.is_lvalue and not mx.suppress_errors:
mx.msg.cant_assign_to_method(mx.context)
if not isinstance(method, OverloadedFuncDef):
signature = function_type(method, mx.named_type("builtins.function"))
@@ -361,7 +379,6 @@ def validate_super_call(node: FuncBase, mx: MemberContext) -> None:
unsafe_super = False
if isinstance(node, FuncDef) and node.is_trivial_body:
unsafe_super = True
impl = node
elif isinstance(node, OverloadedFuncDef):
if node.impl:
impl = node.impl if isinstance(node.impl, FuncDef) else node.impl.func
@@ -505,7 +522,7 @@ def analyze_member_var_access(
if isinstance(vv, Decorator):
# The associated Var node of a decorator contains the type.
v = vv.var
if mx.is_super:
if mx.is_super and not mx.suppress_errors:
validate_super_call(vv.func, mx)

if isinstance(vv, TypeInfo):
@@ -603,7 +620,7 @@ def analyze_member_var_access(
if not itype.extra_attrs.mod_name:
return itype.extra_attrs.attrs[name]

if mx.is_super:
if mx.is_super and not mx.suppress_errors:
mx.msg.undefined_in_superclass(name, mx.context)
return AnyType(TypeOfAny.from_error)
else:
@@ -669,11 +686,10 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:

dunder_get = descriptor_type.type.get_method("__get__")
if dunder_get is None:
mx.msg.fail(
mx.fail(
message_registry.DESCRIPTOR_GET_NOT_CALLABLE.format(
descriptor_type.str_with_options(mx.msg.options)
),
mx.context,
)
)
return AnyType(TypeOfAny.from_error)

@@ -732,11 +748,10 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
return inferred_dunder_get_type

if not isinstance(inferred_dunder_get_type, CallableType):
mx.msg.fail(
mx.fail(
message_registry.DESCRIPTOR_GET_NOT_CALLABLE.format(
descriptor_type.str_with_options(mx.msg.options)
),
mx.context,
)
)
return AnyType(TypeOfAny.from_error)

@@ -747,11 +762,10 @@ def analyze_descriptor_assign(descriptor_type: Instance, mx: MemberContext) -> T
instance_type = get_proper_type(mx.self_type)
dunder_set = descriptor_type.type.get_method("__set__")
if dunder_set is None:
mx.chk.fail(
mx.fail(
message_registry.DESCRIPTOR_SET_NOT_CALLABLE.format(
descriptor_type.str_with_options(mx.msg.options)
),
mx.context,
).value
)
return AnyType(TypeOfAny.from_error)

@@ -851,11 +865,11 @@ def analyze_var(
if typ:
if isinstance(typ, PartialType):
return mx.chk.handle_partial_var_type(typ, mx.is_lvalue, var, mx.context)
if mx.is_lvalue and var.is_property and not var.is_settable_property:
# TODO allow setting attributes in subclass (although it is probably an error)
mx.msg.read_only_property(name, itype.type, mx.context)
if mx.is_lvalue and var.is_classvar:
mx.msg.cant_assign_to_classvar(name, mx.context)
if mx.is_lvalue and not mx.suppress_errors:
if var.is_property and not var.is_settable_property:
mx.msg.read_only_property(name, itype.type, mx.context)
if var.is_classvar:
mx.msg.cant_assign_to_classvar(name, mx.context)
t = freshen_all_functions_type_vars(typ)
t = expand_self_type_if_needed(t, mx, var, original_itype)
t = expand_type_by_instance(t, itype)
@@ -875,11 +889,10 @@ def analyze_var(
call_type = typ

if isinstance(call_type, FunctionLike) and not call_type.is_type_obj():
if mx.is_lvalue:
if var.is_property:
if not var.is_settable_property:
mx.msg.read_only_property(name, itype.type, mx.context)
else:
if mx.is_lvalue and not mx.suppress_errors:
if var.is_property and not var.is_settable_property:
mx.msg.read_only_property(name, itype.type, mx.context)
elif not var.is_property:
mx.msg.cant_assign_to_method(mx.context)

if not var.is_staticmethod:
@@ -1073,22 +1086,20 @@ def analyze_class_attribute_access(

is_decorated = isinstance(node.node, Decorator)
is_method = is_decorated or isinstance(node.node, FuncBase)
if mx.is_lvalue:
if mx.is_lvalue and not mx.suppress_errors:
if is_method:
mx.msg.cant_assign_to_method(mx.context)
if isinstance(node.node, TypeInfo):
mx.msg.fail(message_registry.CANNOT_ASSIGN_TO_TYPE, mx.context)
mx.fail(message_registry.CANNOT_ASSIGN_TO_TYPE)

# Refuse class attribute access if slot defined
if info.slots and name in info.slots:
mx.msg.fail(message_registry.CLASS_VAR_CONFLICTS_SLOTS.format(name), mx.context)
mx.fail(message_registry.CLASS_VAR_CONFLICTS_SLOTS.format(name))

# If a final attribute was declared on `self` in `__init__`, then it
# can't be accessed on the class object.
if node.implicit and isinstance(node.node, Var) and node.node.is_final:
mx.msg.fail(
message_registry.CANNOT_ACCESS_FINAL_INSTANCE_ATTR.format(node.node.name), mx.context
)
mx.fail(message_registry.CANNOT_ACCESS_FINAL_INSTANCE_ATTR.format(node.node.name))

# An assignment to final attribute on class object is also always an error,
# independently of types.
@@ -1146,7 +1157,7 @@ def analyze_class_attribute_access(
message = message_registry.GENERIC_CLASS_VAR_ACCESS
else:
message = message_registry.GENERIC_INSTANCE_VAR_CLASS_ACCESS
mx.msg.fail(message, mx.context)
mx.fail(message)
t = expand_self_type_if_needed(t, mx, node.node, itype, is_class=True)
# Erase non-mapped variables, but keep mapped ones, even if there is an error.
# In the above example this means that we infer following types:
@@ -1176,9 +1187,7 @@ def analyze_class_attribute_access(
return AnyType(TypeOfAny.special_form)

if isinstance(node.node, TypeVarExpr):
mx.msg.fail(
message_registry.CANNOT_USE_TYPEVAR_AS_EXPRESSION.format(info.name, name), mx.context
)
mx.fail(message_registry.CANNOT_USE_TYPEVAR_AS_EXPRESSION.format(info.name, name))
return AnyType(TypeOfAny.from_error)

# TODO: some logic below duplicates analyze_ref_expr in checkexpr.py
@@ -1267,7 +1276,7 @@ def analyze_typeddict_access(
typ, mx.context.index, setitem=True
)
assigned_readonly_keys = typ.readonly_keys & key_names
if assigned_readonly_keys:
if assigned_readonly_keys and not mx.suppress_errors:
mx.msg.readonly_keys_mutated(assigned_readonly_keys, context=mx.context)
else:
# It can also be `a.__setitem__(...)` direct call.
110 changes: 86 additions & 24 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
@@ -145,7 +145,7 @@ class Base:
pass

class Derived(Base):
__hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[Base], int]")
__hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[], int]")

[case testOverridePartialAttributeWithMethod]
# This was crashing: https://github.com/python/mypy/issues/11686.
@@ -4453,7 +4453,7 @@ class A:
def a(self) -> None: pass
b = 1
class B(A):
a = 1 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "Callable[[A], None]")
a = 1 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "Callable[[], None]")
def b(self) -> None: pass # E: Signature of "b" incompatible with supertype "A" \
# N: Superclass: \
# N: int \
@@ -4546,20 +4546,20 @@ main:7: error: Incompatible types in assignment (expression has type "Callable[[
[case testClassSpec]
from typing import Callable
class A():
b = None # type: Callable[[A, int], int]
b = None # type: Callable[[int], int]
class B(A):
def c(self, a: int) -> int: pass
b = c
reveal_type(A().b) # N: Revealed type is "def (builtins.int) -> builtins.int"
reveal_type(B().b) # N: Revealed type is "def (a: builtins.int) -> builtins.int"

[case testClassSpecError]
from typing import Callable
class A():
b = None # type: Callable[[A, int], int]
b = None # type: Callable[[int], int]
class B(A):
def c(self, a: str) -> int: pass
b = c
[out]
main:6: error: Incompatible types in assignment (expression has type "Callable[[str], int]", base class "A" defined the type as "Callable[[int], int]")
b = c # E: Incompatible types in assignment (expression has type "Callable[[str], int]", base class "A" defined the type as "Callable[[int], int]")

[case testClassStaticMethod]
class A():
@@ -4581,10 +4581,11 @@ class A():
class B(A):
@staticmethod
def b(a: str) -> None: pass
c = b
c = b # E: Incompatible types in assignment (expression has type "Callable[[str], None]", base class "A" defined the type as "Callable[[int], None]")
a: A
reveal_type(a.a) # N: Revealed type is "def (a: builtins.int)"
reveal_type(a.c) # N: Revealed type is "def (a: builtins.int)"
[builtins fixtures/staticmethod.pyi]
[out]
main:8: error: Incompatible types in assignment (expression has type "Callable[[str], None]", base class "A" defined the type as "Callable[[int], None]")

[case testClassStaticMethodSubclassing]
class A:
@@ -4649,31 +4650,28 @@ class B(A):
class A:
x = 1
class B(A):
x = "a"
x = "a" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
class C(B):
x = object()
[out]
main:4: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
main:6: error: Incompatible types in assignment (expression has type "object", base class "A" defined the type as "int")
x = object() # E: Incompatible types in assignment (expression has type "object", base class "B" defined the type as "str")

[case testClassOneErrorPerLine]
class A:
x = 1
x = 1
class B(A):
x = ""
x = 1.0
[out]
main:4: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
main:5: error: Incompatible types in assignment (expression has type "float", base class "A" defined the type as "int")
x: str = "" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
x = 1.0 # E: Incompatible types in assignment (expression has type "float", variable has type "str")
class BInfer(A):
x = "" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
x = 1.0 # E: Incompatible types in assignment (expression has type "float", variable has type "str") \
# E: Incompatible types in assignment (expression has type "float", base class "A" defined the type as "int")

[case testClassIgnoreType_RedefinedAttributeAndGrandparentAttributeTypesNotIgnored]
class A:
x = 0
class B(A):
x = '' # type: ignore
class C(B):
x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
[out]
x = ''

[case testClassIgnoreType_RedefinedAttributeTypeIgnoredInChildren]
class A:
@@ -4682,7 +4680,6 @@ class B(A):
x = '' # type: ignore
class C(B):
x = '' # type: ignore
[out]

[case testInvalidMetaclassStructure]
class X(type): pass
@@ -8586,3 +8583,68 @@ class Wrapper(Generic[T, R]):
def __call__(self, s: T) -> list[R]: ...
def deco_instance(fn: Callable[[T, int], R]) -> Wrapper[T, R]: ...
[builtins fixtures/property.pyi]

[case testOverridePropertyWithDescriptor]
from typing import Any

class StrProperty:
def __get__(self, instance: Any, owner: Any) -> str: ...

class Base:
@property
def id(self) -> str: ...

class BadBase:
@property
def id(self) -> int: ...

class Derived(Base):
id = StrProperty()

class BadDerived(BadBase):
id = StrProperty() # E: Incompatible types in assignment (expression has type "str", base class "BadBase" defined the type as "int")
[builtins fixtures/property.pyi]

[case testLambdaInOverrideInference]
class B:
def f(self, x: int) -> int: ...
class C(B):
f = lambda s, x: x

reveal_type(C().f) # N: Revealed type is "def (x: builtins.int) -> builtins.int"

[case testGenericDecoratorInOverrideInference]
from typing import Any, Callable, TypeVar
from typing_extensions import ParamSpec, Concatenate

P = ParamSpec("P")
T = TypeVar("T")
def wrap(f: Callable[Concatenate[Any, P], T]) -> Callable[Concatenate[Any, P], T]: ...

class Base:
def g(self, a: int) -> int:
return a + 1

class Derived(Base):
def _g(self, a: int) -> int:
return a + 2
g = wrap(_g)

reveal_type(Derived().g) # N: Revealed type is "def (a: builtins.int) -> builtins.int"
[builtins fixtures/paramspec.pyi]

[case testClassVarOverrideWithSubclass]
class A: ...
class B(A): ...
class AA:
cls = A
class BB(AA):
cls = B

[case testSelfReferenceWithinMethodFunction]
class B:
x: str
class C(B):
def meth(self) -> None:
def cb() -> None:
self.x: int = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str")
4 changes: 2 additions & 2 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
@@ -3528,8 +3528,8 @@ class Parent:
def method_with(self, param: str) -> "Parent": ...

class Child(Parent):
method_without: Callable[["Child"], "Child"]
method_with: Callable[["Child", str], "Child"] # E: Incompatible types in assignment (expression has type "Callable[[str], Child]", base class "Parent" defined the type as "Callable[[Arg(str, 'param')], Parent]")
method_without: Callable[[], "Child"]
method_with: Callable[[str], "Child"] # E: Incompatible types in assignment (expression has type "Callable[[str], Child]", base class "Parent" defined the type as "Callable[[Arg(str, 'param')], Parent]")
[builtins fixtures/tuple.pyi]

[case testDistinctFormattingUnion]
4 changes: 0 additions & 4 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
@@ -5235,11 +5235,7 @@ class Sub(Base):

[builtins fixtures/property.pyi]
[out]
tmp/a.py:3: error: Cannot determine type of "foo"
tmp/a.py:4: error: Cannot determine type of "foo"
[out2]
tmp/a.py:3: error: Cannot determine type of "foo"
tmp/a.py:4: error: Cannot determine type of "foo"

[case testRedefinitionClass]
import b
2 changes: 1 addition & 1 deletion test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
@@ -548,7 +548,7 @@ b = B._make(['']) # type: B
[case testNamedTupleIncompatibleRedefinition]
from typing import NamedTuple
class Crash(NamedTuple):
count: int # E: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], object], int]")
count: int # E: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[object], int]")
[builtins fixtures/tuple.pyi]

[case testNamedTupleInClassNamespace]
2 changes: 1 addition & 1 deletion test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
@@ -332,7 +332,7 @@ class MyHashable(Protocol):

class C(MyHashable):
__my_hash__ = None # E: Incompatible types in assignment \
(expression has type "None", base class "MyHashable" defined the type as "Callable[[MyHashable], int]")
(expression has type "None", base class "MyHashable" defined the type as "Callable[[], int]")

[case testProtocolsWithNoneAndStrictOptional]
from typing import Protocol
4 changes: 2 additions & 2 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
@@ -1803,7 +1803,7 @@ class C:
def bar(self) -> Self: ...
foo: Callable[[S, Self], Tuple[Self, S]]

reveal_type(C().foo) # N: Revealed type is "def [S] (S`1, __main__.C) -> Tuple[__main__.C, S`1]"
reveal_type(C().foo) # N: Revealed type is "def [S] (S`2, __main__.C) -> Tuple[__main__.C, S`2]"
reveal_type(C().foo(42, C())) # N: Revealed type is "Tuple[__main__.C, builtins.int]"
class This: ...
[builtins fixtures/tuple.pyi]
@@ -1899,7 +1899,7 @@ class C:

class D(C): ...

reveal_type(D.f) # N: Revealed type is "def [T] (T`1) -> T`1"
reveal_type(D.f) # N: Revealed type is "def [T] (T`3) -> T`3"
reveal_type(D().f) # N: Revealed type is "def () -> __main__.D"

[case testTypingSelfOnSuperTypeVarValues]
4 changes: 2 additions & 2 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
@@ -4520,9 +4520,9 @@ x = 0
x = ''
[builtins fixtures/tuple.pyi]
[out]
b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], object], int]")
b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[object], int]")
==
b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], object], int]")
b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[object], int]")

[case testReprocessEllipses1]
import a