@@ -235,8 +235,10 @@ def repr_failure(
235235 return super ().repr_failure (excinfo )
236236
237237
238- def _error_severity (error : str ) -> str :
239- components = [component .strip () for component in error .split (":" )]
238+ def _error_severity (line : str ) -> Optional [str ]:
239+ components = [component .strip () for component in line .split (":" , 3 )]
240+ if len (components ) < 2 :
241+ return None
240242 # The second component is either the line or the severity:
241243 # demo/note.py:2: note: By default the bodies of untyped functions are not checked
242244 # demo/sub/conftest.py: error: Duplicate module named "conftest"
@@ -249,20 +251,22 @@ class MypyFileItem(MypyItem):
249251 def runtest (self ) -> None :
250252 """Raise an exception if mypy found errors for this item."""
251253 results = MypyResults .from_session (self .session )
252- abspath = str (self .path .resolve ())
253- errors = [
254- error .partition (":" )[2 ].strip ()
255- for error in results .abspath_errors .get (abspath , [])
256- ]
257- if errors and not all (_error_severity (error ) == "note" for error in errors ):
254+ lines = results .path_lines .get (self .path .resolve (), [])
255+ if lines and not all (_error_severity (line ) == "note" for line in lines ):
258256 if self .session .config .option .mypy_xfail :
259257 self .add_marker (
260258 pytest .mark .xfail (
261259 raises = MypyError ,
262260 reason = "mypy errors are expected by --mypy-xfail." ,
263261 )
264262 )
265- raise MypyError (file_error_formatter (self , results , errors ))
263+ raise MypyError (
264+ file_error_formatter (
265+ self ,
266+ results ,
267+ errors = [line .partition (":" )[2 ].strip () for line in lines ],
268+ )
269+ )
266270
267271 def reportinfo (self ) -> Tuple [str , None , str ]:
268272 """Produce a heading for the test report."""
@@ -296,24 +300,32 @@ def runtest(self) -> None:
296300class MypyResults :
297301 """Parsed results from Mypy."""
298302
299- _abspath_errors_type = typing .Dict [str , typing .List [str ]]
300303 _encoding = "utf-8"
301304
302305 opts : List [str ]
306+ args : List [str ]
303307 stdout : str
304308 stderr : str
305309 status : int
306- abspath_errors : _abspath_errors_type
307- unmatched_stdout : str
310+ path_lines : Dict [Optional [Path ], List [str ]]
308311
309312 def dump (self , results_f : IO [bytes ]) -> None :
310313 """Cache results in a format that can be parsed by load()."""
311- results_f .write (json .dumps (vars (self )).encode (self ._encoding ))
314+ prepared = vars (self ).copy ()
315+ prepared ["path_lines" ] = {
316+ str (path or "" ): lines for path , lines in prepared ["path_lines" ].items ()
317+ }
318+ results_f .write (json .dumps (prepared ).encode (self ._encoding ))
312319
313320 @classmethod
314321 def load (cls , results_f : IO [bytes ]) -> MypyResults :
315322 """Get results cached by dump()."""
316- return cls (** json .loads (results_f .read ().decode (cls ._encoding )))
323+ prepared = json .loads (results_f .read ().decode (cls ._encoding ))
324+ prepared ["path_lines" ] = {
325+ Path (path ) if path else None : lines
326+ for path , lines in prepared ["path_lines" ].items ()
327+ }
328+ return cls (** prepared )
317329
318330 @classmethod
319331 def from_mypy (
@@ -326,33 +338,31 @@ def from_mypy(
326338
327339 if opts is None :
328340 opts = mypy_argv [:]
329- abspath_errors = {
330- str (path .resolve ()): [] for path in paths
331- } # type: MypyResults._abspath_errors_type
341+ args = [str (path ) for path in paths ]
332342
333- cwd = Path .cwd ()
334- stdout , stderr , status = mypy .api .run (
335- opts + [str (Path (key ).relative_to (cwd )) for key in abspath_errors .keys ()]
336- )
343+ stdout , stderr , status = mypy .api .run (opts + args )
337344
338- unmatched_lines = []
345+ path_lines : Dict [Optional [Path ], List [str ]] = {
346+ path .resolve (): [] for path in paths
347+ }
348+ path_lines [None ] = []
339349 for line in stdout .split ("\n " ):
340350 if not line :
341351 continue
342- path , _ , error = line .partition (":" )
343- abspath = str (Path (path ).resolve ())
352+ path = Path (line .partition (":" )[0 ]).resolve ()
344353 try :
345- abspath_errors [ abspath ]. append ( line )
354+ lines = path_lines [ path ]
346355 except KeyError :
347- unmatched_lines .append (line )
356+ lines = path_lines [None ]
357+ lines .append (line )
348358
349359 return cls (
350360 opts = opts ,
361+ args = args ,
351362 stdout = stdout ,
352363 stderr = stderr ,
353364 status = status ,
354- abspath_errors = abspath_errors ,
355- unmatched_stdout = "\n " .join (unmatched_lines ),
365+ path_lines = path_lines ,
356366 )
357367
358368 @classmethod
@@ -364,9 +374,10 @@ def from_session(cls, session: pytest.Session) -> MypyResults:
364374 with open (mypy_results_path , mode = "rb" ) as results_f :
365375 results = cls .load (results_f )
366376 except FileNotFoundError :
377+ cwd = Path .cwd ()
367378 results = cls .from_mypy (
368379 [
369- item .path
380+ item .path . relative_to ( cwd )
370381 for item in session .items
371382 if isinstance (item , MypyFileItem )
372383 ],
@@ -408,14 +419,17 @@ def pytest_terminal_summary(
408419 else :
409420 for note in (
410421 unreported_note
411- for errors in results .abspath_errors .values ()
412- if all (_error_severity (error ) == "note" for error in errors )
413- for unreported_note in errors
422+ for path , lines in results .path_lines .items ()
423+ if path is not None
424+ if all (_error_severity (line ) == "note" for line in lines )
425+ for unreported_note in lines
414426 ):
415427 terminalreporter .write_line (note )
416- if results .unmatched_stdout :
428+ if results .path_lines . get ( None ) :
417429 color = {"red" : True } if results .status else {"green" : True }
418- terminalreporter .write_line (results .unmatched_stdout , ** color )
430+ terminalreporter .write_line (
431+ "\n " .join (results .path_lines [None ]), ** color
432+ )
419433 if results .stderr :
420434 terminalreporter .write_line (results .stderr , yellow = True )
421435
0 commit comments