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
|
.. _stubtest:
.. program:: stubtest
Automatic stub testing (stubtest)
=================================
Stub files are files containing type annotations. See
`PEP 484 <https://www.python.org/dev/peps/pep-0484/#stub-files>`_
for more motivation and details.
A common problem with stub files is that they tend to diverge from the
actual implementation. Mypy includes the ``stubtest`` tool that can
automatically check for discrepancies between the stubs and the
implementation at runtime.
What stubtest does and does not do
**********************************
Stubtest will import your code and introspect your code objects at runtime, for
example, by using the capabilities of the :py:mod:`inspect` module. Stubtest
will then analyse the stub files, and compare the two, pointing out things that
differ between stubs and the implementation at runtime.
It's important to be aware of the limitations of this comparison. Stubtest will
not make any attempt to statically analyse your actual code and relies only on
dynamic runtime introspection (in particular, this approach means stubtest works
well with extension modules). However, this means that stubtest has limited
visibility; for instance, it cannot tell if a return type of a function is
accurately typed in the stubs.
For clarity, here are some additional things stubtest can't do:
* Type check your code -- use ``mypy`` instead
* Generate stubs -- use ``stubgen`` or ``pyright --createstub`` instead
* Generate stubs based on running your application or test suite -- use ``monkeytype`` instead
* Apply stubs to code to produce inline types -- use ``retype`` or ``libcst`` instead
In summary, stubtest works very well for ensuring basic consistency between
stubs and implementation or to check for stub completeness. It's used to
test Python's official collection of library stubs,
`typeshed <https://github.com/python/typeshed>`_.
.. warning::
stubtest will import and execute Python code from the packages it checks.
Example
*******
Here's a quick example of what stubtest can do:
.. code-block:: shell
$ python3 -m pip install mypy
$ cat library.py
x = "hello, stubtest"
def foo(x=None):
print(x)
$ cat library.pyi
x: int
def foo(x: int) -> None: ...
$ python3 -m mypy.stubtest library
error: library.foo is inconsistent, runtime argument "x" has a default value but stub argument does not
Stub: at line 3
def (x: builtins.int)
Runtime: in file ~/library.py:3
def (x=None)
error: library.x variable differs from runtime type Literal['hello, stubtest']
Stub: at line 1
builtins.int
Runtime:
'hello, stubtest'
Usage
*****
Running stubtest can be as simple as ``stubtest module_to_check``.
Run :option:`stubtest --help` for a quick summary of options.
Stubtest must be able to import the code to be checked, so make sure that mypy
is installed in the same environment as the library to be tested. In some
cases, setting ``PYTHONPATH`` can help stubtest find the code to import.
Similarly, stubtest must be able to find the stubs to be checked. Stubtest
respects the ``MYPYPATH`` environment variable -- consider using this if you
receive a complaint along the lines of "failed to find stubs".
Note that stubtest requires mypy to be able to analyse stubs. If mypy is unable
to analyse stubs, you may get an error on the lines of "not checking stubs due
to mypy build errors". In this case, you will need to mitigate those errors
before stubtest will run. Despite potential overlap in errors here, stubtest is
not intended as a substitute for running mypy directly.
Allowlist
*********
If you wish to ignore some of stubtest's complaints, stubtest supports a
pretty handy :option:`--allowlist` system.
Let's say that you have this python module called ``ex``:
.. code-block:: python
try:
import optional_expensive_dep
except ImportError:
optional_expensive_dep = None
first = 1
if optional_expensive_dep:
second = 2
Let's say that you can't install ``optional_expensive_dep`` in CI for some reason,
but you still want to include ``second: int`` in the stub file:
.. code-block:: python
first: int
second: int
In this case stubtest will correctly complain:
.. code-block:: shell
error: ex.second is not present at runtime
Stub: in file /.../ex.pyi:2
builtins.int
Runtime:
MISSING
Found 1 error (checked 1 module)
To fix this, you can add an ``allowlist`` entry:
.. code-block:: ini
# Allowlist entries in `allowlist.txt` file:
# Does not exist if `optional_expensive_dep` is not installed:
ex.second
And now when running stubtest with ``--allowlist=allowlist.txt``,
no errors will be generated anymore.
Allowlists also support regular expressions,
which can be useful to ignore many similar errors at once.
They can also be useful for suppressing stubtest errors that occur sometimes,
but not on every CI run. For example, if some CI workers have
``optional_expensive_dep`` installed, stubtest might complain with this message
on those workers if you had the ``ex.second`` allowlist entry:
.. code-block:: ini
note: unused allowlist entry ex.second
Found 1 error (checked 1 module)
Changing ``ex.second`` to be ``(ex\.second)?`` will make this error optional,
meaning that stubtest will pass whether or not a CI runner
has``optional_expensive_dep`` installed.
CLI
***
The rest of this section documents the command line interface of stubtest.
.. option:: --concise
Makes stubtest's output more concise, one line per error
.. option:: --ignore-missing-stub
Ignore errors for stub missing things that are present at runtime
.. option:: --ignore-positional-only
Ignore errors for whether an argument should or shouldn't be positional-only
.. option:: --allowlist FILE
Use file as an allowlist. Can be passed multiple times to combine multiple
allowlists. Allowlists can be created with :option:`--generate-allowlist`.
Allowlists support regular expressions.
The presence of an entry in the allowlist means stubtest will not generate
any errors for the corresponding definition.
.. option:: --generate-allowlist
Print an allowlist (to stdout) to be used with :option:`--allowlist`.
When introducing stubtest to an existing project, this is an easy way to
silence all existing errors.
.. option:: --ignore-unused-allowlist
Ignore unused allowlist entries
Without this option enabled, the default is for stubtest to complain if an
allowlist entry is not necessary for stubtest to pass successfully.
Note if an allowlist entry is a regex that matches the empty string,
stubtest will never consider it unused. For example, to get
``--ignore-unused-allowlist`` behaviour for a single allowlist entry like
``foo.bar`` you could add an allowlist entry ``(foo\.bar)?``.
This can be useful when an error only occurs on a specific platform.
.. option:: --mypy-config-file FILE
Use specified mypy config *file* to determine mypy plugins and mypy path
.. option:: --custom-typeshed-dir DIR
Use the custom typeshed in *DIR*
.. option:: --check-typeshed
Check all stdlib modules in typeshed
.. option:: --help
Show a help message :-)
|