I want to write a mypy plugin in order to introduce a type alias for NotRequired[Optional[T]]. (As I found out in this question, it is not possible to write this type alias in plain python, because NotRequired is not allowed outside of a TypedDict definition.)
My idea is to define a generic Possibly type, like so:
# possibly.__init__.py
from typing import Generic, TypeVar
T = TypeVar("T")
class Possibly(Generic[T]):
pass
I then want my plugin to replace any occurrence of Possibly[X] with NotRequired[Optional[X]]. I tried the following approach:
# possibly.plugin
from mypy.plugin import Plugin
class PossiblyPlugin(Plugin):
def get_type_analyze_hook(self, fullname: str):
if fullname != "possibly.Possibly":
return
return self._replace_possibly
def _replace_possibly(self, ctx):
arguments = ctx.type.args
breakpoint()
def plugin(version):
return PossiblyPlugin
At the breakpoint, I understand I have to construct an instance of a subclass of mypy.types.Type based on arguments. But I didn't find a way to construct NotRequired. There is no corresponding type in mypy.types. I figure this might be due to the fact that typing.NotRequired is not a class, but a typing._SpecialForm. (I guess this is because NotRequired does not affect the value type, but the definition of the .__optional_keys__ of the TypedDict it occurs on.)
So, then I thought about a different strategy: I could check for TypedDicts, see which fields are marked Possibly, and set the .__optional_keys__ of the TypedDict instance to make the field not required, and replace the Possibly type by mypy.types.UnionType(*arguments, None). But I didn't find which method on mypy.plugin.Plugin to use in order to get the TypedDicts into the context.
So, I am stuck. It is the first time I dig into the internals of mypy. Could you give me some direction how to achieve what I want to do?
Your first attempt (to construct an instance of subclass of
mypy.types.Type) was correct - mypy simply calls itmypy.types.RequiredType, andNotRequiredis specified as an instance state through the constructor like this:mypy.types.RequiredType(<type>, required=False).Here's an initial attempt at an implementation of
_replace_possibly:Your compliance tests in action:
Notes:
mypy's plugin system is powerful but not thoroughly undocumented. The easiest way to go through mypy's internals, for the purposes of writing a plugin, is to use an existing type construct incorrectly, look at what string or string pattern is used in the error message, then attempt to find mypy's implementation using the string/pattern. For example, the following is an incorrect usage of
typing.NotRequired:You can find this message here, which indicates that, despite
typing.NotRequirednot being a class, mypy models it as a type like any other generic, possibly because of ease of analysing the AST.Your plugin code's organisation is currently this:
When mypy loads your plugin, any runtime code in
possibly.__init__is loaded with the plugin, because mypy will importpossiblywhen it tries to load the entry pointpossibly.plugin.plugin. All runtime code, including any which may be pulled from third-party packages, will be loaded every timemypyis run. I don't think this is desirable unless you can guarantee that your package is lightweight and has no dependencies.In fact, as I'm writing this, I realised that numpy's mypy plugin (
numpy.typing.mypy_plugin) loads numpy (a big library!) because of this organisation.There are ways around this without having to separate the plugin directory from the package - you'll have to implement something in
__init__which tries to not load any runtime subpackages if it's called by mypy.