Skip to content

Commit f7b9bad

Browse files
committed
Fix crash on settable property alias
1 parent db67fac commit f7b9bad

File tree

10 files changed

+88
-114
lines changed

10 files changed

+88
-114
lines changed

mypy/checker.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
703703
# TODO: keep precise type for callables with tricky but valid signatures.
704704
setter_type = fallback_setter_type
705705
defn.items[0].var.setter_type = setter_type
706+
if isinstance(defn.type, Overloaded):
707+
# Update legacy property type for decorated properties.
708+
getter_type = self.extract_callable_type(defn.items[0].var.type, defn)
709+
if getter_type is not None:
710+
getter_type.definition = defn.items[0]
711+
defn.type.items[0] = getter_type
706712
for i, fdef in enumerate(defn.items):
707713
assert isinstance(fdef, Decorator)
708714
if defn.is_property:
@@ -730,7 +736,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
730736
assert isinstance(item, Decorator)
731737
item_type = self.extract_callable_type(item.var.type, item)
732738
if item_type is not None:
733-
item_type.definition = item.func
739+
item_type.definition = item
734740
item_types.append(item_type)
735741
if item_types:
736742
defn.type = Overloaded(item_types)
@@ -3509,6 +3515,7 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) ->
35093515
continue
35103516

35113517
base_type, base_node = self.node_type_from_base(lvalue_node.name, base, lvalue)
3518+
# TODO: if the r.h.s. is a descriptor, we should check setter override as well.
35123519
custom_setter = is_custom_settable_property(base_node)
35133520
if isinstance(base_type, PartialType):
35143521
base_type = None
@@ -4494,6 +4501,8 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
44944501
if isinstance(p_type, Overloaded):
44954502
# TODO: in theory we can have a property with a deleter only.
44964503
var.is_settable_property = True
4504+
assert isinstance(definition, Decorator)
4505+
var.setter_type = definition.var.setter_type
44974506

44984507
def set_inference_error_fallback_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
44994508
"""Store best known type for variable if type inference failed.
@@ -5356,6 +5365,8 @@ def visit_decorator_inner(
53565365
self.check_untyped_after_decorator(sig, e.func)
53575366
self.require_correct_self_argument(sig, e.func)
53585367
sig = set_callable_name(sig, e.func)
5368+
if isinstance(sig, CallableType):
5369+
sig.definition = e
53595370
e.var.type = sig
53605371
e.var.is_ready = True
53615372
if e.func.is_property:
@@ -8651,17 +8662,21 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
86518662
return t.copy_modified(args=[a.accept(self) for a in t.args])
86528663

86538664

8654-
def is_classmethod_node(node: Node | None) -> bool | None:
8665+
def is_classmethod_node(node: SymbolNode | None) -> bool | None:
86558666
"""Find out if a node describes a classmethod."""
8667+
if isinstance(node, Decorator):
8668+
node = node.func
86568669
if isinstance(node, FuncDef):
86578670
return node.is_class
86588671
if isinstance(node, Var):
86598672
return node.is_classmethod
86608673
return None
86618674

86628675

8663-
def is_node_static(node: Node | None) -> bool | None:
8676+
def is_node_static(node: SymbolNode | None) -> bool | None:
86648677
"""Find out if a node describes a static function method."""
8678+
if isinstance(node, Decorator):
8679+
node = node.func
86658680
if isinstance(node, FuncDef):
86668681
return node.is_static
86678682
if isinstance(node, Var):

mypy/checkmember.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,14 @@ def expand_and_bind_callable(
976976
freeze_all_type_vars(expanded)
977977
if not var.is_property:
978978
return expanded
979-
# TODO: a decorated property can result in Overloaded here.
979+
if isinstance(expanded, Overloaded):
980+
# Legacy way to store settable properties is with overloads. Also in case it is
981+
# an actual overloaded property, selecting first item that passed check_self_arg()
982+
# is a good approximation, long-term we should use check_call() inference below.
983+
if not expanded.items:
984+
# A broken overload, error should be already reported.
985+
return AnyType(TypeOfAny.from_error)
986+
expanded = expanded.items[0]
980987
assert isinstance(expanded, CallableType)
981988
if var.is_settable_property and mx.is_lvalue and var.setter_type is not None:
982989
if expanded.variables:

mypy/fixup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,7 @@ def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
181181
if isinstance(o.type, Overloaded):
182182
# For error messages we link the original definition for each item.
183183
for typ, item in zip(o.type.items, o.items):
184-
if isinstance(item, Decorator):
185-
typ.definition = item.func
184+
typ.definition = item
186185

187186
def visit_decorator(self, d: Decorator) -> None:
188187
if self.current_info is not None:
@@ -193,8 +192,9 @@ def visit_decorator(self, d: Decorator) -> None:
193192
d.var.accept(self)
194193
for node in d.decorators:
195194
node.accept(self)
196-
if isinstance(d.var.type, ProperType) and isinstance(d.var.type, CallableType):
197-
d.var.type.definition = d.func
195+
typ = d.var.type
196+
if isinstance(typ, ProperType) and isinstance(typ, CallableType):
197+
typ.definition = d.func
198198

199199
def visit_class_def(self, c: ClassDef) -> None:
200200
for v in c.type_vars:

mypy/messages.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
CallExpr,
4545
ClassDef,
4646
Context,
47+
Decorator,
4748
Expression,
4849
FuncDef,
4950
IndexExpr,
@@ -2938,10 +2939,13 @@ def format_single(arg: Type) -> str:
29382939

29392940
def pretty_class_or_static_decorator(tp: CallableType) -> str | None:
29402941
"""Return @classmethod or @staticmethod, if any, for the given callable type."""
2941-
if tp.definition is not None and isinstance(tp.definition, SYMBOL_FUNCBASE_TYPES):
2942-
if tp.definition.is_class:
2942+
definition = tp.definition
2943+
if isinstance(definition, Decorator):
2944+
definition = definition.func
2945+
if definition is not None and isinstance(definition, SYMBOL_FUNCBASE_TYPES):
2946+
if definition.is_class:
29432947
return "@classmethod"
2944-
if tp.definition.is_static:
2948+
if definition.is_static:
29452949
return "@staticmethod"
29462950
return None
29472951

@@ -2991,12 +2995,15 @@ def [T <: int] f(self, x: int, y: T) -> None
29912995
slash = True
29922996

29932997
# If we got a "special arg" (i.e: self, cls, etc...), prepend it to the arg list
2998+
definition = tp.definition
2999+
if isinstance(definition, Decorator):
3000+
definition = definition.func
29943001
if (
2995-
isinstance(tp.definition, FuncDef)
2996-
and hasattr(tp.definition, "arguments")
3002+
isinstance(definition, FuncDef)
3003+
and hasattr(definition, "arguments")
29973004
and not tp.from_concatenate
29983005
):
2999-
definition_arg_names = [arg.variable.name for arg in tp.definition.arguments]
3006+
definition_arg_names = [arg.variable.name for arg in definition.arguments]
30003007
if (
30013008
len(definition_arg_names) > len(tp.arg_names)
30023009
and definition_arg_names[0]
@@ -3005,7 +3012,7 @@ def [T <: int] f(self, x: int, y: T) -> None
30053012
if s:
30063013
s = ", " + s
30073014
s = definition_arg_names[0] + s
3008-
s = f"{tp.definition.name}({s})"
3015+
s = f"{definition.name}({s})"
30093016
elif tp.name:
30103017
first_arg = get_first_arg(tp)
30113018
if first_arg:
@@ -3051,9 +3058,12 @@ def [T <: int] f(self, x: int, y: T) -> None
30513058

30523059

30533060
def get_first_arg(tp: CallableType) -> str | None:
3054-
if not isinstance(tp.definition, FuncDef) or not tp.definition.info or tp.definition.is_static:
3061+
definition = tp.definition
3062+
if isinstance(definition, Decorator):
3063+
definition = definition.func
3064+
if not isinstance(definition, FuncDef) or not definition.info or definition.is_static:
30553065
return None
3056-
return tp.definition.original_first_arg
3066+
return definition.original_first_arg
30573067

30583068

30593069
def variance_string(variance: int) -> str:

mypy/semanal.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,7 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
12431243
bare_setter_type = self.analyze_property_with_multi_part_definition(defn)
12441244
typ = function_type(first_item.func, self.named_type("builtins.function"))
12451245
assert isinstance(typ, CallableType)
1246+
typ.definition = first_item
12461247
types = [typ]
12471248
else:
12481249
# This is a normal overload. Find the item signatures, the
@@ -1374,6 +1375,7 @@ def analyze_overload_sigs_and_impl(
13741375
if isinstance(item, Decorator):
13751376
callable = function_type(item.func, self.named_type("builtins.function"))
13761377
assert isinstance(callable, CallableType)
1378+
callable.definition = item
13771379
if not any(refers_to_fullname(dec, OVERLOAD_NAMES) for dec in item.decorators):
13781380
if i == len(defn.items) - 1 and not self.is_stub_file:
13791381
# Last item outside a stub is impl

mypy/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,9 @@ def __init__(
18871887
self.fallback = fallback
18881888
assert not name or "<bound method" not in name
18891889
self.name = name
1890+
# The rules for what exactly is considered a definition:
1891+
# * If it is a non-decorated function, FuncDef is the definition
1892+
# * If it is a decorated function, enclosing Decorator is the definition
18901893
self.definition = definition
18911894
self.variables = variables
18921895
self.is_ellipsis_args = is_ellipsis_args

test-data/unit/check-classes.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9211,3 +9211,33 @@ class CGT(BG[T]): ...
92119211

92129212
reveal_type(CGI) # N: Revealed type is "def () -> __main__.CGI"
92139213
reveal_type(CGT) # N: Revealed type is "def [T] () -> __main__.CGT[T`1]"
9214+
9215+
[case testSettablePropertyAlias]
9216+
from typing import Any, TypeVar
9217+
9218+
class A:
9219+
@property
9220+
def prop(self: Any) -> str: ...
9221+
@prop.setter
9222+
def prop(self, val: str) -> None: ...
9223+
9224+
T = TypeVar("T")
9225+
class AT:
9226+
@property
9227+
def prop(self: T) -> T: ...
9228+
@prop.setter
9229+
def prop(self: T, val: list[T]) -> None: ...
9230+
9231+
class B:
9232+
prop: str
9233+
prop_t: str
9234+
9235+
class C(B):
9236+
prop = A.prop
9237+
prop_t = AT.prop # E: Incompatible types in assignment (expression has type "C", base class "B" defined the type as "str")
9238+
9239+
reveal_type(C().prop) # N: Revealed type is "builtins.str"
9240+
C().prop = "no" # E: Invalid self argument "C" to attribute function "prop" with type "Callable[[A, str], None]"
9241+
reveal_type(C().prop_t) # N: Revealed type is "__main__.C"
9242+
C().prop_t = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "list[C]")
9243+
[builtins fixtures/property.pyi]

test-data/unit/check-dataclasses.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2055,7 +2055,7 @@ from dataclasses import dataclass, replace, InitVar
20552055
from typing import ClassVar
20562056

20572057
@dataclass
2058-
class A:
2058+
class A: # N: "replace" of "A" defined here
20592059
x: int
20602060
q: InitVar[int]
20612061
q2: InitVar[int] = 0

test-data/unit/fine-grained.test

Lines changed: 2 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7312,9 +7312,7 @@ class C:
73127312
==
73137313
mod.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")
73147314

7315-
[case testOverloadedMethodSupertype-only_when_cache]
7316-
-- Different cache/no-cache tests because
7317-
-- CallableType.def_extras.first_arg differs ("self"/None)
7315+
[case testOverloadedMethodSupertype]
73187316
from typing import overload, Any
73197317
import b
73207318
class Child(b.Parent):
@@ -7355,49 +7353,6 @@ main:4: note: def f(self, arg: int) -> int
73557353
main:4: note: @overload
73567354
main:4: note: def f(self, arg: str) -> str
73577355

7358-
[case testOverloadedMethodSupertype2-only_when_nocache]
7359-
-- Different cache/no-cache tests because
7360-
-- CallableType.def_extras.first_arg differs ("self"/None)
7361-
from typing import overload, Any
7362-
import b
7363-
class Child(b.Parent):
7364-
@overload # Fail
7365-
def f(self, arg: int) -> int: ...
7366-
@overload
7367-
def f(self, arg: str) -> str: ...
7368-
def f(self, arg: Any) -> Any: ...
7369-
[file b.py]
7370-
from typing import overload, Any
7371-
class C: pass
7372-
class Parent:
7373-
@overload
7374-
def f(self, arg: int) -> int: ...
7375-
@overload
7376-
def f(self, arg: str) -> str: ...
7377-
def f(self, arg: Any) -> Any: ...
7378-
[file b.py.2]
7379-
from typing import overload, Any
7380-
class C: pass
7381-
class Parent:
7382-
@overload
7383-
def f(self, arg: int) -> int: ...
7384-
@overload
7385-
def f(self, arg: str) -> C: ...
7386-
def f(self, arg: Any) -> Any: ...
7387-
[out]
7388-
==
7389-
main:4: error: Signature of "f" incompatible with supertype "b.Parent"
7390-
main:4: note: Superclass:
7391-
main:4: note: @overload
7392-
main:4: note: def f(self, arg: int) -> int
7393-
main:4: note: @overload
7394-
main:4: note: def f(self, arg: str) -> C
7395-
main:4: note: Subclass:
7396-
main:4: note: @overload
7397-
main:4: note: def f(arg: int) -> int
7398-
main:4: note: @overload
7399-
main:4: note: def f(arg: str) -> str
7400-
74017356
[case testOverloadedInitSupertype]
74027357
import a
74037358
[file a.py]
@@ -8486,9 +8441,7 @@ class D:
84868441
==
84878442
a.py:3: error: Cannot override final attribute "meth" (previously declared in base class "C")
84888443

8489-
[case testFinalBodyReprocessedAndStillFinalOverloaded-only_when_cache]
8490-
-- Different cache/no-cache tests because
8491-
-- CallableType.def_extras.first_arg differs ("self"/None)
8444+
[case testFinalBodyReprocessedAndStillFinalOverloaded]
84928445
import a
84938446
[file a.py]
84948447
from c import C
@@ -8533,53 +8486,6 @@ a.py:3: note: def meth(self, x: str) -> str
85338486
a.py:3: note: Subclass:
85348487
a.py:3: note: def meth(self) -> None
85358488

8536-
[case testFinalBodyReprocessedAndStillFinalOverloaded2-only_when_nocache]
8537-
-- Different cache/no-cache tests because
8538-
-- CallableType.def_extras.first_arg differs ("self"/None)
8539-
import a
8540-
[file a.py]
8541-
from c import C
8542-
class A:
8543-
def meth(self) -> None: ...
8544-
8545-
[file a.py.3]
8546-
from c import C
8547-
class A(C):
8548-
def meth(self) -> None: ...
8549-
8550-
[file c.py]
8551-
from typing import final, overload, Union
8552-
from d import D
8553-
8554-
class C:
8555-
@overload
8556-
def meth(self, x: int) -> int: ...
8557-
@overload
8558-
def meth(self, x: str) -> str: ...
8559-
@final
8560-
def meth(self, x: Union[int, str]) -> Union[int, str]:
8561-
D(int())
8562-
return x
8563-
[file d.py]
8564-
class D:
8565-
def __init__(self, x: int) -> None: ...
8566-
[file d.py.2]
8567-
from typing import Optional
8568-
class D:
8569-
def __init__(self, x: Optional[int]) -> None: ...
8570-
[out]
8571-
==
8572-
==
8573-
a.py:3: error: Cannot override final attribute "meth" (previously declared in base class "C")
8574-
a.py:3: error: Signature of "meth" incompatible with supertype "c.C"
8575-
a.py:3: note: Superclass:
8576-
a.py:3: note: @overload
8577-
a.py:3: note: def meth(x: int) -> int
8578-
a.py:3: note: @overload
8579-
a.py:3: note: def meth(x: str) -> str
8580-
a.py:3: note: Subclass:
8581-
a.py:3: note: def meth(self) -> None
8582-
85838489
[case testIfMypyUnreachableClass]
85848490
from a import x
85858491

test-data/unit/pythoneval.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,7 @@ a2 = replace()
19701970
a2 = replace(a, x='spam')
19711971
a2 = replace(a, x=42, q=42)
19721972
[out]
1973+
_testDataclassReplace.py:4: note: "replace" of "A" defined here
19731974
_testDataclassReplace.py:9: note: Revealed type is "_testDataclassReplace.A"
19741975
_testDataclassReplace.py:10: error: Too few arguments for "replace"
19751976
_testDataclassReplace.py:11: error: Argument "x" to "replace" of "A" has incompatible type "str"; expected "int"

0 commit comments

Comments
 (0)