From a38f880f40e70649f976e80dacdb65d8a6d77c03 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Mar 2025 14:22:09 +0000 Subject: [PATCH] Fix crash on decorated getter in settable property --- mypy/checker.py | 16 ++++++++-- mypy/semanal.py | 10 +++++-- test-data/unit/check-classes.test | 50 ++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cd76eb1f916b..6d7e8fa215a1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -658,7 +658,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: assert isinstance(defn.items[1], Decorator) # Perform a reduced visit just to infer the actual setter type. self.visit_decorator_inner(defn.items[1], skip_first_item=True) - setter_type = get_proper_type(defn.items[1].var.type) + setter_type = defn.items[1].var.type # Check if the setter can accept two positional arguments. any_type = AnyType(TypeOfAny.special_form) fallback_setter_type = CallableType( @@ -670,6 +670,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: ) if setter_type and not is_subtype(setter_type, fallback_setter_type): self.fail("Invalid property setter signature", defn.items[1].func) + setter_type = self.extract_callable_type(setter_type, defn) if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2: # TODO: keep precise type for callables with tricky but valid signatures. setter_type = fallback_setter_type @@ -707,8 +708,17 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # We store the getter type as an overall overload type, as some # code paths are getting property type this way. assert isinstance(defn.items[0], Decorator) - var_type = get_proper_type(defn.items[0].var.type) - assert isinstance(var_type, CallableType) + var_type = self.extract_callable_type(defn.items[0].var.type, defn) + if not isinstance(var_type, CallableType): + # Construct a fallback type, invalid types should be already reported. + any_type = AnyType(TypeOfAny.special_form) + var_type = CallableType( + arg_types=[any_type], + arg_kinds=[ARG_POS], + arg_names=[None], + ret_type=any_type, + fallback=self.named_type("builtins.function"), + ) defn.type = Overloaded([var_type]) # Check override validity after we analyzed current definition. if defn.info: diff --git a/mypy/semanal.py b/mypy/semanal.py index 7acea5b2ab91..c48b65f0ee94 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1247,7 +1247,9 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: first_item.accept(self) bare_setter_type = None + is_property = False if isinstance(first_item, Decorator) and first_item.func.is_property: + is_property = True # This is a property. first_item.func.is_overload = True bare_setter_type = self.analyze_property_with_multi_part_definition(defn) @@ -1255,7 +1257,7 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: assert isinstance(typ, CallableType) types = [typ] else: - # This is an a normal overload. Find the item signatures, the + # This is a normal overload. Find the item signatures, the # implementation (if outside a stub), and any missing @overload # decorators. types, impl, non_overload_indexes = self.analyze_overload_sigs_and_impl(defn) @@ -1275,8 +1277,10 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if types and not any( # If some overload items are decorated with other decorators, then # the overload type will be determined during type checking. - isinstance(it, Decorator) and len(it.decorators) > 1 - for it in defn.items + # Note: bare @property is removed in visit_decorator(). + isinstance(it, Decorator) + and len(it.decorators) > (1 if i > 0 or not is_property else 0) + for i, it in enumerate(defn.items) ): # TODO: should we enforce decorated overloads consistency somehow? # Some existing code uses both styles: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 70cd84dd21ac..0da0f7c3bbcd 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8486,7 +8486,7 @@ class C: [builtins fixtures/property.pyi] [case testPropertySetterDecorated] -from typing import Callable, TypeVar +from typing import Callable, TypeVar, Generic class B: def __init__(self) -> None: @@ -8514,12 +8514,23 @@ class C(B): @deco_untyped def baz(self, x: int) -> None: ... + @property + def tricky(self) -> int: ... + @baz.setter + @deco_instance + def tricky(self, x: int) -> None: ... + c: C c.baz = "yes" # OK, because of untyped decorator +c.tricky = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "List[int]") T = TypeVar("T") def deco(fn: Callable[[T, int, int], None]) -> Callable[[T, int], None]: ... def deco_untyped(fn): ... + +class Wrapper(Generic[T]): + def __call__(self, s: T, x: list[int]) -> None: ... +def deco_instance(fn: Callable[[T, int], None]) -> Wrapper[T]: ... [builtins fixtures/property.pyi] [case testPropertyDeleterBodyChecked] @@ -8538,3 +8549,40 @@ class C: def bar(self) -> None: 1() # E: "int" not callable [builtins fixtures/property.pyi] + +[case testSettablePropertyGetterDecorated] +from typing import Callable, TypeVar, Generic + +class C: + @property + @deco + def foo(self, ok: int) -> str: ... + @foo.setter + def foo(self, x: str) -> None: ... + + @property + @deco_instance + def bar(self, ok: int) -> int: ... + @bar.setter + def bar(self, x: int) -> None: ... + + @property + @deco_untyped + def baz(self) -> int: ... + @baz.setter + def baz(self, x: int) -> None: ... + +c: C +reveal_type(c.foo) # N: Revealed type is "builtins.list[builtins.str]" +reveal_type(c.bar) # N: Revealed type is "builtins.list[builtins.int]" +reveal_type(c.baz) # N: Revealed type is "Any" + +T = TypeVar("T") +R = TypeVar("R") +def deco(fn: Callable[[T, int], R]) -> Callable[[T], list[R]]: ... +def deco_untyped(fn): ... + +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]