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

Consider property access from class objects in checkmember.py #18504

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
58 changes: 56 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
ANY_STRATEGY,
MYPYC_NATIVE_INT_NAMES,
OVERLOAD_NAMES,
PROPERTY_DECORATOR_NAMES,
AnyType,
BoolTypeQuery,
CallableType,
Expand Down Expand Up @@ -641,6 +642,13 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
if not defn.items:
# In this case we have already complained about none of these being
# valid overloads.
# We only want to add more helpful note if possible.
if defn.info and defn.unanalyzed_items:
first_item = defn.unanalyzed_items[0]
if isinstance(first_item, Decorator):
for base in defn.info.mro[1:]:
if defn.name in base.names:
self.check_property_component_override(first_item, defn.name, base)
return
if len(defn.items) == 1:
self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, defn)
Expand Down Expand Up @@ -2034,15 +2042,18 @@ def check_method_or_accessor_override_for_base(
if base:
name = defn.name
base_attr = base.names.get(name)
is_bad_property = False
if base_attr:
# First, check if we override a final (always an error, even with Any types).
if is_final_node(base_attr.node) and not is_private(name):
self.msg.cant_override_final(name, base.name, defn)
# Second, final can't override anything writeable independently of types.
if defn.is_final:
self.check_if_final_var_override_writable(name, base_attr.node, defn)
if isinstance(defn, (OverloadedFuncDef, Decorator)):
is_bad_property = self.check_property_component_override(defn, name, base)
found_base_method = True
if check_override_compatibility:
if not is_bad_property and check_override_compatibility:
# Check compatibility of the override signature
# (__init__, __new__, __init_subclass__ are special).
if self.check_method_override_for_base_with_name(defn, name, base):
Expand Down Expand Up @@ -2096,6 +2107,35 @@ def check_setter_type_override(
if not is_subtype(original_type, typ):
self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base)

def check_property_component_override(
self, defn: OverloadedFuncDef | Decorator, name: str, base: TypeInfo
) -> bool:
"""Can the override refer to property setter/deleter?"""
if isinstance(defn, OverloadedFuncDef):
if isinstance(defn.items[0], Decorator):
return self.check_property_component_override(defn.items[0], name, base)
return False

deco_type = next(
(
deco.name
for deco in defn.decorators
if isinstance(deco, MemberExpr) and deco.name in ("setter", "deleter")
),
None,
)
if deco_type is None:
return False

base_attr = base.names.get(name)
if not base_attr or base_attr.node is None or not is_property(base_attr.node):
return False
self.msg.fail(
f"Property {deco_type} overrides are not supported.", defn.func, code=codes.MISC
)
self.msg.note("Consider overriding getter explicitly with super() call.", defn.func)
return True

def check_method_override_for_base_with_name(
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo
) -> bool:
Expand Down Expand Up @@ -2279,6 +2319,20 @@ def check_method_override_for_base_with_name(
)
return False

def get_property_instance(self, method: Decorator) -> Instance | None:
property_deco_name = next(
(
name
for d in method.original_decorators
for name in PROPERTY_DECORATOR_NAMES
if refers_to_fullname(d, name)
),
None,
)
if property_deco_name is not None:
return self.named_type(property_deco_name)
return None

def bind_and_map_method(
self, sym: SymbolTableNode, typ: FunctionLike, sub_info: TypeInfo, super_info: TypeInfo
) -> FunctionLike:
Expand Down Expand Up @@ -5285,7 +5339,7 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None
# For overloaded functions we already checked override for overload as a whole.
if allow_empty:
return
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a major change, though. IMO the updated behavior is correct: even if the function is untyped, it still shouldn't be allowed to have invalid @override attached. We won't check its superclass compatibility unless --check-untyped-defs is set (check_override_compatibility flag down the road).

if e.func.info and not e.is_overload:
found_method_base_classes = self.check_method_override(e)
if (
e.func.is_explicit_override
Expand Down
13 changes: 10 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,12 +378,15 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# Reference to a global function.
result = function_type(node, self.named_type("builtins.function"))
elif isinstance(node, OverloadedFuncDef):
result = node.type
if node.type is None:
if self.chk.in_checked_function() and node.items:
self.chk.handle_cannot_determine_type(node.name, e)
result = AnyType(TypeOfAny.from_error)
else:
result = node.type
elif isinstance(node.items[0], Decorator):
property_type = self.chk.get_property_instance(node.items[0])
if property_type is not None:
result = property_type
elif isinstance(node, TypeInfo):
# Reference to a type object.
if node.typeddict_type:
Expand All @@ -409,7 +412,11 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# Reference to a module object.
result = self.module_type(node)
elif isinstance(node, Decorator):
result = self.analyze_var_ref(node.var, e)
property_type = self.chk.get_property_instance(node)
if property_type is not None:
result = property_type
else:
result = self.analyze_var_ref(node.var, e)
elif isinstance(node, TypeAlias):
# Something that refers to a type alias appears in runtime context.
# Note that we suppress bogus errors for alias redefinitions,
Expand Down
10 changes: 10 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,16 @@ def analyze_class_attribute_access(
# C[int].x -> int
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})

if isinstance(node.node, Decorator) and node.node.func.is_property:
property_type = mx.chk.get_property_instance(node.node)
if property_type is not None:
return property_type
if isinstance(node.node, OverloadedFuncDef) and node.node.is_property:
assert isinstance(node.node.items[0], Decorator)
property_type = mx.chk.get_property_instance(node.node.items[0])
if property_type is not None:
return property_type

is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
isinstance(node.node, FuncBase) and node.node.is_class
)
Expand Down
46 changes: 36 additions & 10 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
PROPERTY_DECORATOR_NAMES,
PROTOCOL_NAMES,
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
Expand Down Expand Up @@ -1650,16 +1651,7 @@ def visit_decorator(self, dec: Decorator) -> None:
removed.append(i)
dec.func.is_explicit_override = True
self.check_decorated_function_is_method("override", dec)
elif refers_to_fullname(
d,
(
"builtins.property",
"abc.abstractproperty",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
),
):
elif refers_to_fullname(d, PROPERTY_DECORATOR_NAMES):
removed.append(i)
dec.func.is_property = True
dec.var.is_property = True
Expand Down Expand Up @@ -7166,6 +7158,40 @@ def already_defined(
f'{noun} "{unmangle(name)}" already defined{extra_msg}', ctx, code=codes.NO_REDEF
)

if (
isinstance(ctx, (OverloadedFuncDef, Decorator))
and node is not None
and self.maybe_property_setter_or_deleter(ctx)
and self.maybe_property_definition(node)
):
self.note("Property setter and deleter must be adjacent to the getter.", ctx)

def maybe_property_setter_or_deleter(self, node: SymbolNode) -> bool:
if isinstance(node, OverloadedFuncDef) and node.unanalyzed_items:
# Use unanalyzed_items: setter+deletter would have empty .items
# due to previous error
node = node.unanalyzed_items[0]
return isinstance(node, Decorator) and any(
isinstance(dec, MemberExpr) and dec.name in ("setter", "deleter")
for dec in node.decorators
)

def maybe_property_definition(self, node: SymbolNode) -> bool:
if isinstance(node, Decorator) and node.func.is_property:
return True
elif isinstance(node, OverloadedFuncDef):
if node.is_property:
# Already analyzed
return True
elif isinstance(node.items[0], Decorator):
for dec in node.items[0].decorators:
if isinstance(dec, (NameExpr, MemberExpr)):
if not dec.fullname:
self.accept(dec)
if dec.fullname in PROPERTY_DECORATOR_NAMES:
return True
return False

def name_already_defined(
self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None = None
) -> None:
Expand Down
9 changes: 9 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@
# Supported @override decorator names.
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")

# Supported property decorators
PROPERTY_DECORATOR_NAMES: Final = (
"builtins.property",
"abc.abstractproperty",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
)

# A placeholder used for Bogus[...] parameters
_dummy: Final[Any] = object()

Expand Down
Loading