Skip to content

Fix crash on decorated getter in settable property #18787

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 1 commit into from
Mar 11, 2025
Merged
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
16 changes: 13 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
@@ -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:
10 changes: 7 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
@@ -1247,15 +1247,17 @@ 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)
typ = function_type(first_item.func, self.named_type("builtins.function"))
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:
50 changes: 49 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
@@ -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

Choose a reason for hiding this comment

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

It feels like this should probably be @tricky.setter. Unless this is intentional, but it definitely looks like a copy/paste issue.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks that way. Thanks for letting us know! Opened #18946 to fix it.

@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]