Skip to content

Commit 940c9b9

Browse files
snarkmasterilevkivskyi
authored andcommitted
Add a customize_class_mro plugin hook (#4567)
The rationale for this MRO hook is documented on #4527 This patch completely addresses my need for customizing the MRO of types that use my metaclass, and I believe it is simple & general enough for other plugin authors.
1 parent 581e514 commit 940c9b9

File tree

3 files changed

+34
-21
lines changed

3 files changed

+34
-21
lines changed

mypy/plugin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ def get_base_class_hook(self, fullname: str
214214
) -> Optional[Callable[[ClassDefContext], None]]:
215215
return None
216216

217+
def get_customize_class_mro_hook(self, fullname: str
218+
) -> Optional[Callable[[ClassDefContext], None]]:
219+
return None
220+
217221

218222
T = TypeVar('T')
219223

@@ -270,6 +274,10 @@ def get_base_class_hook(self, fullname: str
270274
) -> Optional[Callable[[ClassDefContext], None]]:
271275
return self._find_hook(lambda plugin: plugin.get_base_class_hook(fullname))
272276

277+
def get_customize_class_mro_hook(self, fullname: str
278+
) -> Optional[Callable[[ClassDefContext], None]]:
279+
return self._find_hook(lambda plugin: plugin.get_customize_class_mro_hook(fullname))
280+
273281
def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]:
274282
for plugin in self._plugins:
275283
hook = lookup(plugin)

mypy/semanal.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,28 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
11451145
return
11461146
# TODO: Ideally we should move MRO calculation to a later stage, but this is
11471147
# not easy, see issue #5536.
1148-
calculate_class_mro(defn, self.fail_blocker, self.object_type)
1148+
self.calculate_class_mro(defn, self.object_type)
1149+
1150+
def calculate_class_mro(self, defn: ClassDef,
1151+
obj_type: Optional[Callable[[], Instance]] = None) -> None:
1152+
"""Calculate method resolution order for a class.
1153+
1154+
`obj_type` may be omitted in the third pass when all classes are already analyzed.
1155+
It exists just to fill in empty base class list during second pass in case of
1156+
an import cycle.
1157+
"""
1158+
try:
1159+
calculate_mro(defn.info, obj_type)
1160+
except MroError:
1161+
self.fail_blocker('Cannot determine consistent method resolution '
1162+
'order (MRO) for "%s"' % defn.name, defn)
1163+
defn.info.mro = []
1164+
# Allow plugins to alter the MRO to handle the fact that `def mro()`
1165+
# on metaclasses permits MRO rewriting.
1166+
if defn.fullname:
1167+
hook = self.plugin.get_customize_class_mro_hook(defn.fullname)
1168+
if hook:
1169+
hook(ClassDefContext(defn, Expression(), self))
11491170

11501171
def update_metaclass(self, defn: ClassDef) -> None:
11511172
"""Lookup for special metaclass declarations, and update defn fields accordingly.
@@ -3428,22 +3449,6 @@ def refers_to_class_or_function(node: Expression) -> bool:
34283449
isinstance(node.node, (TypeInfo, FuncDef, OverloadedFuncDef)))
34293450

34303451

3431-
def calculate_class_mro(defn: ClassDef, fail: Callable[[str, Context], None],
3432-
obj_type: Optional[Callable[[], Instance]] = None) -> None:
3433-
"""Calculate method resolution order for a class.
3434-
3435-
`obj_type` may be omitted in the third pass when all classes are already analyzed.
3436-
It exists just to fill in empty base class list during second pass in case of
3437-
an import cycle.
3438-
"""
3439-
try:
3440-
calculate_mro(defn.info, obj_type)
3441-
except MroError:
3442-
fail("Cannot determine consistent method resolution order "
3443-
'(MRO) for "%s"' % defn.name, defn)
3444-
defn.info.mro = []
3445-
3446-
34473452
def calculate_mro(info: TypeInfo, obj_type: Optional[Callable[[], Instance]] = None) -> None:
34483453
"""Calculate and set mro (method resolution order).
34493454

mypy/semanal_pass3.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
from mypy.typeanal import TypeAnalyserPass3, collect_any_types
3131
from mypy.typevars import has_no_typevars
3232
from mypy.semanal_shared import PRIORITY_FORWARD_REF, PRIORITY_TYPEVAR_VALUES
33+
from mypy.semanal import SemanticAnalyzerPass2
3334
from mypy.subtypes import is_subtype
3435
from mypy.sametypes import is_same_type
3536
from mypy.scope import Scope
3637
from mypy.semanal_shared import SemanticAnalyzerCoreInterface
37-
import mypy.semanal
3838

3939

4040
class SemanticAnalyzerPass3(TraverserVisitor, SemanticAnalyzerCoreInterface):
@@ -45,7 +45,7 @@ class SemanticAnalyzerPass3(TraverserVisitor, SemanticAnalyzerCoreInterface):
4545
"""
4646

4747
def __init__(self, modules: Dict[str, MypyFile], errors: Errors,
48-
sem: 'mypy.semanal.SemanticAnalyzerPass2') -> None:
48+
sem: SemanticAnalyzerPass2) -> None:
4949
self.modules = modules
5050
self.errors = errors
5151
self.sem = sem
@@ -138,7 +138,7 @@ def visit_class_def(self, tdef: ClassDef) -> None:
138138
# import loop. (Only do so if we succeeded the first time.)
139139
if tdef.info.mro:
140140
tdef.info.mro = [] # Force recomputation
141-
mypy.semanal.calculate_class_mro(tdef, self.fail_blocker)
141+
self.sem.calculate_class_mro(tdef)
142142
if tdef.analyzed is not None:
143143
# Also check synthetic types associated with this ClassDef.
144144
# Currently these are TypedDict, and NamedTuple.
@@ -230,7 +230,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
230230
self.analyze_info(analyzed.info)
231231
if analyzed.info and analyzed.info.mro:
232232
analyzed.info.mro = [] # Force recomputation
233-
mypy.semanal.calculate_class_mro(analyzed.info.defn, self.fail_blocker)
233+
self.sem.calculate_class_mro(analyzed.info.defn)
234234
if isinstance(analyzed, TypeVarExpr):
235235
types = []
236236
if analyzed.upper_bound:

0 commit comments

Comments
 (0)