Skip to content

Use checkmember.py to check variable overrides #18847

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

Merged
merged 5 commits into from
Mar 31, 2025
Merged
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
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
Expand Up @@ -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
Expand All @@ -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,
*,
Expand All @@ -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
Expand Down Expand Up @@ -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'.

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

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

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

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

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