1
1
"""Mypy static type checker plugin for Pytest"""
2
2
3
- import functools
4
3
import json
5
4
import os
6
5
from tempfile import NamedTemporaryFile
6
+ from typing import Dict , List , Optional , TextIO
7
7
8
+ import attr
8
9
from filelock import FileLock # type: ignore
9
10
import mypy .api
10
11
import pytest # type: ignore
@@ -178,9 +179,9 @@ class MypyFileItem(MypyItem):
178
179
179
180
def runtest (self ):
180
181
"""Raise an exception if mypy found errors for this item."""
181
- results = _mypy_results (self .session )
182
+ results = MypyResults . from_session (self .session )
182
183
abspath = os .path .abspath (str (self .fspath ))
183
- errors = results [ ' abspath_errors' ] .get (abspath )
184
+ errors = results . abspath_errors .get (abspath )
184
185
if errors :
185
186
raise MypyError (file_error_formatter (self , results , errors ))
186
187
@@ -199,76 +200,96 @@ class MypyStatusItem(MypyItem):
199
200
200
201
def runtest (self ):
201
202
"""Raise a MypyError if mypy exited with a non-zero status."""
202
- results = _mypy_results (self .session )
203
- if results [ ' status' ] :
203
+ results = MypyResults . from_session (self .session )
204
+ if results . status :
204
205
raise MypyError (
205
206
'mypy exited with status {status}.' .format (
206
- status = results [ ' status' ] ,
207
+ status = results . status ,
207
208
),
208
209
)
209
210
210
211
211
- def _mypy_results (session ):
212
- """Get the cached mypy results for the session, or generate them."""
213
- return _cached_json_results (
214
- results_path = (
212
+ @attr .s (frozen = True , kw_only = True )
213
+ class MypyResults :
214
+
215
+ """Parsed results from Mypy."""
216
+
217
+ _abspath_errors_type = Dict [str , List [str ]]
218
+
219
+ opts = attr .ib (type = List [str ])
220
+ stdout = attr .ib (type = str )
221
+ stderr = attr .ib (type = str )
222
+ status = attr .ib (type = int )
223
+ abspath_errors = attr .ib (type = _abspath_errors_type )
224
+ unmatched_stdout = attr .ib (type = str )
225
+
226
+ def dump (self , results_f : TextIO ) -> None :
227
+ """Cache results in a format that can be parsed by load()."""
228
+ return json .dump (vars (self ), results_f )
229
+
230
+ @classmethod
231
+ def load (cls , results_f : TextIO ) -> 'MypyResults' :
232
+ """Get results cached by dump()."""
233
+ return cls (** json .load (results_f ))
234
+
235
+ @classmethod
236
+ def from_mypy (
237
+ cls ,
238
+ items : List [MypyFileItem ],
239
+ * ,
240
+ opts : Optional [List [str ]] = None
241
+ ) -> 'MypyResults' :
242
+ """Generate results from mypy."""
243
+
244
+ if opts is None :
245
+ opts = mypy_argv [:]
246
+ abspath_errors = {
247
+ os .path .abspath (str (item .fspath )): []
248
+ for item in items
249
+ } # type: MypyResults._abspath_errors_type
250
+
251
+ stdout , stderr , status = mypy .api .run (opts + list (abspath_errors ))
252
+
253
+ unmatched_lines = []
254
+ for line in stdout .split ('\n ' ):
255
+ if not line :
256
+ continue
257
+ path , _ , error = line .partition (':' )
258
+ abspath = os .path .abspath (path )
259
+ try :
260
+ abspath_errors [abspath ].append (error )
261
+ except KeyError :
262
+ unmatched_lines .append (line )
263
+
264
+ return cls (
265
+ opts = opts ,
266
+ stdout = stdout ,
267
+ stderr = stderr ,
268
+ status = status ,
269
+ abspath_errors = abspath_errors ,
270
+ unmatched_stdout = '\n ' .join (unmatched_lines ),
271
+ )
272
+
273
+ @classmethod
274
+ def from_session (cls , session ) -> 'MypyResults' :
275
+ """Load (or generate) cached mypy results for a pytest session."""
276
+ results_path = (
215
277
session .config ._mypy_results_path
216
278
if _is_master (session .config ) else
217
279
_get_xdist_workerinput (session .config )['_mypy_results_path' ]
218
- ),
219
- results_factory = functools .partial (
220
- _mypy_results_factory ,
221
- abspaths = [
222
- os .path .abspath (str (item .fspath ))
223
- for item in session .items
224
- if isinstance (item , MypyFileItem )
225
- ],
226
280
)
227
- )
228
-
229
-
230
- def _cached_json_results (results_path , results_factory = None ):
231
- """
232
- Read results from results_path if it exists;
233
- otherwise, produce them with results_factory,
234
- and write them to results_path.
235
- """
236
- with FileLock (results_path + '.lock' ):
237
- try :
238
- with open (results_path , mode = 'r' ) as results_f :
239
- results = json .load (results_f )
240
- except FileNotFoundError :
241
- if not results_factory :
242
- raise
243
- results = results_factory ()
244
- with open (results_path , mode = 'w' ) as results_f :
245
- json .dump (results , results_f )
246
- return results
247
-
248
-
249
- def _mypy_results_factory (abspaths ):
250
- """Run mypy on abspaths and return the results as a JSON-able dict."""
251
-
252
- stdout , stderr , status = mypy .api .run (mypy_argv + abspaths )
253
-
254
- abspath_errors , unmatched_lines = {}, []
255
- for line in stdout .split ('\n ' ):
256
- if not line :
257
- continue
258
- path , _ , error = line .partition (':' )
259
- abspath = os .path .abspath (path )
260
- if abspath in abspaths :
261
- abspath_errors [abspath ] = abspath_errors .get (abspath , []) + [error ]
262
- else :
263
- unmatched_lines .append (line )
264
-
265
- return {
266
- 'stdout' : stdout ,
267
- 'stderr' : stderr ,
268
- 'status' : status ,
269
- 'abspath_errors' : abspath_errors ,
270
- 'unmatched_stdout' : '\n ' .join (unmatched_lines ),
271
- }
281
+ with FileLock (results_path + '.lock' ):
282
+ try :
283
+ with open (results_path , mode = 'r' ) as results_f :
284
+ results = cls .load (results_f )
285
+ except FileNotFoundError :
286
+ results = cls .from_mypy ([
287
+ item for item in session .items
288
+ if isinstance (item , MypyFileItem )
289
+ ])
290
+ with open (results_path , mode = 'w' ) as results_f :
291
+ results .dump (results_f )
292
+ return results
272
293
273
294
274
295
class MypyError (Exception ):
@@ -282,15 +303,16 @@ def pytest_terminal_summary(terminalreporter):
282
303
"""Report stderr and unrecognized lines from stdout."""
283
304
config = _pytest_terminal_summary_config
284
305
try :
285
- results = _cached_json_results (config ._mypy_results_path )
306
+ with open (config ._mypy_results_path , mode = 'r' ) as results_f :
307
+ results = MypyResults .load (results_f )
286
308
except FileNotFoundError :
287
309
# No MypyItems executed.
288
310
return
289
- if results [ ' unmatched_stdout' ] or results [ ' stderr' ] :
311
+ if results . unmatched_stdout or results . stderr :
290
312
terminalreporter .section ('mypy' )
291
- if results [ ' unmatched_stdout' ] :
292
- color = {'red' : True } if results [ ' status' ] else {'green' : True }
293
- terminalreporter .write_line (results [ ' unmatched_stdout' ] , ** color )
294
- if results [ ' stderr' ] :
295
- terminalreporter .write_line (results [ ' stderr' ] , yellow = True )
313
+ if results . unmatched_stdout :
314
+ color = {'red' : True } if results . status else {'green' : True }
315
+ terminalreporter .write_line (results . unmatched_stdout , ** color )
316
+ if results . stderr :
317
+ terminalreporter .write_line (results . stderr , yellow = True )
296
318
os .remove (config ._mypy_results_path )
0 commit comments