Skip to content

Commit 5ed1b38

Browse files
committed
Add a customize_class_mro plugin hook
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. I did `./runtests.py`, and the only failure is in `testCoberturaParser`, which looks completely unrelated to my changes. Log here: https://gist.github.com/snarkmaster/3f78cf04cfacb7abf2a8da9b95298075 PS You will notice in the patch that I deviated from the pattern of "some_hook(fullname) returns a callback, which takes a context". I did this as a suggestion for improvement. At the very least, all of the -> None hooks could be simplified in this fashion (and probably the others, too). I'm happy to put up a patch for that, if there's a process for dealing with a breaking change in the plugin API. A specific reason I disliked the original pattern is that the provided fullname often does not have enough information for the hook to decide whether it cares, so the hook has to return a callback always. And yet, we spend cycles specifically extracting & passing just the fullname. For this reason, my base class hook is just return base_class_callback.
1 parent 1222c96 commit 5ed1b38

File tree

3 files changed

+27
-14
lines changed

3 files changed

+27
-14
lines changed

mypy/plugin.py

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

213+
def get_customize_class_mro_hook(self, fullname: str
214+
) -> Optional[Callable[[ClassDefContext], None]]:
215+
return None
216+
213217

214218
T = TypeVar('T')
215219

@@ -266,6 +270,10 @@ def get_base_class_hook(self, fullname: str
266270
) -> Optional[Callable[[ClassDefContext], None]]:
267271
return self._find_hook(lambda plugin: plugin.get_base_class_hook(fullname))
268272

273+
def get_customize_class_mro_hook(self, fullname: str
274+
) -> Optional[Callable[[ClassDefContext], None]]:
275+
return self._find_hook(lambda plugin: plugin.get_customize_class_mro_hook(fullname))
276+
269277
def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]:
270278
for plugin in self._plugins:
271279
hook = lookup(plugin)

mypy/semanal.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,12 +1132,26 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
11321132
# Give it an MRO consisting of just the class itself and object.
11331133
defn.info.mro = [defn.info, self.object_type().type]
11341134
return
1135-
calculate_class_mro(defn, self.fail_blocker)
1135+
self.calculate_class_mro(defn)
11361136
# If there are cyclic imports, we may be missing 'object' in
11371137
# the MRO. Fix MRO if needed.
11381138
if info.mro and info.mro[-1].fullname() != 'builtins.object':
11391139
info.mro.append(self.object_type().type)
11401140

1141+
def calculate_class_mro(self, defn: ClassDef) -> None:
1142+
try:
1143+
calculate_mro(defn.info)
1144+
except MroError:
1145+
self.fail_blocker('Cannot determine consistent method resolution '
1146+
'order (MRO) for "%s"' % defn.name, defn)
1147+
defn.info.mro = []
1148+
# Allow plugins to alter the MRO to handle the fact that `def mro()`
1149+
# on metaclasses permits MRO rewriting.
1150+
if defn.fullname:
1151+
hook = self.plugin.get_customize_class_mro_hook(defn.fullname)
1152+
if hook:
1153+
hook(ClassDefContext(defn, Expression(), self))
1154+
11411155
def update_metaclass(self, defn: ClassDef) -> None:
11421156
"""Lookup for special metaclass declarations, and update defn fields accordingly.
11431157
@@ -3416,15 +3430,6 @@ def refers_to_class_or_function(node: Expression) -> bool:
34163430
isinstance(node.node, (TypeInfo, FuncDef, OverloadedFuncDef)))
34173431

34183432

3419-
def calculate_class_mro(defn: ClassDef, fail: Callable[[str, Context], None]) -> None:
3420-
try:
3421-
calculate_mro(defn.info)
3422-
except MroError:
3423-
fail("Cannot determine consistent method resolution order "
3424-
'(MRO) for "%s"' % defn.name, defn)
3425-
defn.info.mro = []
3426-
3427-
34283433
def calculate_mro(info: TypeInfo) -> None:
34293434
"""Calculate and set mro (method resolution order).
34303435

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)