Skip to content

Commit f47bc23

Browse files
authored
Move descriptor access logic to checkmember module (#5460)
Fixes #5455 This does a minor refactoring and also fixes two corner cases: * Descriptors are not invoked for access via `__getattr__` * Descriptors are not invoked if they were assigned to `self` (as opposite to defined in _class body_)
1 parent acfcfc6 commit f47bc23

File tree

4 files changed

+129
-76
lines changed

4 files changed

+129
-76
lines changed

mypy/checker.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
from mypy.sametypes import is_same_type, is_same_types
3636
from mypy.messages import MessageBuilder, make_inferred_type_note
3737
import mypy.checkexpr
38-
from mypy.checkmember import map_type_from_supertype, bind_self, erase_to_bound, type_object_type
38+
from mypy.checkmember import (
39+
map_type_from_supertype, bind_self, erase_to_bound, type_object_type,
40+
analyze_descriptor_access
41+
)
3942
from mypy import messages
4043
from mypy.subtypes import (
4144
is_subtype, is_equivalent, is_proper_subtype, is_more_precise,
@@ -2241,8 +2244,9 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type,
22412244
# (which allow you to override the descriptor with any value), but preserves
22422245
# the type of accessing the attribute (even after the override).
22432246
if attribute_type.type.has_readable_member('__get__'):
2244-
attribute_type = self.expr_checker.analyze_descriptor_access(
2245-
instance_type, attribute_type, context)
2247+
attribute_type = analyze_descriptor_access(
2248+
instance_type, attribute_type, self.named_type,
2249+
self.msg, context, chk=self)
22462250
rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context)
22472251
return rvalue_type, True
22482252

mypy/checkexpr.py

+1-65
Original file line numberDiff line numberDiff line change
@@ -1657,71 +1657,7 @@ def analyze_ordinary_member_access(self, e: MemberExpr,
16571657
e.name, original_type, e, is_lvalue, False, False,
16581658
self.named_type, self.not_ready_callback, self.msg,
16591659
original_type=original_type, chk=self.chk)
1660-
if is_lvalue:
1661-
return member_type
1662-
else:
1663-
return self.analyze_descriptor_access(original_type, member_type, e)
1664-
1665-
def analyze_descriptor_access(self, instance_type: Type, descriptor_type: Type,
1666-
context: Context) -> Type:
1667-
"""Type check descriptor access.
1668-
1669-
Arguments:
1670-
instance_type: The type of the instance on which the descriptor
1671-
attribute is being accessed (the type of ``a`` in ``a.f`` when
1672-
``f`` is a descriptor).
1673-
descriptor_type: The type of the descriptor attribute being accessed
1674-
(the type of ``f`` in ``a.f`` when ``f`` is a descriptor).
1675-
context: The node defining the context of this inference.
1676-
Return:
1677-
The return type of the appropriate ``__get__`` overload for the descriptor.
1678-
"""
1679-
if isinstance(descriptor_type, UnionType):
1680-
# Map the access over union types
1681-
return UnionType.make_simplified_union([
1682-
self.analyze_descriptor_access(instance_type, typ, context)
1683-
for typ in descriptor_type.items
1684-
])
1685-
elif not isinstance(descriptor_type, Instance):
1686-
return descriptor_type
1687-
1688-
if not descriptor_type.type.has_readable_member('__get__'):
1689-
return descriptor_type
1690-
1691-
dunder_get = descriptor_type.type.get_method('__get__')
1692-
1693-
if dunder_get is None:
1694-
self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
1695-
return AnyType(TypeOfAny.from_error)
1696-
1697-
function = function_type(dunder_get, self.named_type('builtins.function'))
1698-
bound_method = bind_self(function, descriptor_type)
1699-
typ = map_instance_to_supertype(descriptor_type, dunder_get.info)
1700-
dunder_get_type = expand_type_by_instance(bound_method, typ)
1701-
1702-
if isinstance(instance_type, FunctionLike) and instance_type.is_type_obj():
1703-
owner_type = instance_type.items()[0].ret_type
1704-
instance_type = NoneTyp()
1705-
elif isinstance(instance_type, TypeType):
1706-
owner_type = instance_type.item
1707-
instance_type = NoneTyp()
1708-
else:
1709-
owner_type = instance_type
1710-
1711-
_, inferred_dunder_get_type = self.check_call(
1712-
dunder_get_type,
1713-
[TempNode(instance_type), TempNode(TypeType.make_normalized(owner_type))],
1714-
[nodes.ARG_POS, nodes.ARG_POS], context)
1715-
1716-
if isinstance(inferred_dunder_get_type, AnyType):
1717-
# check_call failed, and will have reported an error
1718-
return inferred_dunder_get_type
1719-
1720-
if not isinstance(inferred_dunder_get_type, CallableType):
1721-
self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
1722-
return AnyType(TypeOfAny.from_error)
1723-
1724-
return inferred_dunder_get_type.ret_type
1660+
return member_type
17251661

17261662
def analyze_external_member_access(self, member: str, base_type: Type,
17271663
context: Context) -> Type:

mypy/checkmember.py

+86-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010
from mypy.nodes import (
1111
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr,
12-
ARG_POS, ARG_STAR, ARG_STAR2, Decorator, OverloadedFuncDef, TypeAlias
12+
ARG_POS, ARG_STAR, ARG_STAR2, Decorator, OverloadedFuncDef, TypeAlias, TempNode
1313
)
1414
from mypy.messages import MessageBuilder
1515
from mypy.maptype import map_instance_to_supertype
@@ -82,7 +82,7 @@ def analyze_member_access(name: str,
8282
assert isinstance(method, OverloadedFuncDef)
8383
first_item = cast(Decorator, method.items[0])
8484
return analyze_var(name, first_item.var, typ, info, node, is_lvalue, msg,
85-
original_type, not_ready_callback, chk=chk)
85+
original_type, builtin_type, not_ready_callback, chk=chk)
8686
if is_lvalue:
8787
msg.cant_assign_to_method(node)
8888
signature = function_type(method, builtin_type('builtins.function'))
@@ -155,7 +155,7 @@ def analyze_member_access(name: str,
155155
# See https://github.com/python/mypy/pull/1787 for more info.
156156
result = analyze_class_attribute_access(ret_type, name, node, is_lvalue,
157157
builtin_type, not_ready_callback, msg,
158-
original_type=original_type)
158+
original_type=original_type, chk=chk)
159159
if result:
160160
return result
161161
# Look up from the 'type' type.
@@ -203,7 +203,7 @@ def analyze_member_access(name: str,
203203
# See comment above for why operators are skipped
204204
result = analyze_class_attribute_access(item, name, node, is_lvalue,
205205
builtin_type, not_ready_callback, msg,
206-
original_type=original_type)
206+
original_type=original_type, chk=chk)
207207
if result:
208208
if not (isinstance(result, AnyType) and item.type.fallback_to_any):
209209
return result
@@ -261,8 +261,10 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
261261
v.info = info
262262

263263
if isinstance(v, Var):
264+
implicit = info[name].implicit
264265
return analyze_var(name, v, itype, info, node, is_lvalue, msg,
265-
original_type, not_ready_callback, chk=chk)
266+
original_type, builtin_type, not_ready_callback,
267+
chk=chk, implicit=implicit)
266268
elif isinstance(v, FuncDef):
267269
assert False, "Did not expect a function"
268270
elif not v and name not in ['__getattr__', '__setattr__', '__getattribute__']:
@@ -302,6 +304,72 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
302304
return msg.has_no_attr(original_type, itype, name, node)
303305

304306

307+
def analyze_descriptor_access(instance_type: Type, descriptor_type: Type,
308+
builtin_type: Callable[[str], Instance],
309+
msg: MessageBuilder,
310+
context: Context, *,
311+
chk: 'mypy.checker.TypeChecker') -> Type:
312+
"""Type check descriptor access.
313+
314+
Arguments:
315+
instance_type: The type of the instance on which the descriptor
316+
attribute is being accessed (the type of ``a`` in ``a.f`` when
317+
``f`` is a descriptor).
318+
descriptor_type: The type of the descriptor attribute being accessed
319+
(the type of ``f`` in ``a.f`` when ``f`` is a descriptor).
320+
context: The node defining the context of this inference.
321+
Return:
322+
The return type of the appropriate ``__get__`` overload for the descriptor.
323+
"""
324+
if isinstance(descriptor_type, UnionType):
325+
# Map the access over union types
326+
return UnionType.make_simplified_union([
327+
analyze_descriptor_access(instance_type, typ, builtin_type,
328+
msg, context, chk=chk)
329+
for typ in descriptor_type.items
330+
])
331+
elif not isinstance(descriptor_type, Instance):
332+
return descriptor_type
333+
334+
if not descriptor_type.type.has_readable_member('__get__'):
335+
return descriptor_type
336+
337+
dunder_get = descriptor_type.type.get_method('__get__')
338+
339+
if dunder_get is None:
340+
msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
341+
return AnyType(TypeOfAny.from_error)
342+
343+
function = function_type(dunder_get, builtin_type('builtins.function'))
344+
bound_method = bind_self(function, descriptor_type)
345+
typ = map_instance_to_supertype(descriptor_type, dunder_get.info)
346+
dunder_get_type = expand_type_by_instance(bound_method, typ)
347+
348+
if isinstance(instance_type, FunctionLike) and instance_type.is_type_obj():
349+
owner_type = instance_type.items()[0].ret_type
350+
instance_type = NoneTyp()
351+
elif isinstance(instance_type, TypeType):
352+
owner_type = instance_type.item
353+
instance_type = NoneTyp()
354+
else:
355+
owner_type = instance_type
356+
357+
_, inferred_dunder_get_type = chk.expr_checker.check_call(
358+
dunder_get_type,
359+
[TempNode(instance_type), TempNode(TypeType.make_normalized(owner_type))],
360+
[ARG_POS, ARG_POS], context)
361+
362+
if isinstance(inferred_dunder_get_type, AnyType):
363+
# check_call failed, and will have reported an error
364+
return inferred_dunder_get_type
365+
366+
if not isinstance(inferred_dunder_get_type, CallableType):
367+
msg.fail("{}.__get__ is not callable".format(descriptor_type), context)
368+
return AnyType(TypeOfAny.from_error)
369+
370+
return inferred_dunder_get_type.ret_type
371+
372+
305373
def instance_alias_type(alias: TypeAlias,
306374
builtin_type: Callable[[str], Instance]) -> Type:
307375
"""Type of a type alias node targeting an instance, when appears in runtime context.
@@ -317,14 +385,16 @@ def instance_alias_type(alias: TypeAlias,
317385

318386
def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Context,
319387
is_lvalue: bool, msg: MessageBuilder, original_type: Type,
388+
builtin_type: Callable[[str], Instance],
320389
not_ready_callback: Callable[[str, Context], None], *,
321-
chk: 'mypy.checker.TypeChecker') -> Type:
390+
chk: 'mypy.checker.TypeChecker', implicit: bool = False) -> Type:
322391
"""Analyze access to an attribute via a Var node.
323392
324393
This is conceptually part of analyze_member_access and the arguments are similar.
325394
326395
itype is the class object in which var is defined
327396
original_type is the type of E in the expression E.var
397+
if implicit is True, the original Var was created as an assignment to self
328398
"""
329399
# Found a member variable.
330400
itype = map_instance_to_supertype(itype, var.info)
@@ -374,6 +444,9 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
374444
result = AnyType(TypeOfAny.special_form)
375445
fullname = '{}.{}'.format(var.info.fullname(), name)
376446
hook = chk.plugin.get_attribute_hook(fullname)
447+
if result and not is_lvalue and not implicit:
448+
result = analyze_descriptor_access(original_type, result, builtin_type,
449+
msg, node, chk=chk)
377450
if hook:
378451
result = hook(AttributeContext(original_type, result, node, chk))
379452
return result
@@ -445,7 +518,8 @@ def analyze_class_attribute_access(itype: Instance,
445518
builtin_type: Callable[[str], Instance],
446519
not_ready_callback: Callable[[str, Context], None],
447520
msg: MessageBuilder,
448-
original_type: Type) -> Optional[Type]:
521+
original_type: Type,
522+
chk: 'mypy.checker.TypeChecker') -> Optional[Type]:
449523
"""original_type is the type of E in the expression E.var"""
450524
node = itype.type.get(name)
451525
if not node:
@@ -474,7 +548,11 @@ def analyze_class_attribute_access(itype: Instance,
474548
msg.fail(messages.GENERIC_INSTANCE_VAR_CLASS_ACCESS, context)
475549
is_classmethod = ((is_decorated and cast(Decorator, node.node).func.is_class)
476550
or (isinstance(node.node, FuncBase) and node.node.is_class))
477-
return add_class_tvars(t, itype, is_classmethod, builtin_type, original_type)
551+
result = add_class_tvars(t, itype, is_classmethod, builtin_type, original_type)
552+
if not (is_lvalue or is_method):
553+
result = analyze_descriptor_access(original_type, result, builtin_type,
554+
msg, context, chk=chk)
555+
return result
478556
elif isinstance(node.node, Var):
479557
not_ready_callback(name, context)
480558
return AnyType(TypeOfAny.special_form)

test-data/unit/check-classes.test

+35
Original file line numberDiff line numberDiff line change
@@ -4499,6 +4499,41 @@ def __getattr__(attr: str) -> Any: ...
44994499
[builtins fixtures/module.pyi]
45004500
[out]
45014501

4502+
[case testGetAttrDescriptor]
4503+
from typing import TypeVar, Generic, Any
4504+
4505+
T = TypeVar('T')
4506+
class C(Generic[T]):
4507+
normal: T
4508+
def __getattr__(self, attr: str) -> T: ...
4509+
4510+
class Descr:
4511+
def __get__(self, inst: Any, owner: Any) -> int: ...
4512+
4513+
class D(C[Descr]):
4514+
other: Descr
4515+
4516+
d: D
4517+
reveal_type(d.normal) # E: Revealed type is 'builtins.int'
4518+
reveal_type(d.dynamic) # E: Revealed type is '__main__.Descr*'
4519+
reveal_type(D.other) # E: Revealed type is 'builtins.int'
4520+
D.dynamic # E: "Type[D]" has no attribute "dynamic"
4521+
[out]
4522+
4523+
[case testSelfDescriptorAssign]
4524+
from typing import Any
4525+
4526+
class Descr:
4527+
def __get__(self, inst: Any, owner: Any) -> int: ...
4528+
4529+
class C:
4530+
def __init__(self, x: Descr) -> None:
4531+
self.x = x
4532+
4533+
c = C(Descr())
4534+
reveal_type(c.x) # E: Revealed type is '__main__.Descr'
4535+
[out]
4536+
45024537
[case testForwardInstanceWithWrongArgCount]
45034538
from typing import TypeVar, Generic
45044539

0 commit comments

Comments
 (0)