The hook helpers
The helpers under extended_mypy_django_plugin.plugin.hooks exist as a helper
for defining hooks on a mypy plugin.
The way a mypy plugin works is there is a class that inherits from
mypy.plugin.Plugin with specific hooks that take in a string and returns
a function.
So for example, the get_customize_class_mro_hook hook will take in the
fullname representing classes that can be altered, and a function must be
returned if the plugin wants to do something with that object (such that the
function takes in a single ClassDefContext object and returns None).
The first plugin that mypy encounters which returns a function will win and no other plugins will get to look at that fullname.
This is fine but it can get awkward when the function returned takes in more
options than only the context. To improve this situation, this module implements
a Hook class and associated decorator to turn those hooks into python
descriptors that do the correct thing.
from typing import Generic
from mypy.plugin import Plugin, AttributeContext
from mypy.types import Type as MypyType
from extended_mypy_django_plugin.plugin import hook
class MyPlugin(Plugin):
@hook.hook
class get_attribute_hook(hook.Hook["MyPlugin", AttributeContext, MypyType]):
def choose(
self, *, fullname: str, super_hook: hook.MypyHook[AttributeContext, MypyType]
) -> bool:
# return True if we want to use the run method for ``self.fullname``.
# With super_hook being the result of `super().get_attribute_hook(fullname)` on the plugin
return self.fullname.endswith(".blah")
def run(self, ctx: AttributeContext) -> MypyType:
# Do stuff
return ...
The extended_mypy_django_plugin.plugin.hook.hook decorator turns that
attribute into a descriptor that will cache an instance
of the Hook with the instance of the plugin and that hook on the parent class
(useful when the mypy plugin is subclassing an existing mypy plugin)
The descriptor will then return the hook method on the Hook, which matches the required
signature for the hook.
There are two default implementations of this Hook class:
- class extended_mypy_django_plugin.plugin.hook.PlainHook(*, plugin: T_Plugin, super_hook_maker: Callable[[str], Callable[[T_Ctx], T_Ret] | None])
This is an implementation of
Hookthat does the bare minimum.It’s
hookmethod will determine what would have been returned for this hook from the parent class and pass thefullnameand thatsuper_hooktochoose.If
choosereturnsFalse, thenhookreturns thesuper_hook, otherwise it returnsself.run.
- class extended_mypy_django_plugin.plugin.hook.HookWithExtra(*, plugin: T_Plugin, super_hook_maker: Callable[[str], Callable[[T_Ctx], T_Ret] | None])
This is an implementation of Hook that allows passing information from the choose method into the run method.
Mypy plugins work by implementing methods that take in a string and either return a callable that takes action, or returns None. Whichever plugin returns a function first wins.
This hook class splits the two parts into two methods: deciding if we should choose this hook and the running of the logic.
This implementation in particular also allows passing information from the choosing part into the running part:
from typing import Generic from mypy.plugin import Plugin, AttributeContext from mypy.types import Type as MypyType from extended_mypy_django_plugin.plugin import hook class MyPlugin(Plugin): @hook.hook class get_attribute_hook(hook.HookWithExtra["MyPlugin", AttributeContext, str, MypyType]): def choose( self, *, fullname: str, super_hook: hook.MypyHook[AttributeContext, MypyType] ) -> hook.Choice[str]: if fullname.startswith("things"): return True, "one" elif fullname.startswith("other"): return True, "two" else: return False def run( self, ctx: AttributeContext, *, fullname: str, super_hook: hook.MypyHook[AttributeContext, MypyType], extra: str, ) -> MypyType: # at this point, extra will either be "one" or "two" # Do stuff return ...
The type of the data passed between
chooseandrunis specified when filling out the Generic.When the plugin runs the
hookmethod on this implementation, it will determine what the parent class would return from this hook, get a result fromchoosefrom thefullnameand thatsuper_hookand either returnsuper_hookif thechoosereturnedFalseor returnfunctools.partial(self.run, fullname=fullname, super_hook=super_hook, extra=extra)where extra is the second item in a(True, extra)tuple returned fromchoose.
And they must be used with the hook decorator:
- class extended_mypy_django_plugin.plugin.hook.hook(hook_kls: type[Hook[T_Plugin, T_Ctx, T_Ret]])
This is a class decorator used to return a callable object that takes in a string and either returns a function that mypy can use to perform an action, or return None if this hook does not need to do anything in that instance.
It is used as a decorator for a subclass of
Hookand will cache an instance of that hook between multiple access of the descriptor