Skip to content

Commit 29ba885

Browse files
David Eurestiilevkivskyi
David Euresti
authored andcommitted
Add Class Decorator/Metaclass/Base Class plugin (#4328)
Such plugin kinds will be useful for projects like attrs, see #2088
1 parent f6dec70 commit 29ba885

File tree

3 files changed

+95
-9
lines changed

3 files changed

+95
-9
lines changed

mypy/plugin.py

+51-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from abc import abstractmethod
55
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar
66

7-
from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr
7+
from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef
88
from mypy.types import (
99
Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType,
1010
AnyType, TypeList, UnboundType, TypeOfAny
@@ -13,7 +13,7 @@
1313
from mypy.options import Options
1414

1515

16-
class AnalyzerPluginInterface:
16+
class TypeAnalyzerPluginInterface:
1717
"""Interface for accessing semantic analyzer functionality in plugins."""
1818

1919
@abstractmethod
@@ -40,7 +40,7 @@ def analyze_callable_args(self, arglist: TypeList) -> Optional[Tuple[List[Type],
4040
'AnalyzeTypeContext', [
4141
('type', UnboundType), # Type to analyze
4242
('context', Context),
43-
('api', AnalyzerPluginInterface)])
43+
('api', TypeAnalyzerPluginInterface)])
4444

4545

4646
class CheckerPluginInterface:
@@ -53,6 +53,23 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance:
5353
raise NotImplementedError
5454

5555

56+
class SemanticAnalyzerPluginInterface:
57+
"""Interface for accessing semantic analyzer functionality in plugins."""
58+
59+
@abstractmethod
60+
def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance:
61+
raise NotImplementedError
62+
63+
@abstractmethod
64+
def parse_bool(self, expr: Expression) -> Optional[bool]:
65+
raise NotImplementedError
66+
67+
@abstractmethod
68+
def fail(self, msg: str, ctx: Context, serious: bool = False, *,
69+
blocker: bool = False) -> None:
70+
raise NotImplementedError
71+
72+
5673
# A context for a function hook that infers the return type of a function with
5774
# a special signature.
5875
#
@@ -98,6 +115,14 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance:
98115
('context', Context),
99116
('api', CheckerPluginInterface)])
100117

118+
# A context for a class hook that modifies the class definition.
119+
ClassDefContext = NamedTuple(
120+
'ClassDecoratorContext', [
121+
('cls', ClassDef), # The class definition
122+
('reason', Expression), # The expression being applied (decorator, metaclass, base class)
123+
('api', SemanticAnalyzerPluginInterface)
124+
])
125+
101126

102127
class Plugin:
103128
"""Base class of all type checker plugins.
@@ -136,7 +161,17 @@ def get_attribute_hook(self, fullname: str
136161
) -> Optional[Callable[[AttributeContext], Type]]:
137162
return None
138163

139-
# TODO: metaclass / class decorator hook
164+
def get_class_decorator_hook(self, fullname: str
165+
) -> Optional[Callable[[ClassDefContext], None]]:
166+
return None
167+
168+
def get_metaclass_hook(self, fullname: str
169+
) -> Optional[Callable[[ClassDefContext], None]]:
170+
return None
171+
172+
def get_base_class_hook(self, fullname: str
173+
) -> Optional[Callable[[ClassDefContext], None]]:
174+
return None
140175

141176

142177
T = TypeVar('T')
@@ -182,6 +217,18 @@ def get_attribute_hook(self, fullname: str
182217
) -> Optional[Callable[[AttributeContext], Type]]:
183218
return self._find_hook(lambda plugin: plugin.get_attribute_hook(fullname))
184219

220+
def get_class_decorator_hook(self, fullname: str
221+
) -> Optional[Callable[[ClassDefContext], None]]:
222+
return self._find_hook(lambda plugin: plugin.get_class_decorator_hook(fullname))
223+
224+
def get_metaclass_hook(self, fullname: str
225+
) -> Optional[Callable[[ClassDefContext], None]]:
226+
return self._find_hook(lambda plugin: plugin.get_metaclass_hook(fullname))
227+
228+
def get_base_class_hook(self, fullname: str
229+
) -> Optional[Callable[[ClassDefContext], None]]:
230+
return self._find_hook(lambda plugin: plugin.get_base_class_hook(fullname))
231+
185232
def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]:
186233
for plugin in self._plugins:
187234
hook = lookup(plugin)

mypy/semanal.py

+42-3
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
from mypy.sametypes import is_same_type
8282
from mypy.options import Options
8383
from mypy import experiments
84-
from mypy.plugin import Plugin
84+
from mypy.plugin import Plugin, ClassDefContext, SemanticAnalyzerPluginInterface
8585
from mypy import join
8686
from mypy.util import get_prefix
8787

@@ -172,7 +172,7 @@
172172
}
173173

174174

175-
class SemanticAnalyzerPass2(NodeVisitor[None]):
175+
class SemanticAnalyzerPass2(NodeVisitor[None], SemanticAnalyzerPluginInterface):
176176
"""Semantically analyze parsed mypy files.
177177
178178
The analyzer binds names and does various consistency checks for a
@@ -715,9 +715,48 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]:
715715
yield True
716716
self.calculate_abstract_status(defn.info)
717717
self.setup_type_promotion(defn)
718-
718+
self.apply_class_plugin_hooks(defn)
719719
self.leave_class()
720720

721+
def apply_class_plugin_hooks(self, defn: ClassDef) -> None:
722+
"""Apply a plugin hook that may infer a more precise definition for a class."""
723+
def get_fullname(expr: Expression) -> Optional[str]:
724+
if isinstance(expr, CallExpr):
725+
return get_fullname(expr.callee)
726+
elif isinstance(expr, IndexExpr):
727+
return get_fullname(expr.base)
728+
elif isinstance(expr, RefExpr):
729+
if expr.fullname:
730+
return expr.fullname
731+
# If we don't have a fullname look it up. This happens because base classes are
732+
# analyzed in a different manner (see exprtotype.py) and therefore those AST
733+
# nodes will not have full names.
734+
sym = self.lookup_type_node(expr)
735+
if sym:
736+
return sym.fullname
737+
return None
738+
739+
for decorator in defn.decorators:
740+
decorator_name = get_fullname(decorator)
741+
if decorator_name:
742+
hook = self.plugin.get_class_decorator_hook(decorator_name)
743+
if hook:
744+
hook(ClassDefContext(defn, decorator, self))
745+
746+
if defn.metaclass:
747+
metaclass_name = get_fullname(defn.metaclass)
748+
if metaclass_name:
749+
hook = self.plugin.get_metaclass_hook(metaclass_name)
750+
if hook:
751+
hook(ClassDefContext(defn, defn.metaclass, self))
752+
753+
for base_expr in defn.base_type_exprs:
754+
base_name = get_fullname(base_expr)
755+
if base_name:
756+
hook = self.plugin.get_base_class_hook(base_name)
757+
if hook:
758+
hook(ClassDefContext(defn, base_expr, self))
759+
721760
def analyze_class_keywords(self, defn: ClassDef) -> None:
722761
for value in defn.keywords.values():
723762
value.accept(self)

mypy/typeanal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from mypy.sametypes import is_same_type
2828
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
2929
from mypy.subtypes import is_subtype
30-
from mypy.plugin import Plugin, AnalyzerPluginInterface, AnalyzeTypeContext
30+
from mypy.plugin import Plugin, TypeAnalyzerPluginInterface, AnalyzeTypeContext
3131
from mypy import nodes, messages
3232

3333

@@ -132,7 +132,7 @@ def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str:
132132
return msg
133133

134134

135-
class TypeAnalyser(SyntheticTypeVisitor[Type], AnalyzerPluginInterface):
135+
class TypeAnalyser(SyntheticTypeVisitor[Type], TypeAnalyzerPluginInterface):
136136
"""Semantic analyzer for types (semantic analysis pass 2).
137137
138138
Converts unbound types into bound types.

0 commit comments

Comments
 (0)