Skip to content

Commit b805551

Browse files
authored
Enable support for decorated properties where possible (#13409)
Fixes #1362 I delete the old cryptic error message and instead: * Enable the situations that we can already handle (cover 95% of what was discussed in the issue) * For the rest give more precise error messages Co-authored-by: graingert <>
1 parent 3a13b8e commit b805551

8 files changed

+136
-28
lines changed

mypy/checker.py

+3
Original file line numberDiff line numberDiff line change
@@ -4310,6 +4310,9 @@ def visit_decorator(self, e: Decorator) -> None:
43104310
e.var.type = sig
43114311
e.var.is_ready = True
43124312
if e.func.is_property:
4313+
if isinstance(sig, CallableType):
4314+
if len([k for k in sig.arg_kinds if k.is_required()]) > 1:
4315+
self.msg.fail("Too many arguments for property", e)
43134316
self.check_incompatible_property_override(e)
43144317
if e.func.info and not e.func.is_dynamic():
43154318
self.check_method_override(e)

mypy/semanal.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,8 @@ def analyze_overload_sigs_and_impl(
10491049
else:
10501050
item.func.is_overload = True
10511051
types.append(callable)
1052+
if item.var.is_property:
1053+
self.fail("An overload can not be a property", item)
10521054
elif isinstance(item, FuncDef):
10531055
if i == len(defn.items) - 1 and not self.is_stub_file:
10541056
impl = item
@@ -1168,16 +1170,18 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
11681170
deleted_items = []
11691171
for i, item in enumerate(items[1:]):
11701172
if isinstance(item, Decorator):
1171-
if len(item.decorators) == 1:
1173+
if len(item.decorators) >= 1:
11721174
node = item.decorators[0]
11731175
if isinstance(node, MemberExpr):
11741176
if node.name == "setter":
11751177
# The first item represents the entire property.
11761178
first_item.var.is_settable_property = True
11771179
# Get abstractness from the original definition.
11781180
item.func.abstract_status = first_item.func.abstract_status
1179-
else:
1180-
self.fail("Decorated property not supported", item)
1181+
else:
1182+
self.fail(
1183+
f"Only supported top decorator is @{first_item.func.name}.setter", item
1184+
)
11811185
item.func.accept(self)
11821186
else:
11831187
self.fail(f'Unexpected definition for property "{first_item.func.name}"', item)
@@ -1258,6 +1262,7 @@ def visit_decorator(self, dec: Decorator) -> None:
12581262
d.accept(self)
12591263
removed: List[int] = []
12601264
no_type_check = False
1265+
could_be_decorated_property = False
12611266
for i, d in enumerate(dec.decorators):
12621267
# A bunch of decorators are special cased here.
12631268
if refers_to_fullname(d, "abc.abstractmethod"):
@@ -1288,8 +1293,6 @@ def visit_decorator(self, dec: Decorator) -> None:
12881293
elif refers_to_fullname(d, "functools.cached_property"):
12891294
dec.var.is_settable_property = True
12901295
self.check_decorated_function_is_method("property", dec)
1291-
if len(dec.func.arguments) > 1:
1292-
self.fail("Too many arguments", dec.func)
12931296
elif refers_to_fullname(d, "typing.no_type_check"):
12941297
dec.var.type = AnyType(TypeOfAny.special_form)
12951298
no_type_check = True
@@ -1304,15 +1307,21 @@ def visit_decorator(self, dec: Decorator) -> None:
13041307
removed.append(i)
13051308
else:
13061309
self.fail("@final cannot be used with non-method functions", d)
1310+
elif not dec.var.is_property:
1311+
# We have seen a "non-trivial" decorator before seeing @property, if
1312+
# we will see a @property later, give an error, as we don't support this.
1313+
could_be_decorated_property = True
13071314
for i in reversed(removed):
13081315
del dec.decorators[i]
13091316
if (not dec.is_overload or dec.var.is_property) and self.type:
13101317
dec.var.info = self.type
13111318
dec.var.is_initialized_in_class = True
13121319
if not no_type_check and self.recurse_into_functions:
13131320
dec.func.accept(self)
1314-
if dec.decorators and dec.var.is_property:
1315-
self.fail("Decorated property not supported", dec)
1321+
if could_be_decorated_property and dec.decorators and dec.var.is_property:
1322+
self.fail("Decorators on top of @property are not supported", dec)
1323+
if (dec.func.is_static or dec.func.is_class) and dec.var.is_property:
1324+
self.fail("Only instance methods can be decorated with @property", dec)
13161325
if dec.func.abstract_status == IS_ABSTRACT and dec.func.is_final:
13171326
self.fail(f"Method {dec.func.name} is both abstract and final", dec)
13181327

test-data/unit/check-abstract.test

+20
Original file line numberDiff line numberDiff line change
@@ -1030,3 +1030,23 @@ def deco(cls: Type[T]) -> Type[T]: ...
10301030
class A(metaclass=ABCMeta):
10311031
@abstractmethod
10321032
def foo(self, x: int) -> None: ...
1033+
1034+
[case testAbstractPropertiesAllowed]
1035+
from abc import abstractmethod
1036+
1037+
class B:
1038+
@property
1039+
@abstractmethod
1040+
def x(self) -> int: ...
1041+
@property
1042+
@abstractmethod
1043+
def y(self) -> int: ...
1044+
@y.setter
1045+
@abstractmethod
1046+
def y(self, value: int) -> None: ...
1047+
1048+
B() # E: Cannot instantiate abstract class "B" with abstract attributes "x" and "y"
1049+
b: B
1050+
b.x = 1 # E: Property "x" defined in "B" is read-only
1051+
b.y = 1
1052+
[builtins fixtures/property.pyi]

test-data/unit/check-classes.test

+4-4
Original file line numberDiff line numberDiff line change
@@ -7006,8 +7006,8 @@ class A:
70067006
def y(self) -> int: ...
70077007
@y.setter
70087008
def y(self, value: int) -> None: ...
7009-
@dec
7010-
def y(self) -> None: ... # TODO: This should generate an error
7009+
@dec # E: Only supported top decorator is @y.setter
7010+
def y(self) -> None: ...
70117011

70127012
reveal_type(A().y) # N: Revealed type is "builtins.int"
70137013
[builtins fixtures/property.pyi]
@@ -7044,7 +7044,7 @@ reveal_type(D1() + 0.5) # N: Revealed type is "__main__.D1"
70447044
[builtins fixtures/primitives.pyi]
70457045

70467046
[case testRefMethodWithDecorator]
7047-
from typing import Type
7047+
from typing import Type, final
70487048

70497049
class A:
70507050
pass
@@ -7058,7 +7058,7 @@ class B:
70587058
return A
70597059

70607060
class C:
7061-
@property
7061+
@final
70627062
@staticmethod
70637063
def A() -> Type[A]:
70647064
return A

test-data/unit/check-functions.test

+85
Original file line numberDiff line numberDiff line change
@@ -2589,3 +2589,88 @@ def a(b: any): pass # E: Function "builtins.any" is not valid as a type \
25892589
def a(b: callable): pass # E: Function "builtins.callable" is not valid as a type \
25902590
# N: Perhaps you meant "typing.Callable" instead of "callable"?
25912591
[builtins fixtures/callable.pyi]
2592+
2593+
[case testDecoratedProperty]
2594+
from typing import TypeVar, Callable, final
2595+
2596+
T = TypeVar("T")
2597+
2598+
def dec(f: Callable[[T], int]) -> Callable[[T], str]: ...
2599+
def dec2(f: T) -> T: ...
2600+
2601+
class A:
2602+
@property
2603+
@dec
2604+
def f(self) -> int: pass
2605+
@property
2606+
@dec2
2607+
def g(self) -> int: pass
2608+
reveal_type(A().f) # N: Revealed type is "builtins.str"
2609+
reveal_type(A().g) # N: Revealed type is "builtins.int"
2610+
2611+
class B:
2612+
@final
2613+
@property
2614+
@dec
2615+
def f(self) -> int: pass
2616+
reveal_type(B().f) # N: Revealed type is "builtins.str"
2617+
2618+
class C:
2619+
@property # E: Only instance methods can be decorated with @property
2620+
@classmethod
2621+
def f(cls) -> int: pass
2622+
reveal_type(C().f) # N: Revealed type is "builtins.int"
2623+
[builtins fixtures/property.pyi]
2624+
[out]
2625+
2626+
[case testDecoratedPropertySetter]
2627+
from typing import TypeVar, Callable, final
2628+
2629+
T = TypeVar("T")
2630+
def dec(f: T) -> T: ...
2631+
2632+
class A:
2633+
@property
2634+
@dec
2635+
def f(self) -> int: pass
2636+
@f.setter
2637+
@dec
2638+
def f(self, v: int) -> None: pass
2639+
reveal_type(A().f) # N: Revealed type is "builtins.int"
2640+
2641+
class B:
2642+
@property
2643+
@dec
2644+
def f(self) -> int: pass
2645+
@dec # E: Only supported top decorator is @f.setter
2646+
@f.setter
2647+
def f(self, v: int) -> None: pass
2648+
2649+
class C:
2650+
@dec # E: Decorators on top of @property are not supported
2651+
@property
2652+
def f(self) -> int: pass
2653+
@f.setter
2654+
@dec
2655+
def f(self, v: int) -> None: pass
2656+
[builtins fixtures/property.pyi]
2657+
[out]
2658+
2659+
[case testInvalidArgCountForProperty]
2660+
from typing import Callable, TypeVar
2661+
2662+
T = TypeVar("T")
2663+
def dec(f: Callable[[T], int]) -> Callable[[T, int], int]: ...
2664+
2665+
class A:
2666+
@property # E: Too many arguments for property
2667+
def f(self, x) -> int: pass
2668+
@property # E: Too many arguments for property
2669+
@dec
2670+
def e(self) -> int: pass
2671+
@property
2672+
def g() -> int: pass # E: Method must have at least one argument
2673+
@property
2674+
def h(self, *args, **kwargs) -> int: pass # OK
2675+
[builtins fixtures/property.pyi]
2676+
[out]

test-data/unit/check-functools.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ class Child(Parent):
119119
def f(self) -> str: pass
120120
@cached_property
121121
def g(self) -> int: pass
122-
@cached_property
123-
def h(self, arg) -> int: pass # E: Too many arguments
122+
@cached_property # E: Too many arguments for property
123+
def h(self, arg) -> int: pass
124124
reveal_type(Parent().f) # N: Revealed type is "builtins.str"
125125
reveal_type(Child().f) # N: Revealed type is "builtins.str"
126126
reveal_type(Child().g) # N: Revealed type is "builtins.int"

test-data/unit/fixtures/property.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class type:
1111
class function: pass
1212

1313
property = object() # Dummy definition
14+
class classmethod: pass
1415

1516
class dict: pass
1617
class int: pass

test-data/unit/semanal-errors.test

+5-15
Original file line numberDiff line numberDiff line change
@@ -1205,23 +1205,13 @@ def f() -> int: pass
12051205
[builtins fixtures/property.pyi]
12061206
[out]
12071207

1208-
[case testInvalidArgCountForProperty]
1209-
import typing
1210-
class A:
1211-
@property
1212-
def f(self, x) -> int: pass # E: Too many arguments
1213-
@property
1214-
def g() -> int: pass # E: Method must have at least one argument
1215-
[builtins fixtures/property.pyi]
1216-
[out]
1217-
12181208
[case testOverloadedProperty]
12191209
from typing import overload
12201210
class A:
1221-
@overload # E: Decorated property not supported
1211+
@overload # E: Decorators on top of @property are not supported
12221212
@property
12231213
def f(self) -> int: pass
1224-
@property # E: Decorated property not supported
1214+
@property # E: Only supported top decorator is @f.setter
12251215
@overload
12261216
def f(self) -> int: pass
12271217
[builtins fixtures/property.pyi]
@@ -1232,7 +1222,7 @@ from typing import overload
12321222
class A:
12331223
@overload # E: An overloaded function outside a stub file must have an implementation
12341224
def f(self) -> int: pass
1235-
@property # E: Decorated property not supported
1225+
@property # E: An overload can not be a property
12361226
@overload
12371227
def f(self) -> int: pass
12381228
[builtins fixtures/property.pyi]
@@ -1242,10 +1232,10 @@ class A:
12421232
import typing
12431233
def dec(f): pass
12441234
class A:
1245-
@dec # E: Decorated property not supported
1235+
@dec # E: Decorators on top of @property are not supported
12461236
@property
12471237
def f(self) -> int: pass
1248-
@property # E: Decorated property not supported
1238+
@property # OK
12491239
@dec
12501240
def g(self) -> int: pass
12511241
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)