From f7b9bad33cc2f6083d8ba8e2665fae3b457930e9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 8 Aug 2025 14:35:45 +0100 Subject: [PATCH 1/3] Fix crash on settable property alias --- mypy/checker.py | 21 +++++- mypy/checkmember.py | 9 ++- mypy/fixup.py | 8 +-- mypy/messages.py | 28 +++++--- mypy/semanal.py | 2 + mypy/types.py | 3 + test-data/unit/check-classes.test | 30 ++++++++ test-data/unit/check-dataclasses.test | 2 +- test-data/unit/fine-grained.test | 98 +-------------------------- test-data/unit/pythoneval.test | 1 + 10 files changed, 88 insertions(+), 114 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 68f9bd4c1383..6227a80ed738 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -703,6 +703,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: @@ -730,7 +736,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) @@ -3509,6 +3515,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 @@ -4494,6 +4501,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) + 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. @@ -5356,6 +5365,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: @@ -8651,8 +8662,10 @@ 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): @@ -8660,8 +8673,10 @@ def is_classmethod_node(node: Node | None) -> bool | None: 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): diff --git a/mypy/checkmember.py b/mypy/checkmember.py index da67591a4553..1af2f4b88910 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -976,7 +976,14 @@ 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. + 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) if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: if expanded.variables: diff --git a/mypy/fixup.py b/mypy/fixup.py index 0007fe8faabf..18bdc1c6f497 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -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: @@ -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: diff --git a/mypy/messages.py b/mypy/messages.py index 6b55da59d183..482c6c41e40a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -44,6 +44,7 @@ CallExpr, ClassDef, Context, + Decorator, Expression, FuncDef, IndexExpr, @@ -2938,10 +2939,13 @@ 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 = tp.definition + if isinstance(definition, Decorator): + definition = definition.func + 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 @@ -2991,12 +2995,15 @@ 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 = tp.definition + if isinstance(definition, Decorator): + definition = definition.func 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] @@ -3005,7 +3012,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: @@ -3051,9 +3058,12 @@ 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 = tp.definition + if isinstance(definition, Decorator): + definition = definition.func + 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: diff --git a/mypy/semanal.py b/mypy/semanal.py index 99d1eb36e788..fb66fb5158db 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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 @@ -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 diff --git a/mypy/types.py b/mypy/types.py index b4771b15f77a..a73ac3c3524a 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1887,6 +1887,9 @@ def __init__( self.fallback = fallback assert not name or " __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] diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index a6ac30e20c36..f43c49c200c8 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -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 diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index c25ed79e7356..84734e2c27bf 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7312,9 +7312,7 @@ class C: == mod.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testOverloadedMethodSupertype-only_when_cache] --- Different cache/no-cache tests because --- CallableType.def_extras.first_arg differs ("self"/None) +[case testOverloadedMethodSupertype] from typing import overload, Any import b class Child(b.Parent): @@ -7355,49 +7353,6 @@ main:4: note: def f(self, arg: int) -> int main:4: note: @overload main:4: note: def f(self, arg: str) -> str -[case testOverloadedMethodSupertype2-only_when_nocache] --- Different cache/no-cache tests because --- CallableType.def_extras.first_arg differs ("self"/None) -from typing import overload, Any -import b -class Child(b.Parent): - @overload # Fail - def f(self, arg: int) -> int: ... - @overload - def f(self, arg: str) -> str: ... - def f(self, arg: Any) -> Any: ... -[file b.py] -from typing import overload, Any -class C: pass -class Parent: - @overload - def f(self, arg: int) -> int: ... - @overload - def f(self, arg: str) -> str: ... - def f(self, arg: Any) -> Any: ... -[file b.py.2] -from typing import overload, Any -class C: pass -class Parent: - @overload - def f(self, arg: int) -> int: ... - @overload - def f(self, arg: str) -> C: ... - def f(self, arg: Any) -> Any: ... -[out] -== -main:4: error: Signature of "f" incompatible with supertype "b.Parent" -main:4: note: Superclass: -main:4: note: @overload -main:4: note: def f(self, arg: int) -> int -main:4: note: @overload -main:4: note: def f(self, arg: str) -> C -main:4: note: Subclass: -main:4: note: @overload -main:4: note: def f(arg: int) -> int -main:4: note: @overload -main:4: note: def f(arg: str) -> str - [case testOverloadedInitSupertype] import a [file a.py] @@ -8486,9 +8441,7 @@ class D: == a.py:3: error: Cannot override final attribute "meth" (previously declared in base class "C") -[case testFinalBodyReprocessedAndStillFinalOverloaded-only_when_cache] --- Different cache/no-cache tests because --- CallableType.def_extras.first_arg differs ("self"/None) +[case testFinalBodyReprocessedAndStillFinalOverloaded] import a [file a.py] from c import C @@ -8533,53 +8486,6 @@ a.py:3: note: def meth(self, x: str) -> str a.py:3: note: Subclass: a.py:3: note: def meth(self) -> None -[case testFinalBodyReprocessedAndStillFinalOverloaded2-only_when_nocache] --- Different cache/no-cache tests because --- CallableType.def_extras.first_arg differs ("self"/None) -import a -[file a.py] -from c import C -class A: - def meth(self) -> None: ... - -[file a.py.3] -from c import C -class A(C): - def meth(self) -> None: ... - -[file c.py] -from typing import final, overload, Union -from d import D - -class C: - @overload - def meth(self, x: int) -> int: ... - @overload - def meth(self, x: str) -> str: ... - @final - def meth(self, x: Union[int, str]) -> Union[int, str]: - D(int()) - return x -[file d.py] -class D: - def __init__(self, x: int) -> None: ... -[file d.py.2] -from typing import Optional -class D: - def __init__(self, x: Optional[int]) -> None: ... -[out] -== -== -a.py:3: error: Cannot override final attribute "meth" (previously declared in base class "C") -a.py:3: error: Signature of "meth" incompatible with supertype "c.C" -a.py:3: note: Superclass: -a.py:3: note: @overload -a.py:3: note: def meth(x: int) -> int -a.py:3: note: @overload -a.py:3: note: def meth(x: str) -> str -a.py:3: note: Subclass: -a.py:3: note: def meth(self) -> None - [case testIfMypyUnreachableClass] from a import x diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 4bd94dfce03e..9b5d8a1ac54c 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1970,6 +1970,7 @@ a2 = replace() a2 = replace(a, x='spam') a2 = replace(a, x=42, q=42) [out] +_testDataclassReplace.py:4: note: "replace" of "A" defined here _testDataclassReplace.py:9: note: Revealed type is "_testDataclassReplace.A" _testDataclassReplace.py:10: error: Too few arguments for "replace" _testDataclassReplace.py:11: error: Argument "x" to "replace" of "A" has incompatible type "str"; expected "int" From 4749d4a98503fca8dad4ad7aa6c8914c569d8bc5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 8 Aug 2025 17:57:43 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> --- mypy/checker.py | 2 +- mypy/checkmember.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6227a80ed738..0fb44ff34f3e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4501,7 +4501,7 @@ 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) + 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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 1af2f4b88910..2c41f2e273cc 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -984,7 +984,7 @@ def expand_and_bind_callable( # A broken overload, error should be already reported. return AnyType(TypeOfAny.from_error) expanded = expanded.items[0] - assert isinstance(expanded, CallableType) + 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) From c6cd357f85aa00c9e9ce1d2b1f92f34088abb4fe Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 8 Aug 2025 18:21:02 +0100 Subject: [PATCH 3/3] Fix __eq__ note --- mypy/checker.py | 6 ++++-- mypy/messages.py | 14 ++++---------- mypy/nodes.py | 7 +++++++ test-data/unit/check-classes.test | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0fb44ff34f3e..35407a676ad8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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 @@ -2507,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) diff --git a/mypy/messages.py b/mypy/messages.py index 482c6c41e40a..f626d4c71916 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -44,7 +44,6 @@ CallExpr, ClassDef, Context, - Decorator, Expression, FuncDef, IndexExpr, @@ -56,6 +55,7 @@ SymbolTable, TypeInfo, Var, + get_func_def, reverse_builtin_aliases, ) from mypy.operators import op_methods, op_methods_to_symbols @@ -2939,9 +2939,7 @@ 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.""" - definition = tp.definition - if isinstance(definition, Decorator): - definition = definition.func + definition = get_func_def(tp) if definition is not None and isinstance(definition, SYMBOL_FUNCBASE_TYPES): if definition.is_class: return "@classmethod" @@ -2995,9 +2993,7 @@ 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 = tp.definition - if isinstance(definition, Decorator): - definition = definition.func + definition = get_func_def(tp) if ( isinstance(definition, FuncDef) and hasattr(definition, "arguments") @@ -3058,9 +3054,7 @@ def [T <: int] f(self, x: int, y: T) -> None def get_first_arg(tp: CallableType) -> str | None: - definition = tp.definition - if isinstance(definition, Decorator): - definition = definition.func + definition = get_func_def(tp) if not isinstance(definition, FuncDef) or not definition.info or definition.is_static: return None return definition.original_first_arg diff --git a/mypy/nodes.py b/mypy/nodes.py index 9d5867c5371d..99b9bf72c948 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -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]: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 990c5ccd3d6e..62f538260fff 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9241,3 +9241,20 @@ C().prop = "no" # E: Invalid self argument "C" to attribute function "prop" wit 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