Skip to content

Commit dd83104

Browse files
committed
Merge branch 'property-setters'
2 parents 7a6f725 + 60bb757 commit dd83104

File tree

6 files changed

+139
-46
lines changed

6 files changed

+139
-46
lines changed

mypy/checker.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ def infer_local_variable_type(self, x, y, z):
416416

417417
def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> Type:
418418
num_abstract = 0
419+
if defn.is_property:
420+
# HACK: Infer the type of the property.
421+
self.visit_decorator(defn.items[0])
419422
for fdef in defn.items:
420423
self.check_func_item(fdef.func, name=fdef.func.name())
421424
if fdef.func.is_abstract:

mypy/checkmember.py

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Overloaded, TypeVarType, TypeTranslator, UnionType
88
)
99
from mypy.nodes import TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context
10-
from mypy.nodes import ARG_POS, function_type, Decorator
10+
from mypy.nodes import ARG_POS, function_type, Decorator, OverloadedFuncDef
1111
from mypy.messages import MessageBuilder
1212
from mypy.maptype import map_instance_to_supertype
1313
from mypy.expandtype import expand_type_by_instance
@@ -47,6 +47,10 @@ def analyse_member_access(name: str, typ: Type, node: Context, is_lvalue: bool,
4747
# Look up the member. First look up the method dictionary.
4848
method = info.get_method(name)
4949
if method:
50+
if method.is_property:
51+
assert isinstance(method, OverloadedFuncDef)
52+
method = cast(OverloadedFuncDef, method)
53+
return analyze_var(name, method.items[0].var, typ, info, node, is_lvalue, msg)
5054
if is_lvalue:
5155
msg.cant_assign_to_method(node)
5256
typ = map_instance_to_supertype(typ, method.info)
@@ -114,37 +118,7 @@ def analyse_member_var_access(name: str, itype: Instance, info: TypeInfo,
114118
v = vv.var
115119

116120
if isinstance(v, Var):
117-
# Found a member variable.
118-
var = v
119-
itype = map_instance_to_supertype(itype, var.info)
120-
if var.type:
121-
t = expand_type_by_instance(var.type, itype)
122-
if var.is_initialized_in_class and isinstance(t, FunctionLike):
123-
if is_lvalue:
124-
if var.is_property:
125-
msg.read_only_property(name, info, node)
126-
else:
127-
msg.cant_assign_to_method(node)
128-
129-
if not var.is_staticmethod:
130-
# Class-level function objects and classmethods become bound
131-
# methods: the former to the instance, the latter to the
132-
# class.
133-
functype = cast(FunctionLike, t)
134-
check_method_type(functype, itype, node, msg)
135-
signature = method_type(functype)
136-
if var.is_property:
137-
# A property cannot have an overloaded type => the cast
138-
# is fine.
139-
return cast(CallableType, signature).ret_type
140-
else:
141-
return signature
142-
return t
143-
else:
144-
if not var.is_ready:
145-
msg.cannot_determine_type(var.name(), node)
146-
# Implicit 'Any' type.
147-
return AnyType()
121+
return analyze_var(name, v, itype, info, node, is_lvalue, msg)
148122
elif isinstance(v, FuncDef):
149123
assert False, "Did not expect a function"
150124
elif not v and name not in ['__getattr__', '__setattr__']:
@@ -165,6 +139,45 @@ def analyse_member_var_access(name: str, itype: Instance, info: TypeInfo,
165139
return msg.has_no_attr(report_type or itype, name, node)
166140

167141

142+
def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Context,
143+
is_lvalue: bool, msg: MessageBuilder) -> Type:
144+
"""Analyze access to an attribute via a Var node.
145+
146+
This is conceptually part of analyse_member_access and the arguments are similar.
147+
"""
148+
# Found a member variable.
149+
itype = map_instance_to_supertype(itype, var.info)
150+
if var.type:
151+
t = expand_type_by_instance(var.type, itype)
152+
if var.is_initialized_in_class and isinstance(t, FunctionLike):
153+
if is_lvalue:
154+
if var.is_property:
155+
if not var.is_settable_property:
156+
msg.read_only_property(name, info, node)
157+
else:
158+
msg.cant_assign_to_method(node)
159+
160+
if not var.is_staticmethod:
161+
# Class-level function objects and classmethods become bound
162+
# methods: the former to the instance, the latter to the
163+
# class.
164+
functype = cast(FunctionLike, t)
165+
check_method_type(functype, itype, node, msg)
166+
signature = method_type(functype)
167+
if var.is_property:
168+
# A property cannot have an overloaded type => the cast
169+
# is fine.
170+
return cast(CallableType, signature).ret_type
171+
else:
172+
return signature
173+
return t
174+
else:
175+
if not var.is_ready:
176+
msg.cannot_determine_type(var.name(), node)
177+
# Implicit 'Any' type.
178+
return AnyType()
179+
180+
168181
def lookup_member_var_or_accessor(info: TypeInfo, name: str,
169182
is_lvalue: bool) -> SymbolNode:
170183
"""Find the attribute/accessor node that refers to a member of a type."""

mypy/nodes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ class FuncBase(SymbolNode):
199199
type = None # type: mypy.types.Type
200200
# If method, reference to TypeInfo
201201
info = None # type: TypeInfo
202+
is_property = False
202203

203204
@abstractmethod
204205
def name(self) -> str: pass
@@ -380,6 +381,7 @@ class Var(SymbolNode):
380381
is_staticmethod = False
381382
is_classmethod = False
382383
is_property = False
384+
is_settable_property = False
383385

384386
def __init__(self, name: str, type: 'mypy.types.Type' = None) -> None:
385387
self._name = name

mypy/semanal.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,17 @@ def is_defined_type_var(self, tvar: str, context: Node) -> bool:
264264

265265
def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
266266
t = [] # type: List[CallableType]
267-
for item in defn.items:
267+
for i, item in enumerate(defn.items):
268268
# TODO support decorated overloaded functions properly
269269
item.is_overload = True
270270
item.func.is_overload = True
271271
item.accept(self)
272272
t.append(cast(CallableType, function_type(item.func,
273273
self.builtin_type('builtins.function'))))
274+
if item.func.is_property and i == 0:
275+
# This defines a property, probably with a setter and/or deleter.
276+
self.analyse_property_with_multi_part_definition(defn)
277+
break
274278
if not [dec for dec in item.decorators
275279
if refers_to_fullname(dec, 'typing.overload')]:
276280
self.fail("'overload' decorator expected", item)
@@ -285,6 +289,24 @@ def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
285289
elif self.is_func_scope():
286290
self.add_local_func(defn, defn)
287291

292+
def analyse_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -> None:
293+
"""Analyze a propery defined using multiple methods (e.g., using @x.setter).
294+
295+
Assume that the first method (@property) has already been analyzed.
296+
"""
297+
defn.is_property = True
298+
items = defn.items
299+
for item in items[1:]:
300+
if len(item.decorators) == 1:
301+
node = item.decorators[0]
302+
if isinstance(node, MemberExpr):
303+
if node.name == 'setter':
304+
# The first item represents the entire property.
305+
defn.items[0].var.is_settable_property = True
306+
else:
307+
self.fail("Decorated property not supported", item)
308+
item.func.accept(self)
309+
288310
def analyse_function(self, defn: FuncItem) -> None:
289311
is_method = self.is_class_scope()
290312
tvarnodes = self.add_func_type_variables_to_symbol_table(defn)
@@ -1198,15 +1220,6 @@ def analyze_types(self, items: List[Node]) -> List[Type]:
11981220
return result
11991221

12001222
def visit_decorator(self, dec: Decorator) -> None:
1201-
if not dec.is_overload:
1202-
if self.is_func_scope():
1203-
self.add_symbol(dec.var.name(), SymbolTableNode(LDEF, dec),
1204-
dec)
1205-
elif self.type:
1206-
dec.var.info = self.type
1207-
dec.var.is_initialized_in_class = True
1208-
self.add_symbol(dec.var.name(), SymbolTableNode(MDEF, dec),
1209-
dec)
12101223
for d in dec.decorators:
12111224
d.accept(self)
12121225
removed = [] # type: List[int]
@@ -1232,13 +1245,22 @@ def visit_decorator(self, dec: Decorator) -> None:
12321245
removed.append(i)
12331246
dec.func.is_property = True
12341247
dec.var.is_property = True
1235-
if dec.is_overload:
1236-
self.fail('A property cannot be overloaded', dec)
12371248
self.check_decorated_function_is_method('property', dec)
12381249
if len(dec.func.args) > 1:
12391250
self.fail('Too many arguments', dec.func)
12401251
for i in reversed(removed):
12411252
del dec.decorators[i]
1253+
if not dec.is_overload or dec.var.is_property:
1254+
if self.is_func_scope():
1255+
self.add_symbol(dec.var.name(), SymbolTableNode(LDEF, dec),
1256+
dec)
1257+
elif self.type:
1258+
dec.var.info = self.type
1259+
dec.var.is_initialized_in_class = True
1260+
self.add_symbol(dec.var.name(), SymbolTableNode(MDEF, dec),
1261+
dec)
1262+
if dec.decorators and dec.var.is_property:
1263+
self.fail('Decorated property not supported', dec)
12421264
dec.func.accept(self)
12431265
if not dec.decorators and not dec.var.is_property:
12441266
# No non-special decorators left. We can trivially infer the type

mypy/test/data/check-classes.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,35 @@ a.f.xx
815815
a.f = '' # E: Property "f" defined in "A" is read-only
816816
[builtins fixtures/property.py]
817817

818+
[case testPropertyWithSetter]
819+
import typing
820+
class A:
821+
@property
822+
def f(self) -> int:
823+
return 1
824+
@f.setter
825+
def f(self, x: int) -> None:
826+
pass
827+
a = A()
828+
a.f = a.f
829+
a.f.x # E: "int" has no attribute "x"
830+
a.f = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
831+
[builtins fixtures/property.py]
832+
833+
[case testPropertyWithDeleterButNoSetter]
834+
import typing
835+
class A:
836+
@property
837+
def f(self) -> int:
838+
return 1
839+
@f.deleter
840+
def f(self, x) -> None:
841+
pass
842+
a = A()
843+
a.f = a.f # E: Property "f" defined in "A" is read-only
844+
a.f.x # E: "int" has no attribute "x"
845+
[builtins fixtures/property.py]
846+
818847

819848
-- Multiple inheritance, non-object built-in class as base
820849
-- -------------------------------------------------------

mypy/test/data/semanal-errors.test

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,15 +1248,39 @@ class A:
12481248
[case testOverloadedProperty]
12491249
from typing import overload
12501250
class A:
1251-
@overload # E: A property cannot be overloaded
1251+
@overload # E: Decorated property not supported
12521252
@property
12531253
def f(self) -> int: pass
1254-
@property # E: A property cannot be overloaded
1254+
@property # E: Decorated property not supported
12551255
@overload
12561256
def f(self) -> int: pass
12571257
[builtins fixtures/property.py]
12581258
[out]
12591259

1260+
[case testOverloadedProperty2]
1261+
from typing import overload
1262+
class A:
1263+
@overload
1264+
def f(self) -> int: pass
1265+
@property # E: Decorated property not supported
1266+
@overload
1267+
def f(self) -> int: pass
1268+
[builtins fixtures/property.py]
1269+
[out]
1270+
1271+
[case testDecoratedProperty]
1272+
import typing
1273+
def dec(f): pass
1274+
class A:
1275+
@dec # E: Decorated property not supported
1276+
@property
1277+
def f(self) -> int: pass
1278+
@property # E: Decorated property not supported
1279+
@dec
1280+
def g(self) -> int: pass
1281+
[builtins fixtures/property.py]
1282+
[out]
1283+
12601284
[case testImportTwoModulesWithSameNameInFunction]
12611285
import typing
12621286
def f() -> None:

0 commit comments

Comments
 (0)