Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the descriptor protocol #2266

Merged
merged 6 commits into from
Dec 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should also have a branch for TypeType. (Unfortunately there are two ways that a class can be represented internally in mypy -- if it came from a specific class reference, it's a FunctionLike with is_type_obj() set, but if it came from Type[C] it's a TypeType.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I covered both places that I was checking is_type_obj with a check for TypeType as well (and added tests to that effect)

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