Skip to content

Commit af5186e

Browse files
authored
Consistently store settable property type (#18774)
Fixes #18764 There are two things important to understand this PR: * First, mypy has a performance optimization - we store decorator/overload type during semantic analysis in certain "trivial" situations (to avoid deferrals). Otherwise, we infer the type of decorated function or an overload variant during type checking. * Second, for settable properties we store getter type in two places, as a `Var.type` of getter (as a decorator), and also in overall `OverloadedFuncDef.type`. The latter is ugly, but unfortunately it is hard to get rid of, since some code in multiple plugins rely on this. It turns out there are _three_ inconsistencies in how these two things interact (first one causes the actual crash): * For trivial settable properties (i.e. without extra decorators) when we store the type in `semanal.py` we only store it the second way (i.e. as `OverloadedFuncDef.type`). * For non-trivial settable properties (where getter and/or setter are themselves decorated), we only set the inferred type the first way (as `Var.type`). * When inferring setter type (unlike getter, that is handled correctly) we actually ignore any extra decorators (this is probably quire niche, but still inconsistent). Essentially I simply remove these inconsistencies.
1 parent 830a0fa commit af5186e

File tree

3 files changed

+141
-22
lines changed

3 files changed

+141
-22
lines changed

mypy/checker.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -654,23 +654,34 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
654654
assert isinstance(defn.items[0], Decorator)
655655
self.visit_decorator(defn.items[0])
656656
if defn.items[0].var.is_settable_property:
657+
# TODO: here and elsewhere we assume setter immediately follows getter.
657658
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:
659+
# Perform a reduced visit just to infer the actual setter type.
660+
self.visit_decorator_inner(defn.items[1], skip_first_item=True)
661+
setter_type = get_proper_type(defn.items[1].var.type)
662+
# Check if the setter can accept two positional arguments.
663+
any_type = AnyType(TypeOfAny.special_form)
664+
fallback_setter_type = CallableType(
665+
arg_types=[any_type, any_type],
666+
arg_kinds=[ARG_POS, ARG_POS],
667+
arg_names=[None, None],
668+
ret_type=any_type,
669+
fallback=self.named_type("builtins.function"),
670+
)
671+
if setter_type and not is_subtype(setter_type, fallback_setter_type):
662672
self.fail("Invalid property setter signature", defn.items[1].func)
663-
any_type = AnyType(TypeOfAny.from_error)
664-
setter_type = setter_type.copy_modified(
665-
arg_types=[any_type, any_type],
666-
arg_kinds=[ARG_POS, ARG_POS],
667-
arg_names=[None, None],
668-
)
673+
if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2:
674+
# TODO: keep precise type for callables with tricky but valid signatures.
675+
setter_type = fallback_setter_type
669676
defn.items[0].var.setter_type = setter_type
670-
for fdef in defn.items:
677+
for i, fdef in enumerate(defn.items):
671678
assert isinstance(fdef, Decorator)
672679
if defn.is_property:
673-
self.check_func_item(fdef.func, name=fdef.func.name, allow_empty=True)
680+
assert isinstance(defn.items[0], Decorator)
681+
settable = defn.items[0].var.is_settable_property
682+
# Do not visit the second time the items we checked above.
683+
if (settable and i > 1) or (not settable and i > 0):
684+
self.check_func_item(fdef.func, name=fdef.func.name, allow_empty=True)
674685
else:
675686
# Perform full check for real overloads to infer type of all decorated
676687
# overload variants.
@@ -692,6 +703,13 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
692703
item_types.append(item_type)
693704
if item_types:
694705
defn.type = Overloaded(item_types)
706+
elif defn.type is None:
707+
# We store the getter type as an overall overload type, as some
708+
# code paths are getting property type this way.
709+
assert isinstance(defn.items[0], Decorator)
710+
var_type = get_proper_type(defn.items[0].var.type)
711+
assert isinstance(var_type, CallableType)
712+
defn.type = Overloaded([var_type])
695713
# Check override validity after we analyzed current definition.
696714
if defn.info:
697715
found_method_base_classes = self.check_method_override(defn)
@@ -5277,25 +5295,34 @@ def visit_decorator(self, e: Decorator) -> None:
52775295
return
52785296
self.visit_decorator_inner(e)
52795297

5280-
def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None:
5298+
def visit_decorator_inner(
5299+
self, e: Decorator, allow_empty: bool = False, skip_first_item: bool = False
5300+
) -> None:
52815301
if self.recurse_into_functions:
52825302
with self.tscope.function_scope(e.func):
52835303
self.check_func_item(e.func, name=e.func.name, allow_empty=allow_empty)
52845304

52855305
# Process decorators from the inside out to determine decorated signature, which
52865306
# may be different from the declared signature.
52875307
sig: Type = self.function_type(e.func)
5288-
for d in reversed(e.decorators):
5308+
non_trivial_decorator = False
5309+
# For settable properties skip the first decorator (that is @foo.setter).
5310+
for d in reversed(e.decorators[1:] if skip_first_item else e.decorators):
5311+
if refers_to_fullname(d, "abc.abstractmethod"):
5312+
# This is a hack to avoid spurious errors because of incomplete type
5313+
# of @abstractmethod in the test fixtures.
5314+
continue
52895315
if refers_to_fullname(d, OVERLOAD_NAMES):
52905316
if not allow_empty:
52915317
self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, e)
52925318
continue
5319+
non_trivial_decorator = True
52935320
dec = self.expr_checker.accept(d)
52945321
temp = self.temp_node(sig, context=d)
52955322
fullname = None
52965323
if isinstance(d, RefExpr):
52975324
fullname = d.fullname or None
5298-
# if this is a expression like @b.a where b is an object, get the type of b
5325+
# if this is an expression like @b.a where b is an object, get the type of b,
52995326
# so we can pass it the method hook in the plugins
53005327
object_type: Type | None = None
53015328
if fullname is None and isinstance(d, MemberExpr) and self.has_type(d.expr):
@@ -5305,7 +5332,8 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None
53055332
sig, t2 = self.expr_checker.check_call(
53065333
dec, [temp], [nodes.ARG_POS], e, callable_name=fullname, object_type=object_type
53075334
)
5308-
self.check_untyped_after_decorator(sig, e.func)
5335+
if non_trivial_decorator:
5336+
self.check_untyped_after_decorator(sig, e.func)
53095337
sig = set_callable_name(sig, e.func)
53105338
e.var.type = sig
53115339
e.var.is_ready = True
@@ -5314,8 +5342,8 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None
53145342
if len([k for k in sig.arg_kinds if k.is_required()]) > 1:
53155343
self.msg.fail("Too many arguments for property", e)
53165344
self.check_incompatible_property_override(e)
5317-
# For overloaded functions we already checked override for overload as a whole.
5318-
if allow_empty:
5345+
# For overloaded functions/properties we already checked override for overload as a whole.
5346+
if allow_empty or skip_first_item:
53195347
return
53205348
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
53215349
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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8464,3 +8464,77 @@ 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+
@property
8512+
def baz(self) -> int: ...
8513+
@baz.setter
8514+
@deco_untyped
8515+
def baz(self, x: int) -> None: ...
8516+
8517+
c: C
8518+
c.baz = "yes" # OK, because of untyped decorator
8519+
8520+
T = TypeVar("T")
8521+
def deco(fn: Callable[[T, int, int], None]) -> Callable[[T, int], None]: ...
8522+
def deco_untyped(fn): ...
8523+
[builtins fixtures/property.pyi]
8524+
8525+
[case testPropertyDeleterBodyChecked]
8526+
class C:
8527+
@property
8528+
def foo(self) -> int: ...
8529+
@foo.deleter
8530+
def foo(self) -> None:
8531+
1() # E: "int" not callable
8532+
8533+
@property
8534+
def bar(self) -> int: ...
8535+
@bar.setter
8536+
def bar(self, x: str) -> None: ...
8537+
@bar.deleter
8538+
def bar(self) -> None:
8539+
1() # E: "int" not callable
8540+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)