Skip to content

Commit da033a3

Browse files
committed
Consistently store settable property type
1 parent 830a0fa commit da033a3

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

mypy/checker.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -655,13 +655,15 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
655655
self.visit_decorator(defn.items[0])
656656
if defn.items[0].var.is_settable_property:
657657
assert isinstance(defn.items[1], Decorator)
658-
self.visit_func_def(defn.items[1].func)
659-
setter_type = self.function_type(defn.items[1].func)
660-
assert isinstance(setter_type, CallableType)
661-
if len(setter_type.arg_types) != 2:
658+
# Perform a reduced visit just to infer the actual setter type.
659+
self.visit_decorator_inner(defn.items[1], skip_first_item=True)
660+
setter_type = get_proper_type(defn.items[1].var.type)
661+
if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2:
662662
self.fail("Invalid property setter signature", defn.items[1].func)
663663
any_type = AnyType(TypeOfAny.from_error)
664-
setter_type = setter_type.copy_modified(
664+
fallback_setter_type = self.function_type(defn.items[1].func)
665+
assert isinstance(fallback_setter_type, CallableType)
666+
setter_type = fallback_setter_type.copy_modified(
665667
arg_types=[any_type, any_type],
666668
arg_kinds=[ARG_POS, ARG_POS],
667669
arg_names=[None, None],
@@ -692,6 +694,13 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
692694
item_types.append(item_type)
693695
if item_types:
694696
defn.type = Overloaded(item_types)
697+
elif defn.type is None:
698+
# We store the getter type as an overall overload type, as some
699+
# code paths are getting property type this way.
700+
assert isinstance(defn.items[0], Decorator)
701+
var_type = get_proper_type(defn.items[0].var.type)
702+
assert isinstance(var_type, CallableType)
703+
defn.type = Overloaded([var_type])
695704
# Check override validity after we analyzed current definition.
696705
if defn.info:
697706
found_method_base_classes = self.check_method_override(defn)
@@ -5277,15 +5286,22 @@ def visit_decorator(self, e: Decorator) -> None:
52775286
return
52785287
self.visit_decorator_inner(e)
52795288

5280-
def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None:
5289+
def visit_decorator_inner(
5290+
self, e: Decorator, allow_empty: bool = False, skip_first_item: bool = False
5291+
) -> None:
52815292
if self.recurse_into_functions:
52825293
with self.tscope.function_scope(e.func):
52835294
self.check_func_item(e.func, name=e.func.name, allow_empty=allow_empty)
52845295

52855296
# Process decorators from the inside out to determine decorated signature, which
52865297
# may be different from the declared signature.
52875298
sig: Type = self.function_type(e.func)
5288-
for d in reversed(e.decorators):
5299+
# For settable properties skip the first decorator (that is @foo.setter).
5300+
for d in reversed(e.decorators[1:] if skip_first_item else e.decorators):
5301+
if refers_to_fullname(d, "abc.abstractmethod"):
5302+
# This is a hack to avoid spurious errors because of incomplete type
5303+
# of @abstractmethod in the test fixtures.
5304+
continue
52895305
if refers_to_fullname(d, OVERLOAD_NAMES):
52905306
if not allow_empty:
52915307
self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, e)
@@ -5314,8 +5330,8 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None
53145330
if len([k for k in sig.arg_kinds if k.is_required()]) > 1:
53155331
self.msg.fail("Too many arguments for property", e)
53165332
self.check_incompatible_property_override(e)
5317-
# For overloaded functions we already checked override for overload as a whole.
5318-
if allow_empty:
5333+
# For overloaded functions/properties we already checked override for overload as a whole.
5334+
if allow_empty or skip_first_item:
53195335
return
53205336
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
53215337
found_method_base_classes = self.check_method_override(e)

mypy/semanal.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,10 +1246,11 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
12461246
with self.overload_item_set(0):
12471247
first_item.accept(self)
12481248

1249+
bare_setter_type = None
12491250
if isinstance(first_item, Decorator) and first_item.func.is_property:
12501251
# This is a property.
12511252
first_item.func.is_overload = True
1252-
self.analyze_property_with_multi_part_definition(defn)
1253+
bare_setter_type = self.analyze_property_with_multi_part_definition(defn)
12531254
typ = function_type(first_item.func, self.named_type("builtins.function"))
12541255
assert isinstance(typ, CallableType)
12551256
types = [typ]
@@ -1283,6 +1284,11 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
12831284
# * Put decorator everywhere, use "bare" types in overloads.
12841285
defn.type = Overloaded(types)
12851286
defn.type.line = defn.line
1287+
# In addition, we can set the getter/setter type for valid properties as some
1288+
# code paths may either use the above type, or var.type etc. of the first item.
1289+
if isinstance(first_item, Decorator) and bare_setter_type:
1290+
first_item.var.type = types[0]
1291+
first_item.var.setter_type = bare_setter_type
12861292

12871293
if not defn.items:
12881294
# It was not a real overload after all, but function redefinition. We've
@@ -1502,26 +1508,37 @@ def process_static_or_class_method_in_overload(self, defn: OverloadedFuncDef) ->
15021508
defn.is_class = class_status[0]
15031509
defn.is_static = static_status[0]
15041510

1505-
def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -> None:
1511+
def analyze_property_with_multi_part_definition(
1512+
self, defn: OverloadedFuncDef
1513+
) -> CallableType | None:
15061514
"""Analyze a property defined using multiple methods (e.g., using @x.setter).
15071515
15081516
Assume that the first method (@property) has already been analyzed.
1517+
Return bare setter type (without any other decorators applied), this may be used
1518+
by the caller for performance optimizations.
15091519
"""
15101520
defn.is_property = True
15111521
items = defn.items
15121522
first_item = defn.items[0]
15131523
assert isinstance(first_item, Decorator)
15141524
deleted_items = []
1525+
bare_setter_type = None
15151526
for i, item in enumerate(items[1:]):
15161527
if isinstance(item, Decorator):
1517-
if len(item.decorators) >= 1:
1528+
item.func.accept(self)
1529+
if item.decorators:
15181530
first_node = item.decorators[0]
15191531
if isinstance(first_node, MemberExpr):
15201532
if first_node.name == "setter":
15211533
# The first item represents the entire property.
15221534
first_item.var.is_settable_property = True
15231535
# Get abstractness from the original definition.
15241536
item.func.abstract_status = first_item.func.abstract_status
1537+
setter_func_type = function_type(
1538+
item.func, self.named_type("builtins.function")
1539+
)
1540+
assert isinstance(setter_func_type, CallableType)
1541+
bare_setter_type = setter_func_type
15251542
if first_node.name == "deleter":
15261543
item.func.abstract_status = first_item.func.abstract_status
15271544
for other_node in item.decorators[1:]:
@@ -1530,7 +1547,6 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
15301547
self.fail(
15311548
f"Only supported top decorator is @{first_item.func.name}.setter", item
15321549
)
1533-
item.func.accept(self)
15341550
else:
15351551
self.fail(f'Unexpected definition for property "{first_item.func.name}"', item)
15361552
deleted_items.append(i + 1)
@@ -1544,6 +1560,7 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
15441560
item.func.deprecated = (
15451561
f"function {item.fullname} is deprecated: {deprecated}"
15461562
)
1563+
return bare_setter_type
15471564

15481565
def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None:
15491566
if self.is_class_scope():

test-data/unit/check-classes.test

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8464,3 +8464,50 @@ def deco(fn: Callable[[], list[T]]) -> Callable[[], T]: ...
84648464
@deco
84658465
def f() -> list[str]: ...
84668466
[builtins fixtures/property.pyi]
8467+
8468+
[case testPropertySetterSuperclassDeferred2]
8469+
import a
8470+
[file a.py]
8471+
import b
8472+
class D(b.C):
8473+
@property
8474+
def foo(self) -> str: ...
8475+
@foo.setter # E: Incompatible override of a setter type \
8476+
# N: (base class "C" defined the type as "str", \
8477+
# N: override has type "int")
8478+
def foo(self, x: int) -> None: ...
8479+
[file b.py]
8480+
from a import D
8481+
class C:
8482+
@property
8483+
def foo(self) -> str: ...
8484+
@foo.setter
8485+
def foo(self, x: str) -> None: ...
8486+
[builtins fixtures/property.pyi]
8487+
8488+
[case testPropertySetterDecorated]
8489+
from typing import Callable, TypeVar
8490+
8491+
class B:
8492+
def __init__(self) -> None:
8493+
self.foo: str
8494+
self.bar: int
8495+
8496+
class C(B):
8497+
@property
8498+
def foo(self) -> str: ...
8499+
@foo.setter # E: Incompatible override of a setter type \
8500+
# N: (base class "B" defined the type as "str", \
8501+
# N: override has type "int")
8502+
@deco
8503+
def foo(self, x: int, y: int) -> None: ...
8504+
8505+
@property
8506+
def bar(self) -> int: ...
8507+
@bar.setter
8508+
@deco
8509+
def bar(self, x: int, y: int) -> None: ...
8510+
8511+
T = TypeVar("T")
8512+
def deco(fn: Callable[[T, int, int], None]) -> Callable[[T, int], None]: ...
8513+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)