Skip to content

Commit 2013112

Browse files
cpenningtonJukkaL
authored andcommitted
Implement the descriptor protocol (#2266)
* Implement the descriptor protocol
1 parent aee172d commit 2013112

File tree

4 files changed

+484
-13
lines changed

4 files changed

+484
-13
lines changed

mypy/checker.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from mypy.maptype import map_instance_to_supertype
4646
from mypy.semanal import fill_typevars, set_callable_name, refers_to_fullname
4747
from mypy.erasetype import erase_typevars
48-
from mypy.expandtype import expand_type
48+
from mypy.expandtype import expand_type, expand_type_by_instance
4949
from mypy.visitor import NodeVisitor
5050
from mypy.join import join_types
5151
from mypy.treetransform import TransformVisitor
@@ -1149,6 +1149,11 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
11491149
not new_syntax):
11501150
# Allow None's to be assigned to class variables with non-Optional types.
11511151
rvalue_type = lvalue_type
1152+
elif (isinstance(lvalue, MemberExpr) and
1153+
lvalue.kind is None): # Ignore member access to modules
1154+
instance_type = self.accept(lvalue.expr)
1155+
rvalue_type, infer_lvalue_type = self.check_member_assignment(
1156+
instance_type, lvalue_type, rvalue, lvalue)
11521157
else:
11531158
rvalue_type = self.check_simple_assignment(lvalue_type, rvalue, lvalue)
11541159

@@ -1478,6 +1483,60 @@ def check_simple_assignment(self, lvalue_type: Type, rvalue: Expression,
14781483
'{} has type'.format(lvalue_name))
14791484
return rvalue_type
14801485

1486+
def check_member_assignment(self, instance_type: Type, attribute_type: Type,
1487+
rvalue: Expression, context: Context) -> Tuple[Type, bool]:
1488+
"""Type member assigment.
1489+
1490+
This is defers to check_simple_assignment, unless the member expression
1491+
is a descriptor, in which case this checks descriptor semantics as well.
1492+
1493+
Return the inferred rvalue_type and whether to infer anything about the attribute type
1494+
"""
1495+
# Descriptors don't participate in class-attribute access
1496+
if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or
1497+
isinstance(instance_type, TypeType)):
1498+
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
1499+
return rvalue_type, True
1500+
1501+
if not isinstance(attribute_type, Instance):
1502+
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
1503+
return rvalue_type, True
1504+
1505+
if not attribute_type.type.has_readable_member('__set__'):
1506+
# If there is no __set__, we type-check that the assigned value matches
1507+
# the return type of __get__. This doesn't match the python semantics,
1508+
# (which allow you to override the descriptor with any value), but preserves
1509+
# the type of accessing the attribute (even after the override).
1510+
if attribute_type.type.has_readable_member('__get__'):
1511+
attribute_type = self.expr_checker.analyze_descriptor_access(
1512+
instance_type, attribute_type, context)
1513+
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
1514+
return rvalue_type, True
1515+
1516+
dunder_set = attribute_type.type.get_method('__set__')
1517+
if dunder_set is None:
1518+
self.msg.fail("{}.__set__ is not callable".format(attribute_type), context)
1519+
return AnyType(), False
1520+
1521+
function = function_type(dunder_set, self.named_type('builtins.function'))
1522+
bound_method = bind_self(function, attribute_type)
1523+
typ = map_instance_to_supertype(attribute_type, dunder_set.info)
1524+
dunder_set_type = expand_type_by_instance(bound_method, typ)
1525+
1526+
_, inferred_dunder_set_type = self.expr_checker.check_call(
1527+
dunder_set_type, [TempNode(instance_type), rvalue],
1528+
[nodes.ARG_POS, nodes.ARG_POS], context)
1529+
1530+
if not isinstance(inferred_dunder_set_type, CallableType):
1531+
self.fail("__set__ is not callable", context)
1532+
return AnyType(), True
1533+
1534+
if len(inferred_dunder_set_type.arg_types) < 2:
1535+
# A message already will have been recorded in check_call
1536+
return AnyType(), False
1537+
1538+
return inferred_dunder_set_type.arg_types[1], False
1539+
14811540
def check_indexed_assignment(self, lvalue: IndexExpr,
14821541
rvalue: Expression, context: Context) -> None:
14831542
"""Type check indexed assignment base[index] = rvalue.

mypy/checkexpr.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
88
TupleType, TypedDictType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType,
99
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType,
10-
true_only, false_only, is_named_instance, function_type,
10+
true_only, false_only, is_named_instance, function_type, FunctionLike,
1111
get_typ_args, set_typ_args,
1212
)
1313
from mypy.nodes import (
@@ -30,13 +30,14 @@
3030
from mypy import messages
3131
from mypy.infer import infer_type_arguments, infer_function_type_arguments
3232
from mypy import join
33+
from mypy.maptype import map_instance_to_supertype
3334
from mypy.subtypes import is_subtype, is_equivalent
3435
from mypy import applytype
3536
from mypy import erasetype
36-
from mypy.checkmember import analyze_member_access, type_object_type
37+
from mypy.checkmember import analyze_member_access, type_object_type, bind_self
3738
from mypy.constraints import get_actual_type
3839
from mypy.checkstrformat import StringFormatterChecker
39-
from mypy.expandtype import expand_type
40+
from mypy.expandtype import expand_type, expand_type_by_instance
4041
from mypy.util import split_module_names
4142
from mypy.semanal import fill_typevars
4243

@@ -983,10 +984,69 @@ def analyze_ordinary_member_access(self, e: MemberExpr,
983984
else:
984985
# This is a reference to a non-module attribute.
985986
original_type = self.accept(e.expr)
986-
return analyze_member_access(e.name, original_type, e,
987-
is_lvalue, False, False,
988-
self.named_type, self.not_ready_callback, self.msg,
989-
original_type=original_type, chk=self.chk)
987+
member_type = analyze_member_access(
988+
e.name, original_type, e, is_lvalue, False, False,
989+
self.named_type, self.not_ready_callback, self.msg,
990+
original_type=original_type, chk=self.chk)
991+
if is_lvalue:
992+
return member_type
993+
else:
994+
return self.analyze_descriptor_access(original_type, member_type, e)
995+
996+
def analyze_descriptor_access(self, instance_type: Type, descriptor_type: Type,
997+
context: Context) -> Type:
998+
"""Type check descriptor access.
999+
1000+
Arguments:
1001+
instance_type: The type of the instance on which the descriptor
1002+
attribute is being accessed (the type of ``a`` in ``a.f`` when
1003+
``f`` is a descriptor).
1004+
descriptor_type: The type of the descriptor attribute being accessed
1005+
(the type of ``f`` in ``a.f`` when ``f`` is a descriptor).
1006+
context: The node defining the context of this inference.
1007+
Return:
1008+
The return type of the appropriate ``__get__`` overload for the descriptor.
1009+
"""
1010+
if not isinstance(descriptor_type, Instance):
1011+
return descriptor_type
1012+
1013+
if not descriptor_type.type.has_readable_member('__get__'):
1014+
return descriptor_type
1015+
1016+
dunder_get = descriptor_type.type.get_method('__get__')
1017+
1018+
if dunder_get is None:
1019+
self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
1020+
return AnyType()
1021+
1022+
function = function_type(dunder_get, self.named_type('builtins.function'))
1023+
bound_method = bind_self(function, descriptor_type)
1024+
typ = map_instance_to_supertype(descriptor_type, dunder_get.info)
1025+
dunder_get_type = expand_type_by_instance(bound_method, typ)
1026+
owner_type = None # type: Type
1027+
1028+
if isinstance(instance_type, FunctionLike) and instance_type.is_type_obj():
1029+
owner_type = instance_type.items()[0].ret_type
1030+
instance_type = NoneTyp()
1031+
elif isinstance(instance_type, TypeType):
1032+
owner_type = instance_type.item
1033+
instance_type = NoneTyp()
1034+
else:
1035+
owner_type = instance_type
1036+
1037+
_, inferred_dunder_get_type = self.check_call(
1038+
dunder_get_type, [TempNode(instance_type), TempNode(TypeType(owner_type))],
1039+
[nodes.ARG_POS, nodes.ARG_POS], context)
1040+
1041+
if isinstance(inferred_dunder_get_type, AnyType):
1042+
# check_call failed, and will have reported an error
1043+
return inferred_dunder_get_type
1044+
1045+
if not isinstance(inferred_dunder_get_type, CallableType):
1046+
self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
1047+
return AnyType()
1048+
1049+
return inferred_dunder_get_type.ret_type
9901050

9911051
def analyze_external_member_access(self, member: str, base_type: Type,
9921052
context: Context) -> Type:

mypy/checkmember.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
210210
if isinstance(vv, Decorator):
211211
# The associated Var node of a decorator contains the type.
212212
v = vv.var
213+
213214
if isinstance(v, Var):
214215
return analyze_var(name, v, itype, info, node, is_lvalue, msg,
215216
original_type, not_ready_callback)

0 commit comments

Comments
 (0)