Skip to content

Commit 0f5ddd5

Browse files
authored
Fix crash on decorated getter in settable property (#18787)
Follow up for #18774 Fix for crash is trivial, properly handle getter the same way as setter. Note I also consistently handle callable instances.
1 parent 2b176ab commit 0f5ddd5

File tree

3 files changed

+69
-7
lines changed

3 files changed

+69
-7
lines changed

mypy/checker.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
658658
assert isinstance(defn.items[1], Decorator)
659659
# Perform a reduced visit just to infer the actual setter type.
660660
self.visit_decorator_inner(defn.items[1], skip_first_item=True)
661-
setter_type = get_proper_type(defn.items[1].var.type)
661+
setter_type = defn.items[1].var.type
662662
# Check if the setter can accept two positional arguments.
663663
any_type = AnyType(TypeOfAny.special_form)
664664
fallback_setter_type = CallableType(
@@ -670,6 +670,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
670670
)
671671
if setter_type and not is_subtype(setter_type, fallback_setter_type):
672672
self.fail("Invalid property setter signature", defn.items[1].func)
673+
setter_type = self.extract_callable_type(setter_type, defn)
673674
if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2:
674675
# TODO: keep precise type for callables with tricky but valid signatures.
675676
setter_type = fallback_setter_type
@@ -707,8 +708,17 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
707708
# We store the getter type as an overall overload type, as some
708709
# code paths are getting property type this way.
709710
assert isinstance(defn.items[0], Decorator)
710-
var_type = get_proper_type(defn.items[0].var.type)
711-
assert isinstance(var_type, CallableType)
711+
var_type = self.extract_callable_type(defn.items[0].var.type, defn)
712+
if not isinstance(var_type, CallableType):
713+
# Construct a fallback type, invalid types should be already reported.
714+
any_type = AnyType(TypeOfAny.special_form)
715+
var_type = CallableType(
716+
arg_types=[any_type],
717+
arg_kinds=[ARG_POS],
718+
arg_names=[None],
719+
ret_type=any_type,
720+
fallback=self.named_type("builtins.function"),
721+
)
712722
defn.type = Overloaded([var_type])
713723
# Check override validity after we analyzed current definition.
714724
if defn.info:

mypy/semanal.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -1247,15 +1247,17 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
12471247
first_item.accept(self)
12481248

12491249
bare_setter_type = None
1250+
is_property = False
12501251
if isinstance(first_item, Decorator) and first_item.func.is_property:
1252+
is_property = True
12511253
# This is a property.
12521254
first_item.func.is_overload = True
12531255
bare_setter_type = self.analyze_property_with_multi_part_definition(defn)
12541256
typ = function_type(first_item.func, self.named_type("builtins.function"))
12551257
assert isinstance(typ, CallableType)
12561258
types = [typ]
12571259
else:
1258-
# This is an a normal overload. Find the item signatures, the
1260+
# This is a normal overload. Find the item signatures, the
12591261
# implementation (if outside a stub), and any missing @overload
12601262
# decorators.
12611263
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:
12751277
if types and not any(
12761278
# If some overload items are decorated with other decorators, then
12771279
# the overload type will be determined during type checking.
1278-
isinstance(it, Decorator) and len(it.decorators) > 1
1279-
for it in defn.items
1280+
# Note: bare @property is removed in visit_decorator().
1281+
isinstance(it, Decorator)
1282+
and len(it.decorators) > (1 if i > 0 or not is_property else 0)
1283+
for i, it in enumerate(defn.items)
12801284
):
12811285
# TODO: should we enforce decorated overloads consistency somehow?
12821286
# Some existing code uses both styles:

test-data/unit/check-classes.test

+49-1
Original file line numberDiff line numberDiff line change
@@ -8486,7 +8486,7 @@ class C:
84868486
[builtins fixtures/property.pyi]
84878487

84888488
[case testPropertySetterDecorated]
8489-
from typing import Callable, TypeVar
8489+
from typing import Callable, TypeVar, Generic
84908490

84918491
class B:
84928492
def __init__(self) -> None:
@@ -8514,12 +8514,23 @@ class C(B):
85148514
@deco_untyped
85158515
def baz(self, x: int) -> None: ...
85168516

8517+
@property
8518+
def tricky(self) -> int: ...
8519+
@baz.setter
8520+
@deco_instance
8521+
def tricky(self, x: int) -> None: ...
8522+
85178523
c: C
85188524
c.baz = "yes" # OK, because of untyped decorator
8525+
c.tricky = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "List[int]")
85198526

85208527
T = TypeVar("T")
85218528
def deco(fn: Callable[[T, int, int], None]) -> Callable[[T, int], None]: ...
85228529
def deco_untyped(fn): ...
8530+
8531+
class Wrapper(Generic[T]):
8532+
def __call__(self, s: T, x: list[int]) -> None: ...
8533+
def deco_instance(fn: Callable[[T, int], None]) -> Wrapper[T]: ...
85238534
[builtins fixtures/property.pyi]
85248535

85258536
[case testPropertyDeleterBodyChecked]
@@ -8538,3 +8549,40 @@ class C:
85388549
def bar(self) -> None:
85398550
1() # E: "int" not callable
85408551
[builtins fixtures/property.pyi]
8552+
8553+
[case testSettablePropertyGetterDecorated]
8554+
from typing import Callable, TypeVar, Generic
8555+
8556+
class C:
8557+
@property
8558+
@deco
8559+
def foo(self, ok: int) -> str: ...
8560+
@foo.setter
8561+
def foo(self, x: str) -> None: ...
8562+
8563+
@property
8564+
@deco_instance
8565+
def bar(self, ok: int) -> int: ...
8566+
@bar.setter
8567+
def bar(self, x: int) -> None: ...
8568+
8569+
@property
8570+
@deco_untyped
8571+
def baz(self) -> int: ...
8572+
@baz.setter
8573+
def baz(self, x: int) -> None: ...
8574+
8575+
c: C
8576+
reveal_type(c.foo) # N: Revealed type is "builtins.list[builtins.str]"
8577+
reveal_type(c.bar) # N: Revealed type is "builtins.list[builtins.int]"
8578+
reveal_type(c.baz) # N: Revealed type is "Any"
8579+
8580+
T = TypeVar("T")
8581+
R = TypeVar("R")
8582+
def deco(fn: Callable[[T, int], R]) -> Callable[[T], list[R]]: ...
8583+
def deco_untyped(fn): ...
8584+
8585+
class Wrapper(Generic[T, R]):
8586+
def __call__(self, s: T) -> list[R]: ...
8587+
def deco_instance(fn: Callable[[T, int], R]) -> Wrapper[T, R]: ...
8588+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)