Skip to content

Fix crash on settable property alias #19615

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 3 commits into from
Aug 8, 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
27 changes: 22 additions & 5 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
WhileStmt,
WithStmt,
YieldExpr,
get_func_def,
is_final_node,
)
from mypy.operators import flip_ops, int_op_to_method, neg_ops
Expand Down Expand Up @@ -703,6 +704,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
# TODO: keep precise type for callables with tricky but valid signatures.
setter_type = fallback_setter_type
defn.items[0].var.setter_type = setter_type
if isinstance(defn.type, Overloaded):
# Update legacy property type for decorated properties.
getter_type = self.extract_callable_type(defn.items[0].var.type, defn)
if getter_type is not None:
getter_type.definition = defn.items[0]
defn.type.items[0] = getter_type
for i, fdef in enumerate(defn.items):
assert isinstance(fdef, Decorator)
if defn.is_property:
Expand Down Expand Up @@ -730,7 +737,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
assert isinstance(item, Decorator)
item_type = self.extract_callable_type(item.var.type, item)
if item_type is not None:
item_type.definition = item.func
item_type.definition = item
item_types.append(item_type)
if item_types:
defn.type = Overloaded(item_types)
Expand Down Expand Up @@ -2501,8 +2508,9 @@ def check_override(

override_ids = override.type_var_ids()
type_name = None
if isinstance(override.definition, FuncDef):
type_name = override.definition.info.name
definition = get_func_def(override)
if isinstance(definition, FuncDef):
type_name = definition.info.name

def erase_override(t: Type) -> Type:
return erase_typevars(t, ids_to_erase=override_ids)
Expand Down Expand Up @@ -3509,6 +3517,7 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) ->
continue

base_type, base_node = self.node_type_from_base(lvalue_node.name, base, lvalue)
# TODO: if the r.h.s. is a descriptor, we should check setter override as well.
custom_setter = is_custom_settable_property(base_node)
if isinstance(base_type, PartialType):
base_type = None
Expand Down Expand Up @@ -4494,6 +4503,8 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
if isinstance(p_type, Overloaded):
# TODO: in theory we can have a property with a deleter only.
var.is_settable_property = True
assert isinstance(definition, Decorator), definition
var.setter_type = definition.var.setter_type

def set_inference_error_fallback_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
"""Store best known type for variable if type inference failed.
Expand Down Expand Up @@ -5356,6 +5367,8 @@ def visit_decorator_inner(
self.check_untyped_after_decorator(sig, e.func)
self.require_correct_self_argument(sig, e.func)
sig = set_callable_name(sig, e.func)
if isinstance(sig, CallableType):
sig.definition = e
e.var.type = sig
e.var.is_ready = True
if e.func.is_property:
Expand Down Expand Up @@ -8651,17 +8664,21 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
return t.copy_modified(args=[a.accept(self) for a in t.args])


def is_classmethod_node(node: Node | None) -> bool | None:
def is_classmethod_node(node: SymbolNode | None) -> bool | None:
"""Find out if a node describes a classmethod."""
if isinstance(node, Decorator):
node = node.func
if isinstance(node, FuncDef):
return node.is_class
if isinstance(node, Var):
return node.is_classmethod
return None


def is_node_static(node: Node | None) -> bool | None:
def is_node_static(node: SymbolNode | None) -> bool | None:
"""Find out if a node describes a static function method."""
if isinstance(node, Decorator):
node = node.func
if isinstance(node, FuncDef):
return node.is_static
if isinstance(node, Var):
Expand Down
11 changes: 9 additions & 2 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,15 @@ def expand_and_bind_callable(
freeze_all_type_vars(expanded)
if not var.is_property:
return expanded
# TODO: a decorated property can result in Overloaded here.
assert isinstance(expanded, CallableType)
if isinstance(expanded, Overloaded):
# Legacy way to store settable properties is with overloads. Also in case it is
# an actual overloaded property, selecting first item that passed check_self_arg()
# is a good approximation, long-term we should use check_call() inference below.
if not expanded.items:
# A broken overload, error should be already reported.
return AnyType(TypeOfAny.from_error)
expanded = expanded.items[0]
assert isinstance(expanded, CallableType), expanded
if var.is_settable_property and mx.is_lvalue and var.setter_type is not None:
if expanded.variables:
type_ctx = mx.rvalue or TempNode(AnyType(TypeOfAny.special_form), context=mx.context)
Expand Down
8 changes: 4 additions & 4 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,7 @@ def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
if isinstance(o.type, Overloaded):
# For error messages we link the original definition for each item.
for typ, item in zip(o.type.items, o.items):
if isinstance(item, Decorator):
typ.definition = item.func
typ.definition = item

def visit_decorator(self, d: Decorator) -> None:
if self.current_info is not None:
Expand All @@ -193,8 +192,9 @@ def visit_decorator(self, d: Decorator) -> None:
d.var.accept(self)
for node in d.decorators:
node.accept(self)
if isinstance(d.var.type, ProperType) and isinstance(d.var.type, CallableType):
d.var.type.definition = d.func
typ = d.var.type
if isinstance(typ, ProperType) and isinstance(typ, CallableType):
typ.definition = d.func

def visit_class_def(self, c: ClassDef) -> None:
for v in c.type_vars:
Expand Down
22 changes: 13 additions & 9 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
SymbolTable,
TypeInfo,
Var,
get_func_def,
reverse_builtin_aliases,
)
from mypy.operators import op_methods, op_methods_to_symbols
Expand Down Expand Up @@ -2938,10 +2939,11 @@ def format_single(arg: Type) -> str:

def pretty_class_or_static_decorator(tp: CallableType) -> str | None:
"""Return @classmethod or @staticmethod, if any, for the given callable type."""
if tp.definition is not None and isinstance(tp.definition, SYMBOL_FUNCBASE_TYPES):
if tp.definition.is_class:
definition = get_func_def(tp)
if definition is not None and isinstance(definition, SYMBOL_FUNCBASE_TYPES):
if definition.is_class:
return "@classmethod"
if tp.definition.is_static:
if definition.is_static:
return "@staticmethod"
return None

Expand Down Expand Up @@ -2991,12 +2993,13 @@ def [T <: int] f(self, x: int, y: T) -> None
slash = True

# If we got a "special arg" (i.e: self, cls, etc...), prepend it to the arg list
definition = get_func_def(tp)
if (
isinstance(tp.definition, FuncDef)
and hasattr(tp.definition, "arguments")
isinstance(definition, FuncDef)
and hasattr(definition, "arguments")
and not tp.from_concatenate
):
definition_arg_names = [arg.variable.name for arg in tp.definition.arguments]
definition_arg_names = [arg.variable.name for arg in definition.arguments]
if (
len(definition_arg_names) > len(tp.arg_names)
and definition_arg_names[0]
Expand All @@ -3005,7 +3008,7 @@ def [T <: int] f(self, x: int, y: T) -> None
if s:
s = ", " + s
s = definition_arg_names[0] + s
s = f"{tp.definition.name}({s})"
s = f"{definition.name}({s})"
elif tp.name:
first_arg = get_first_arg(tp)
if first_arg:
Expand Down Expand Up @@ -3051,9 +3054,10 @@ def [T <: int] f(self, x: int, y: T) -> None


def get_first_arg(tp: CallableType) -> str | None:
if not isinstance(tp.definition, FuncDef) or not tp.definition.info or tp.definition.is_static:
definition = get_func_def(tp)
if not isinstance(definition, FuncDef) or not definition.info or definition.is_static:
return None
return tp.definition.original_first_arg
return definition.original_first_arg


def variance_string(variance: int) -> str:
Expand Down
7 changes: 7 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4380,6 +4380,13 @@ def is_final_node(node: SymbolNode | None) -> bool:
return isinstance(node, (Var, FuncDef, OverloadedFuncDef, Decorator)) and node.is_final


def get_func_def(typ: mypy.types.CallableType) -> SymbolNode | None:
definition = typ.definition
if isinstance(definition, Decorator):
definition = definition.func
return definition


def local_definitions(
names: SymbolTable, name_prefix: str, info: TypeInfo | None = None
) -> Iterator[Definition]:
Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
bare_setter_type = self.analyze_property_with_multi_part_definition(defn)
typ = function_type(first_item.func, self.named_type("builtins.function"))
assert isinstance(typ, CallableType)
typ.definition = first_item
types = [typ]
else:
# This is a normal overload. Find the item signatures, the
Expand Down Expand Up @@ -1374,6 +1375,7 @@ def analyze_overload_sigs_and_impl(
if isinstance(item, Decorator):
callable = function_type(item.func, self.named_type("builtins.function"))
assert isinstance(callable, CallableType)
callable.definition = item
if not any(refers_to_fullname(dec, OVERLOAD_NAMES) for dec in item.decorators):
if i == len(defn.items) - 1 and not self.is_stub_file:
# Last item outside a stub is impl
Expand Down
3 changes: 3 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1887,6 +1887,9 @@ def __init__(
self.fallback = fallback
assert not name or "<bound method" not in name
self.name = name
# The rules for what exactly is considered a definition:
# * If it is a non-decorated function, FuncDef is the definition
# * If it is a decorated function, enclosing Decorator is the definition
self.definition = definition
self.variables = variables
self.is_ellipsis_args = is_ellipsis_args
Expand Down
47 changes: 47 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -9211,3 +9211,50 @@ class CGT(BG[T]): ...

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

[case testSettablePropertyAlias]
from typing import Any, TypeVar

class A:
@property
def prop(self: Any) -> str: ...
@prop.setter
def prop(self, val: str) -> None: ...

T = TypeVar("T")
class AT:
@property
def prop(self: T) -> T: ...
@prop.setter
def prop(self: T, val: list[T]) -> None: ...

class B:
prop: str
prop_t: str

class C(B):
prop = A.prop
prop_t = AT.prop # E: Incompatible types in assignment (expression has type "C", base class "B" defined the type as "str")

reveal_type(C().prop) # N: Revealed type is "builtins.str"
C().prop = "no" # E: Invalid self argument "C" to attribute function "prop" with type "Callable[[A, str], None]"
reveal_type(C().prop_t) # N: Revealed type is "__main__.C"
C().prop_t = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "list[C]")
[builtins fixtures/property.pyi]

[case testClassEqDecoratedAbstractNote]
from abc import abstractmethod

class C:
@abstractmethod
def __eq__(self, other: C) -> bool: ...
[builtins fixtures/plugin_attrs.pyi]
[out]
main:5: error: Argument 1 of "__eq__" is incompatible with supertype "builtins.object"; supertype defines the argument type as "object"
main:5: note: This violates the Liskov substitution principle
main:5: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
main:5: note: It is recommended for "__eq__" to work with arbitrary objects, for example:
main:5: note: def __eq__(self, other: object) -> bool:
main:5: note: if not isinstance(other, C):
main:5: note: return NotImplemented
main:5: note: return <logic to compare two C instances>
2 changes: 1 addition & 1 deletion test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -2055,7 +2055,7 @@ from dataclasses import dataclass, replace, InitVar
from typing import ClassVar

@dataclass
class A:
class A: # N: "replace" of "A" defined here
x: int
q: InitVar[int]
q2: InitVar[int] = 0
Expand Down
Loading
Loading