Skip to content

Commit

Permalink
Add a customize_class_mro plugin hook
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
snarkmaster committed Aug 5, 2018
1 parent 1222c96 commit 5ed1b38
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 14 deletions.
8 changes: 8 additions & 0 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ def get_base_class_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return None

def get_customize_class_mro_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return None


T = TypeVar('T')

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

def get_customize_class_mro_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return self._find_hook(lambda plugin: plugin.get_customize_class_mro_hook(fullname))

def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]:
for plugin in self._plugins:
hook = lookup(plugin)
Expand Down
25 changes: 15 additions & 10 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,12 +1132,26 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
# Give it an MRO consisting of just the class itself and object.
defn.info.mro = [defn.info, self.object_type().type]
return
calculate_class_mro(defn, self.fail_blocker)
self.calculate_class_mro(defn)
# If there are cyclic imports, we may be missing 'object' in
# the MRO. Fix MRO if needed.
if info.mro and info.mro[-1].fullname() != 'builtins.object':
info.mro.append(self.object_type().type)

def calculate_class_mro(self, defn: ClassDef) -> None:
try:
calculate_mro(defn.info)
except MroError:
self.fail_blocker('Cannot determine consistent method resolution '
'order (MRO) for "%s"' % defn.name, defn)
defn.info.mro = []
# Allow plugins to alter the MRO to handle the fact that `def mro()`
# on metaclasses permits MRO rewriting.
if defn.fullname:
hook = self.plugin.get_customize_class_mro_hook(defn.fullname)
if hook:
hook(ClassDefContext(defn, Expression(), self))

def update_metaclass(self, defn: ClassDef) -> None:
"""Lookup for special metaclass declarations, and update defn fields accordingly.
Expand Down Expand Up @@ -3416,15 +3430,6 @@ def refers_to_class_or_function(node: Expression) -> bool:
isinstance(node.node, (TypeInfo, FuncDef, OverloadedFuncDef)))


def calculate_class_mro(defn: ClassDef, fail: Callable[[str, Context], None]) -> None:
try:
calculate_mro(defn.info)
except MroError:
fail("Cannot determine consistent method resolution order "
'(MRO) for "%s"' % defn.name, defn)
defn.info.mro = []


def calculate_mro(info: TypeInfo) -> None:
"""Calculate and set mro (method resolution order).
Expand Down
8 changes: 4 additions & 4 deletions mypy/semanal_pass3.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
from mypy.typeanal import TypeAnalyserPass3, collect_any_types
from mypy.typevars import has_no_typevars
from mypy.semanal_shared import PRIORITY_FORWARD_REF, PRIORITY_TYPEVAR_VALUES
from mypy.semanal import SemanticAnalyzerPass2
from mypy.subtypes import is_subtype
from mypy.sametypes import is_same_type
from mypy.scope import Scope
from mypy.semanal_shared import SemanticAnalyzerCoreInterface
import mypy.semanal


class SemanticAnalyzerPass3(TraverserVisitor, SemanticAnalyzerCoreInterface):
Expand All @@ -45,7 +45,7 @@ class SemanticAnalyzerPass3(TraverserVisitor, SemanticAnalyzerCoreInterface):
"""

def __init__(self, modules: Dict[str, MypyFile], errors: Errors,
sem: 'mypy.semanal.SemanticAnalyzerPass2') -> None:
sem: SemanticAnalyzerPass2) -> None:
self.modules = modules
self.errors = errors
self.sem = sem
Expand Down Expand Up @@ -138,7 +138,7 @@ def visit_class_def(self, tdef: ClassDef) -> None:
# import loop. (Only do so if we succeeded the first time.)
if tdef.info.mro:
tdef.info.mro = [] # Force recomputation
mypy.semanal.calculate_class_mro(tdef, self.fail_blocker)
self.sem.calculate_class_mro(tdef)
if tdef.analyzed is not None:
# Also check synthetic types associated with this ClassDef.
# Currently these are TypedDict, and NamedTuple.
Expand Down Expand Up @@ -230,7 +230,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
self.analyze_info(analyzed.info)
if analyzed.info and analyzed.info.mro:
analyzed.info.mro = [] # Force recomputation
mypy.semanal.calculate_class_mro(analyzed.info.defn, self.fail_blocker)
self.sem.calculate_class_mro(analyzed.info.defn)
if isinstance(analyzed, TypeVarExpr):
types = []
if analyzed.upper_bound:
Expand Down

0 comments on commit 5ed1b38

Please sign in to comment.