11"""Mypy static type checker plugin for Pytest"""
22
3+ from __future__ import annotations
4+
35from dataclasses import dataclass
46import json
57from pathlib import Path
68from tempfile import NamedTemporaryFile
7- from typing import Dict , List , Optional , TextIO
9+ import typing
810import warnings
911
10- from filelock import FileLock # type: ignore
12+ from filelock import FileLock
1113import mypy .api
1214import pytest
1315
16+ if typing .TYPE_CHECKING : # pragma: no cover
17+ from typing import (
18+ Any ,
19+ Dict ,
20+ Iterator ,
21+ List ,
22+ Optional ,
23+ TextIO ,
24+ Tuple ,
25+ Union ,
26+ )
27+
28+ # https://github.com/pytest-dev/pytest/issues/7469
29+ from _pytest ._code .code import TerminalRepr
30+
31+ # https://github.com/pytest-dev/pytest/pull/12661
32+ from _pytest .terminal import TerminalReporter
33+
34+ # https://github.com/pytest-dev/pytest-xdist/issues/1121
35+ from xdist .workermanage import WorkerController # type: ignore
36+
1437
1538@dataclass (frozen = True ) # compat python < 3.10 (kw_only=True)
1639class MypyConfigStash :
@@ -19,30 +42,34 @@ class MypyConfigStash:
1942 mypy_results_path : Path
2043
2144 @classmethod
22- def from_serialized (cls , serialized ) :
45+ def from_serialized (cls , serialized : str ) -> MypyConfigStash :
2346 return cls (mypy_results_path = Path (serialized ))
2447
25- def serialized (self ):
48+ def serialized (self ) -> str :
2649 return str (self .mypy_results_path )
2750
2851
29- mypy_argv = []
52+ mypy_argv : List [ str ] = []
3053nodeid_name = "mypy"
3154stash_key = {
3255 "config" : pytest .StashKey [MypyConfigStash ](),
3356}
3457terminal_summary_title = "mypy"
3558
3659
37- def default_file_error_formatter (item , results , errors ):
60+ def default_file_error_formatter (
61+ item : MypyItem ,
62+ results : MypyResults ,
63+ errors : List [str ],
64+ ) -> str :
3865 """Create a string to be displayed when mypy finds errors in a file."""
3966 return "\n " .join (errors )
4067
4168
4269file_error_formatter = default_file_error_formatter
4370
4471
45- def pytest_addoption (parser ) :
72+ def pytest_addoption (parser : pytest . Parser ) -> None :
4673 """Add options for enabling and running mypy."""
4774 group = parser .getgroup ("mypy" )
4875 group .addoption ("--mypy" , action = "store_true" , help = "run mypy on .py files" )
@@ -59,31 +86,33 @@ def pytest_addoption(parser):
5986 )
6087
6188
62- def _xdist_worker (config ) :
89+ def _xdist_worker (config : pytest . Config ) -> Dict [ str , Any ] :
6390 try :
6491 return {"input" : _xdist_workerinput (config )}
6592 except AttributeError :
6693 return {}
6794
6895
69- def _xdist_workerinput (node ) :
96+ def _xdist_workerinput (node : Union [ WorkerController , pytest . Config ]) -> Any :
7097 try :
71- return node .workerinput
98+ # mypy complains that pytest.Config does not have this attribute,
99+ # but xdist.remote defines it in worker processes.
100+ return node .workerinput # type: ignore[union-attr]
72101 except AttributeError : # compat xdist < 2.0
73- return node .slaveinput
102+ return node .slaveinput # type: ignore[union-attr]
74103
75104
76105class MypyXdistControllerPlugin :
77106 """A plugin that is only registered on xdist controller processes."""
78107
79- def pytest_configure_node (self , node ) :
108+ def pytest_configure_node (self , node : WorkerController ) -> None :
80109 """Pass the config stash to workers."""
81110 _xdist_workerinput (node )["mypy_config_stash_serialized" ] = node .config .stash [
82111 stash_key ["config" ]
83112 ].serialized ()
84113
85114
86- def pytest_configure (config ) :
115+ def pytest_configure (config : pytest . Config ) -> None :
87116 """
88117 Initialize the path used to cache mypy results,
89118 register a custom marker for MypyItems,
@@ -125,7 +154,10 @@ def pytest_configure(config):
125154 mypy_argv .append (f"--config-file={ mypy_config_file } " )
126155
127156
128- def pytest_collect_file (file_path , parent ):
157+ def pytest_collect_file (
158+ file_path : Path ,
159+ parent : pytest .Collector ,
160+ ) -> Optional [MypyFile ]:
129161 """Create a MypyFileItem for every file mypy should run on."""
130162 if file_path .suffix in {".py" , ".pyi" } and any (
131163 [
@@ -145,7 +177,7 @@ def pytest_collect_file(file_path, parent):
145177class MypyFile (pytest .File ):
146178 """A File that Mypy will run on."""
147179
148- def collect (self ):
180+ def collect (self ) -> Iterator [ MypyItem ] :
149181 """Create a MypyFileItem for the File."""
150182 yield MypyFileItem .from_parent (parent = self , name = nodeid_name )
151183 # Since mypy might check files that were not collected,
@@ -163,24 +195,28 @@ class MypyItem(pytest.Item):
163195
164196 MARKER = "mypy"
165197
166- def __init__ (self , * args , ** kwargs ):
198+ def __init__ (self , * args : Any , ** kwargs : Any ):
167199 super ().__init__ (* args , ** kwargs )
168200 self .add_marker (self .MARKER )
169201
170- def repr_failure (self , excinfo ):
202+ def repr_failure (
203+ self ,
204+ excinfo : pytest .ExceptionInfo [BaseException ],
205+ style : Optional [str ] = None ,
206+ ) -> Union [str , TerminalRepr ]:
171207 """
172208 Unwrap mypy errors so we get a clean error message without the
173209 full exception repr.
174210 """
175211 if excinfo .errisinstance (MypyError ):
176- return excinfo .value .args [0 ]
212+ return str ( excinfo .value .args [0 ])
177213 return super ().repr_failure (excinfo )
178214
179215
180216class MypyFileItem (MypyItem ):
181217 """A check for Mypy errors in a File."""
182218
183- def runtest (self ):
219+ def runtest (self ) -> None :
184220 """Raise an exception if mypy found errors for this item."""
185221 results = MypyResults .from_session (self .session )
186222 abspath = str (self .path .absolute ())
@@ -193,10 +229,10 @@ def runtest(self):
193229 raise MypyError (file_error_formatter (self , results , errors ))
194230 warnings .warn ("\n " + "\n " .join (errors ), MypyWarning )
195231
196- def reportinfo (self ):
232+ def reportinfo (self ) -> Tuple [ str , None , str ] :
197233 """Produce a heading for the test report."""
198234 return (
199- self .path ,
235+ str ( self .path ) ,
200236 None ,
201237 str (self .path .relative_to (self .config .invocation_params .dir )),
202238 )
@@ -205,7 +241,7 @@ def reportinfo(self):
205241class MypyStatusItem (MypyItem ):
206242 """A check for a non-zero mypy exit status."""
207243
208- def runtest (self ):
244+ def runtest (self ) -> None :
209245 """Raise a MypyError if mypy exited with a non-zero status."""
210246 results = MypyResults .from_session (self .session )
211247 if results .status :
@@ -216,7 +252,7 @@ def runtest(self):
216252class MypyResults :
217253 """Parsed results from Mypy."""
218254
219- _abspath_errors_type = Dict [str , List [str ]]
255+ _abspath_errors_type = typing . Dict [str , typing . List [str ]]
220256
221257 opts : List [str ]
222258 stdout : str
@@ -230,7 +266,7 @@ def dump(self, results_f: TextIO) -> None:
230266 return json .dump (vars (self ), results_f )
231267
232268 @classmethod
233- def load (cls , results_f : TextIO ) -> " MypyResults" :
269+ def load (cls , results_f : TextIO ) -> MypyResults :
234270 """Get results cached by dump()."""
235271 return cls (** json .load (results_f ))
236272
@@ -240,7 +276,7 @@ def from_mypy(
240276 paths : List [Path ],
241277 * ,
242278 opts : Optional [List [str ]] = None ,
243- ) -> " MypyResults" :
279+ ) -> MypyResults :
244280 """Generate results from mypy."""
245281
246282 if opts is None :
@@ -275,7 +311,7 @@ def from_mypy(
275311 )
276312
277313 @classmethod
278- def from_session (cls , session ) -> " MypyResults" :
314+ def from_session (cls , session : pytest . Session ) -> MypyResults :
279315 """Load (or generate) cached mypy results for a pytest session."""
280316 mypy_results_path = session .config .stash [stash_key ["config" ]].mypy_results_path
281317 with FileLock (str (mypy_results_path ) + ".lock" ):
@@ -309,7 +345,11 @@ class MypyWarning(pytest.PytestWarning):
309345class MypyReportingPlugin :
310346 """A Pytest plugin that reports mypy results."""
311347
312- def pytest_terminal_summary (self , terminalreporter , config ):
348+ def pytest_terminal_summary (
349+ self ,
350+ terminalreporter : TerminalReporter ,
351+ config : pytest .Config ,
352+ ) -> None :
313353 """Report stderr and unrecognized lines from stdout."""
314354 mypy_results_path = config .stash [stash_key ["config" ]].mypy_results_path
315355 try :
0 commit comments