Skip to content

Commit

Permalink
Implement the descriptor protocol (#2266)
Browse files Browse the repository at this point in the history
* Implement the descriptor protocol
  • Loading branch information
cpennington authored and JukkaL committed Dec 13, 2016
1 parent aee172d commit 2013112
Show file tree
Hide file tree
Showing 4 changed files with 484 additions and 13 deletions.
61 changes: 60 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from mypy.maptype import map_instance_to_supertype
from mypy.semanal import fill_typevars, set_callable_name, refers_to_fullname
from mypy.erasetype import erase_typevars
from mypy.expandtype import expand_type
from mypy.expandtype import expand_type, expand_type_by_instance
from mypy.visitor import NodeVisitor
from mypy.join import join_types
from mypy.treetransform import TransformVisitor
Expand Down Expand Up @@ -1149,6 +1149,11 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
not new_syntax):
# Allow None's to be assigned to class variables with non-Optional types.
rvalue_type = lvalue_type
elif (isinstance(lvalue, MemberExpr) and
lvalue.kind is None): # Ignore member access to modules
instance_type = self.accept(lvalue.expr)
rvalue_type, infer_lvalue_type = self.check_member_assignment(
instance_type, lvalue_type, rvalue, lvalue)
else:
rvalue_type = self.check_simple_assignment(lvalue_type, rvalue, lvalue)

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

def check_member_assignment(self, instance_type: Type, attribute_type: Type,
rvalue: Expression, context: Context) -> Tuple[Type, bool]:
"""Type member assigment.
This is defers to check_simple_assignment, unless the member expression
is a descriptor, in which case this checks descriptor semantics as well.
Return the inferred rvalue_type and whether to infer anything about the attribute type
"""
# Descriptors don't participate in class-attribute access
if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or
isinstance(instance_type, TypeType)):
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
return rvalue_type, True

if not isinstance(attribute_type, Instance):
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
return rvalue_type, True

if not attribute_type.type.has_readable_member('__set__'):
# If there is no __set__, we type-check that the assigned value matches
# the return type of __get__. This doesn't match the python semantics,
# (which allow you to override the descriptor with any value), but preserves
# the type of accessing the attribute (even after the override).
if attribute_type.type.has_readable_member('__get__'):
attribute_type = self.expr_checker.analyze_descriptor_access(
instance_type, attribute_type, context)
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
return rvalue_type, True

dunder_set = attribute_type.type.get_method('__set__')
if dunder_set is None:
self.msg.fail("{}.__set__ is not callable".format(attribute_type), context)
return AnyType(), False

function = function_type(dunder_set, self.named_type('builtins.function'))
bound_method = bind_self(function, attribute_type)
typ = map_instance_to_supertype(attribute_type, dunder_set.info)
dunder_set_type = expand_type_by_instance(bound_method, typ)

_, inferred_dunder_set_type = self.expr_checker.check_call(
dunder_set_type, [TempNode(instance_type), rvalue],
[nodes.ARG_POS, nodes.ARG_POS], context)

if not isinstance(inferred_dunder_set_type, CallableType):
self.fail("__set__ is not callable", context)
return AnyType(), True

if len(inferred_dunder_set_type.arg_types) < 2:
# A message already will have been recorded in check_call
return AnyType(), False

return inferred_dunder_set_type.arg_types[1], False

def check_indexed_assignment(self, lvalue: IndexExpr,
rvalue: Expression, context: Context) -> None:
"""Type check indexed assignment base[index] = rvalue.
Expand Down
74 changes: 67 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
TupleType, TypedDictType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType,
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType,
true_only, false_only, is_named_instance, function_type,
true_only, false_only, is_named_instance, function_type, FunctionLike,
get_typ_args, set_typ_args,
)
from mypy.nodes import (
Expand All @@ -30,13 +30,14 @@
from mypy import messages
from mypy.infer import infer_type_arguments, infer_function_type_arguments
from mypy import join
from mypy.maptype import map_instance_to_supertype
from mypy.subtypes import is_subtype, is_equivalent
from mypy import applytype
from mypy import erasetype
from mypy.checkmember import analyze_member_access, type_object_type
from mypy.checkmember import analyze_member_access, type_object_type, bind_self
from mypy.constraints import get_actual_type
from mypy.checkstrformat import StringFormatterChecker
from mypy.expandtype import expand_type
from mypy.expandtype import expand_type, expand_type_by_instance
from mypy.util import split_module_names
from mypy.semanal import fill_typevars

Expand Down Expand Up @@ -983,10 +984,69 @@ def analyze_ordinary_member_access(self, e: MemberExpr,
else:
# This is a reference to a non-module attribute.
original_type = self.accept(e.expr)
return analyze_member_access(e.name, original_type, e,
is_lvalue, False, False,
self.named_type, self.not_ready_callback, self.msg,
original_type=original_type, chk=self.chk)
member_type = analyze_member_access(
e.name, original_type, e, is_lvalue, False, False,
self.named_type, self.not_ready_callback, self.msg,
original_type=original_type, chk=self.chk)
if is_lvalue:
return member_type
else:
return self.analyze_descriptor_access(original_type, member_type, e)

def analyze_descriptor_access(self, instance_type: Type, descriptor_type: Type,
context: Context) -> Type:
"""Type check descriptor access.
Arguments:
instance_type: The type of the instance on which the descriptor
attribute is being accessed (the type of ``a`` in ``a.f`` when
``f`` is a descriptor).
descriptor_type: The type of the descriptor attribute being accessed
(the type of ``f`` in ``a.f`` when ``f`` is a descriptor).
context: The node defining the context of this inference.
Return:
The return type of the appropriate ``__get__`` overload for the descriptor.
"""
if not isinstance(descriptor_type, Instance):
return descriptor_type

if not descriptor_type.type.has_readable_member('__get__'):
return descriptor_type

dunder_get = descriptor_type.type.get_method('__get__')

if dunder_get is None:
self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
return AnyType()

function = function_type(dunder_get, self.named_type('builtins.function'))
bound_method = bind_self(function, descriptor_type)
typ = map_instance_to_supertype(descriptor_type, dunder_get.info)
dunder_get_type = expand_type_by_instance(bound_method, typ)
owner_type = None # type: Type

if isinstance(instance_type, FunctionLike) and instance_type.is_type_obj():
owner_type = instance_type.items()[0].ret_type
instance_type = NoneTyp()
elif isinstance(instance_type, TypeType):
owner_type = instance_type.item
instance_type = NoneTyp()
else:
owner_type = instance_type

_, inferred_dunder_get_type = self.check_call(
dunder_get_type, [TempNode(instance_type), TempNode(TypeType(owner_type))],
[nodes.ARG_POS, nodes.ARG_POS], context)

if isinstance(inferred_dunder_get_type, AnyType):
# check_call failed, and will have reported an error
return inferred_dunder_get_type

if not isinstance(inferred_dunder_get_type, CallableType):
self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
return AnyType()

return inferred_dunder_get_type.ret_type

def analyze_external_member_access(self, member: str, base_type: Type,
context: Context) -> Type:
Expand Down
1 change: 1 addition & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
if isinstance(vv, Decorator):
# The associated Var node of a decorator contains the type.
v = vv.var

if isinstance(v, Var):
return analyze_var(name, v, itype, info, node, is_lvalue, msg,
original_type, not_ready_callback)
Expand Down
Loading

0 comments on commit 2013112

Please sign in to comment.