File: extending_mypy.rst

package info (click to toggle)
mypy 1.15.0-5
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 20,576 kB
  • sloc: python: 105,159; cpp: 11,380; ansic: 6,629; makefile: 247; sh: 20
file content (248 lines) | stat: -rw-r--r-- 9,632 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
.. _extending-mypy:

Extending and integrating mypy
==============================

.. _integrating-mypy:

Integrating mypy into another Python application
************************************************

It is possible to integrate mypy into another Python 3 application by
importing ``mypy.api`` and calling the ``run`` function with a parameter of type ``list[str]``, containing
what normally would have been the command line arguments to mypy.

Function ``run`` returns a ``tuple[str, str, int]``, namely
``(<normal_report>, <error_report>, <exit_status>)``, in which ``<normal_report>``
is what mypy normally writes to :py:data:`sys.stdout`, ``<error_report>`` is what mypy
normally writes to :py:data:`sys.stderr` and ``exit_status`` is the exit status mypy normally
returns to the operating system.

A trivial example of using the api is the following

.. code-block:: python

    import sys
    from mypy import api

    result = api.run(sys.argv[1:])

    if result[0]:
        print('\nType checking report:\n')
        print(result[0])  # stdout

    if result[1]:
        print('\nError report:\n')
        print(result[1])  # stderr

    print('\nExit status:', result[2])


.. _extending-mypy-using-plugins:

Extending mypy using plugins
****************************

Python is a highly dynamic language and has extensive metaprogramming
capabilities. Many popular libraries use these to create APIs that may
be more flexible and/or natural for humans, but are hard to express using
static types. Extending the :pep:`484` type system to accommodate all existing
dynamic patterns is impractical and often just impossible.

Mypy supports a plugin system that lets you customize the way mypy type checks
code. This can be useful if you want to extend mypy so it can type check code
that uses a library that is difficult to express using just :pep:`484` types.

The plugin system is focused on improving mypy's understanding
of *semantics* of third party frameworks. There is currently no way to define
new first class kinds of types.

.. note::

   The plugin system is experimental and prone to change. If you want to write
   a mypy plugin, we recommend you start by contacting the mypy core developers
   on `gitter <https://gitter.im/python/typing>`_. In particular, there are
   no guarantees about backwards compatibility.

   Backwards incompatible changes may be made without a deprecation period,
   but we will announce them in
   `the plugin API changes announcement issue <https://github.com/python/mypy/issues/6617>`_.

Configuring mypy to use plugins
*******************************

Plugins are Python files that can be specified in a mypy
:ref:`config file <config-file>` using the :confval:`plugins` option and one of the two formats: relative or
absolute path to the plugin file, or a module name (if the plugin
is installed using ``pip install`` in the same virtual environment where mypy
is running). The two formats can be mixed, for example:

.. code-block:: ini

    [mypy]
    plugins = /one/plugin.py, other.plugin

Mypy will try to import the plugins and will look for an entry point function
named ``plugin``. If the plugin entry point function has a different name, it
can be specified after colon:

.. code-block:: ini

    [mypy]
    plugins = custom_plugin:custom_entry_point

In the following sections we describe the basics of the plugin system with
some examples. For more technical details, please read the docstrings in
`mypy/plugin.py <https://github.com/python/mypy/blob/master/mypy/plugin.py>`_
in mypy source code. Also you can find good examples in the bundled plugins
located in `mypy/plugins <https://github.com/python/mypy/tree/master/mypy/plugins>`_.

High-level overview
*******************

Every entry point function should accept a single string argument
that is a full mypy version and return a subclass of ``mypy.plugin.Plugin``:

.. code-block:: python

   from mypy.plugin import Plugin

   class CustomPlugin(Plugin):
       def get_type_analyze_hook(self, fullname: str):
           # see explanation below
           ...

   def plugin(version: str):
       # ignore version argument if the plugin works with all mypy versions.
       return CustomPlugin

During different phases of analyzing the code (first in semantic analysis,
and then in type checking) mypy calls plugin methods such as
``get_type_analyze_hook()`` on user plugins. This particular method, for example,
can return a callback that mypy will use to analyze unbound types with the given
full name. See the full plugin hook method list :ref:`below <plugin_hooks>`.

Mypy maintains a list of plugins it gets from the config file plus the default
(built-in) plugin that is always enabled. Mypy calls a method once for each
plugin in the list until one of the methods returns a non-``None`` value.
This callback will be then used to customize the corresponding aspect of
analyzing/checking the current abstract syntax tree node.

The callback returned by the ``get_xxx`` method will be given a detailed
current context and an API to create new nodes, new types, emit error messages,
etc., and the result will be used for further processing.

Plugin developers should ensure that their plugins work well in incremental and
daemon modes. In particular, plugins should not hold global state due to caching
of plugin hook results.

.. _plugin_hooks:

Current list of plugin hooks
****************************

**get_type_analyze_hook()** customizes behaviour of the type analyzer.
For example, :pep:`484` doesn't support defining variadic generic types:

.. code-block:: python

   from lib import Vector

   a: Vector[int, int]
   b: Vector[int, int, int]

When analyzing this code, mypy will call ``get_type_analyze_hook("lib.Vector")``,
so the plugin can return some valid type for each variable.

**get_function_hook()** is used to adjust the return type of a function call.
This hook will be also called for instantiation of classes.
This is a good choice if the return type is too complex
to be expressed by regular python typing.

**get_function_signature_hook()** is used to adjust the signature of a function.

**get_method_hook()** is the same as ``get_function_hook()`` but for methods
instead of module level functions.

**get_method_signature_hook()** is used to adjust the signature of a method.
This includes special Python methods except :py:meth:`~object.__init__` and :py:meth:`~object.__new__`.
For example in this code:

.. code-block:: python

   from ctypes import Array, c_int

   x: Array[c_int]
   x[0] = 42

mypy will call ``get_method_signature_hook("ctypes.Array.__setitem__")``
so that the plugin can mimic the :py:mod:`ctypes` auto-convert behavior.

**get_attribute_hook()** overrides instance member field lookups and property
access (not method calls). This hook is only called for
fields which already exist on the class. *Exception:* if :py:meth:`__getattr__ <object.__getattr__>` or
:py:meth:`__getattribute__ <object.__getattribute__>` is a method on the class, the hook is called for all
fields which do not refer to methods.

**get_class_attribute_hook()** is similar to above, but for attributes on classes rather than instances.
Unlike above, this does not have special casing for :py:meth:`__getattr__ <object.__getattr__>` or
:py:meth:`__getattribute__ <object.__getattribute__>`.

**get_class_decorator_hook()** can be used to update class definition for
given class decorators. For example, you can add some attributes to the class
to match runtime behaviour:

.. code-block:: python

   from dataclasses import dataclass

   @dataclass  # built-in plugin adds `__init__` method here
   class User:
       name: str

   user = User(name='example')  # mypy can understand this using a plugin

**get_metaclass_hook()** is similar to above, but for metaclasses.

**get_base_class_hook()** is similar to above, but for base classes.

**get_dynamic_class_hook()** can be used to allow dynamic class definitions
in mypy. This plugin hook is called for every assignment to a simple name
where right hand side is a function call:

.. code-block:: python

   from lib import dynamic_class

   X = dynamic_class('X', [])

For such definition, mypy will call ``get_dynamic_class_hook("lib.dynamic_class")``.
The plugin should create the corresponding ``mypy.nodes.TypeInfo`` object, and
place it into a relevant symbol table. (Instances of this class represent
classes in mypy and hold essential information such as qualified name,
method resolution order, etc.)

**get_customize_class_mro_hook()** can be used to modify class MRO (for example
insert some entries there) before the class body is analyzed.

**get_additional_deps()** can be used to add new dependencies for a
module. It is called before semantic analysis. For example, this can
be used if a library has dependencies that are dynamically loaded
based on configuration information.

**report_config_data()** can be used if the plugin has some sort of
per-module configuration that can affect typechecking. In that case,
when the configuration for a module changes, we want to invalidate
mypy's cache for that module so that it can be rechecked. This hook
should be used to report to mypy any relevant configuration data,
so that mypy knows to recheck the module if the configuration changes.
The hooks should return data encodable as JSON.

Useful tools
************

Mypy ships ``mypy.plugins.proper_plugin`` plugin which can be useful
for plugin authors, since it finds missing ``get_proper_type()`` calls,
which is a pretty common mistake.

It is recommended to enable it as a part of your plugin's CI.