Skip to content

Commit 6f9a449

Browse files
author
root
committed
Restore FuncBase.__instancecheck__ and fix decorators another way
@ilevkivskyi, I see your point about how overriding `__instancecheck__` would create problems with debugging and `mypyc`, so I have implemented an alternate solution. I have created a `get_callable` method to check whether a node is callable and, in the case of a decorator, return an instance of a separate `CallableDecorator` class that inherits from `FuncItem`. I had originally thought to just have an `is_callable` method that would test whether a `Node` was either an instance of `FuncBase` or a callable `Decorator`, but this created problems with type resolution. This solution is not exactly ideal either, but I think will create fewer problems. I agree that the best solution would be to remove the `Decorator` class altogether, but this would be a fairly major refactor. The problem is that we do need a `Decorator` class in early passes before we have type information, and we use the `Node.accept` method to add type information to an instance, we don't have a simple way to replace a node with another node of a different type. Let me know what you think of this approach. One other stray observation: * This fix requires that the decorator is explicitly annotated to return a `Callable`. It seems like this should be something we could determine through type inference, but I'm not sure how easy that would be to do.
1 parent 2657bf7 commit 6f9a449

File tree

6 files changed

+67
-61
lines changed

6 files changed

+67
-61
lines changed

mypy/checker.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
Context, Decorator, PrintStmt, BreakStmt, PassStmt, ContinueStmt,
2222
ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr,
2323
Import, ImportFrom, ImportAll, ImportBase, TypeAlias,
24-
ARG_POS, ARG_STAR, LITERAL_TYPE, MDEF, GDEF,
25-
CONTRAVARIANT, COVARIANT, INVARIANT,
24+
ARG_POS, ARG_STAR, LITERAL_TYPE, MDEF, GDEF, CallableDecorator,
25+
CONTRAVARIANT, COVARIANT, INVARIANT, get_callable
2626
)
2727
from mypy import nodes
2828
from mypy.literals import literal, literal_hash
@@ -1619,11 +1619,15 @@ def check_compatibility(self, name: str, base1: TypeInfo,
16191619
first = base1[name]
16201620
second = base2[name]
16211621
first_type = first.type
1622-
if first_type is None and isinstance(first.node, FuncBase):
1623-
first_type = self.function_type(first.node)
1622+
if first_type is None:
1623+
method = get_callable(first.node)
1624+
if method:
1625+
first_type = self.function_type(method)
16241626
second_type = second.type
1625-
if second_type is None and isinstance(second.node, FuncBase):
1626-
second_type = self.function_type(second.node)
1627+
if second_type is None:
1628+
method = get_callable(second.node)
1629+
if method:
1630+
second_type = self.function_type(method)
16271631
# TODO: What if some classes are generic?
16281632
if (isinstance(first_type, FunctionLike) and
16291633
isinstance(second_type, FunctionLike)):
@@ -3026,9 +3030,10 @@ def visit_decorator(self, e: Decorator) -> None:
30263030
if isinstance(sig, CallableType):
30273031
if e.func.is_property:
30283032
assert isinstance(sig, CallableType)
3029-
e.is_callable = isinstance(sig.ret_type, CallableType)
3033+
if isinstance(sig.ret_type, CallableType):
3034+
e.callable_decorator = CallableDecorator(e)
30303035
else:
3031-
e.is_callable = True
3036+
e.callable_decorator = CallableDecorator(e)
30323037
if e.func.info and not e.func.is_dynamic():
30333038
self.check_method_override(e)
30343039

mypy/nodes.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -383,18 +383,7 @@ def __str__(self) -> str:
383383
] # type: Final
384384

385385

386-
class FuncBaseMeta(type):
387-
def __instancecheck__(self, instance: object) -> bool:
388-
if isinstance(instance, Decorator):
389-
if instance.is_overload:
390-
return issubclass(OverloadedFuncDef, self) and instance.is_callable
391-
else:
392-
return issubclass(FuncBase, self) and instance.is_callable
393-
else:
394-
return super().__instancecheck__(instance)
395-
396-
397-
class FuncBase(Node, metaclass=FuncBaseMeta):
386+
class FuncBase(Node):
398387
"""Abstract base class for function-like nodes"""
399388

400389
__slots__ = ('type',
@@ -668,7 +657,7 @@ class Decorator(SymbolNode, Statement):
668657
# TODO: This is mostly used for the type; consider replacing with a 'type' attribute
669658
var = None # type: Var # Represents the decorated function obj
670659
is_overload = False
671-
is_callable = False
660+
callable_decorator = None # type: Optional[CallableDecorator]
672661

673662
def __init__(self, func: FuncDef, decorators: List[Expression],
674663
var: 'Var') -> None:
@@ -684,23 +673,6 @@ def name(self) -> str:
684673
def fullname(self) -> Bogus[str]:
685674
return self.func.fullname()
686675

687-
@property
688-
def items(self) -> List[OverloadPart]:
689-
assert isinstance(self.func, OverloadedFuncDef)
690-
return self.func.items
691-
692-
@property
693-
def is_property(self) -> bool:
694-
return self.func.is_property
695-
696-
@property
697-
def is_static(self) -> bool:
698-
return self.func.is_static
699-
700-
@property
701-
def is_class(self) -> bool:
702-
return self.func.is_class
703-
704676
@property
705677
def is_final(self) -> bool:
706678
return self.func.is_final
@@ -733,6 +705,28 @@ def deserialize(cls, data: JsonDict) -> 'Decorator':
733705
return dec
734706

735707

708+
class CallableDecorator(FuncItem):
709+
"""A wrapper around a Decorator that allows it to be treated as a callable function"""
710+
def __init__(self, decorator: Decorator) -> None:
711+
super().__init__(decorator.func.arguments, decorator.func.body,
712+
cast('mypy.types.CallableType', decorator.type))
713+
self.is_final = decorator.is_final
714+
self.is_class = decorator.func.is_class
715+
self.is_property = decorator.func.is_property
716+
self.is_static = decorator.func.is_static
717+
self.is_overload = decorator.func.is_overload
718+
self.is_generator = decorator.func.is_generator
719+
self.is_async_generator = decorator.func.is_async_generator
720+
self.is_awaitable_coroutine = decorator.func.is_awaitable_coroutine
721+
self.expanded = decorator.func.expanded
722+
self.info = decorator.info
723+
self._name = decorator.func.name()
724+
self._fullname = decorator.func._fullname
725+
726+
def name(self) -> str:
727+
return self._name
728+
729+
736730
VAR_FLAGS = [
737731
'is_self', 'is_initialized_in_class', 'is_staticmethod',
738732
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import',
@@ -2337,11 +2331,7 @@ def has_readable_member(self, name: str) -> bool:
23372331
def get_method(self, name: str) -> Optional[FuncBase]:
23382332
for cls in self.mro:
23392333
if name in cls.names:
2340-
node = cls.names[name].node
2341-
if isinstance(node, FuncBase):
2342-
return node
2343-
else:
2344-
return None
2334+
return get_callable(cls.names[name].node)
23452335
return None
23462336

23472337
def calculate_metaclass_type(self) -> 'Optional[mypy.types.Instance]':
@@ -2964,3 +2954,13 @@ def is_class_var(expr: NameExpr) -> bool:
29642954
if isinstance(expr.node, Var):
29652955
return expr.node.is_classvar
29662956
return False
2957+
2958+
2959+
def get_callable(node: Optional[Node]) -> Optional[FuncBase]:
2960+
"""Check if the passed node represents a callable function or funcion-like object"""
2961+
if isinstance(node, FuncBase):
2962+
return node
2963+
elif isinstance(node, Decorator):
2964+
return node.callable_decorator
2965+
else:
2966+
return None

mypy/plugins/attrs.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
from mypy.fixup import lookup_qualified_stnode
88
from mypy.nodes import (
99
Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt,
10-
TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncBase,
11-
is_class_var, TempNode, Decorator, MemberExpr, Expression, FuncDef, Block,
12-
PassStmt, SymbolTableNode, MDEF, JsonDict, OverloadedFuncDef
10+
TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, is_class_var,
11+
TempNode, Decorator, MemberExpr, Expression, FuncDef, Block,
12+
PassStmt, SymbolTableNode, MDEF, JsonDict, OverloadedFuncDef, get_callable
1313
)
1414
from mypy.plugins.common import (
1515
_get_argument, _get_bool_argument, _get_decorator_bool_argument
@@ -405,9 +405,8 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext',
405405
# TODO: Support complex converters, e.g. lambdas, calls, etc.
406406
if converter:
407407
if isinstance(converter, RefExpr) and converter.node:
408-
if (isinstance(converter.node, FuncBase)
409-
and converter.node.type
410-
and isinstance(converter.node.type, FunctionLike)):
408+
method = get_callable(converter.node)
409+
if method and method.type and isinstance(method.type, FunctionLike):
411410
return Converter(converter.node.fullname())
412411
elif isinstance(converter.node, TypeInfo):
413412
return Converter(converter.node.fullname())

mypy/plugins/common.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from typing import List, Optional, Any
22

33
from mypy.nodes import (
4-
ARG_POS, MDEF, Argument, Block, CallExpr, Expression, FuncBase,
5-
FuncDef, PassStmt, RefExpr, SymbolTableNode, Var
4+
ARG_POS, MDEF, Argument, Block, CallExpr, Expression, FuncDef,
5+
PassStmt, RefExpr, SymbolTableNode, Var, get_callable
66
)
77
from mypy.plugin import ClassDefContext
88
from mypy.semanal import set_callable_name
@@ -53,8 +53,9 @@ def _get_argument(call: CallExpr, name: str) -> Optional[Expression]:
5353
callee_type = None
5454
# mypyc hack to workaround mypy misunderstanding multiple inheritance (#3603)
5555
callee_node = call.callee.node # type: Any
56-
if (isinstance(callee_node, (Var, FuncBase))
57-
and callee_node.type):
56+
if not isinstance(callee_node, Var):
57+
callee_node = get_callable(callee_node)
58+
if callee_node and callee_node.type:
5859
callee_node_type = callee_node.type
5960
if isinstance(callee_node_type, Overloaded):
6061
# We take the last overload.

mypy/semanal.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2518,8 +2518,6 @@ def visit_decorator(self, dec: Decorator) -> None:
25182518
self.check_decorated_function_is_method('classmethod', dec)
25192519
elif (refers_to_fullname(d, 'builtins.property') or
25202520
refers_to_fullname(d, 'abc.abstractproperty')):
2521-
# TODO: Stop treating properties as special cases and treat as generalized
2522-
# TODO: descriptors
25232521
removed.append(i)
25242522
dec.func.is_property = True
25252523
dec.var.is_property = True

mypy/server/deps.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a
9191
ImportFrom, CallExpr, CastExpr, TypeVarExpr, TypeApplication, IndexExpr, UnaryExpr, OpExpr,
9292
ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt,
9393
TupleExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block,
94-
TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr,
94+
TypeInfo, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr,
9595
LDEF, MDEF, GDEF, TypeAliasExpr, NewTypeExpr, ImportAll, EnumCallExpr, AwaitExpr,
96-
op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods
96+
op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods, get_callable
9797
)
9898
from mypy.traverser import TraverserVisitor
9999
from mypy.types import (
@@ -127,17 +127,18 @@ def get_dependencies_of_target(module_id: str,
127127
# TODO: Add tests for this function.
128128
visitor = DependencyVisitor(type_map, python_version, module_tree.alias_deps)
129129
visitor.scope.enter_file(module_id)
130+
method = get_callable(target)
130131
if isinstance(target, MypyFile):
131132
# Only get dependencies of the top-level of the module. Don't recurse into
132133
# functions.
133134
for defn in target.defs:
134135
# TODO: Recurse into top-level statements and class bodies but skip functions.
135136
if not isinstance(defn, (ClassDef, Decorator, FuncDef, OverloadedFuncDef)):
136137
defn.accept(visitor)
137-
elif isinstance(target, FuncBase) and target.info:
138+
elif method and method.info:
138139
# It's a method.
139140
# TODO: Methods in nested classes.
140-
visitor.scope.enter_class(target.info)
141+
visitor.scope.enter_class(method.info)
141142
target.accept(visitor)
142143
visitor.scope.leave()
143144
else:
@@ -425,8 +426,10 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
425426
if isinstance(rvalue.callee.node, TypeInfo):
426427
# use actual __init__ as a dependency source
427428
init = rvalue.callee.node.get('__init__')
428-
if init and isinstance(init.node, FuncBase):
429-
fname = init.node.fullname()
429+
if init:
430+
method = get_callable(init.node)
431+
if method:
432+
fname = method.fullname()
430433
else:
431434
fname = rvalue.callee.fullname
432435
if fname is None:

0 commit comments

Comments
 (0)