@@ -151,16 +151,10 @@ def parse_item(item, _normcase, _pathsep):
151151 # This can result from importing a test function from another module.
152152
153153 # Figure out the file.
154- relfile = fileid
155- fspath = _normcase (str (item .fspath ))
156- if not fspath .endswith (relfile [1 :]):
157- raise should_never_reach_here (
158- item ,
159- fspath ,
160- relfile ,
161- )
162- testroot = fspath [:- len (relfile ) + 1 ]
163- location , fullname = _get_location (item , relfile , _normcase , _pathsep )
154+ testroot , relfile = _split_fspath (str (item .fspath ), fileid , item ,
155+ _normcase )
156+ location , fullname = _get_location (item , testroot , relfile ,
157+ _normcase , _pathsep )
164158 if kind == 'function' :
165159 if testfunc and fullname != testfunc + parameterized :
166160 raise should_never_reach_here (
@@ -218,43 +212,114 @@ def parse_item(item, _normcase, _pathsep):
218212 return test , parents
219213
220214
221- def _get_location (item , relfile , _normcase , _pathsep ):
215+ def _split_fspath (fspath , fileid , item , _normcase ):
216+ """Return (testroot, relfile) for the given fspath.
217+
218+ "relfile" will match "fileid".
219+ """
220+ # "fileid" comes from nodeid, is always normcased, and is always
221+ # relative to the testroot (with a "./" prefix.
222+ _relsuffix = fileid [1 :] # Drop (only) the "." prefix.
223+ if not _normcase (fspath ).endswith (_relsuffix ):
224+ raise should_never_reach_here (
225+ item ,
226+ fspath ,
227+ fileid ,
228+ )
229+ testroot = fspath [:- len (fileid ) + 1 ] # Ignore the "./" prefix.
230+ relfile = '.' + fspath [- len (fileid ) + 1 :] # Keep the pathsep.
231+ return testroot , relfile
232+
233+
234+ def _get_location (item , testroot , relfile , _normcase , _pathsep ):
222235 """Return (loc str, fullname) for the given item."""
223236 srcfile , lineno , fullname = item .location
224- srcfile = _normcase (srcfile )
225- if srcfile in (relfile , relfile [len (_pathsep ) + 1 :]):
237+ if _matches_relfile (srcfile , testroot , relfile , _normcase , _pathsep ):
226238 srcfile = relfile
227239 else :
228240 # pytest supports discovery of tests imported from other
229241 # modules. This is reflected by a different filename
230242 # in item.location.
231- srcfile , lineno = _find_location (
232- srcfile , lineno , relfile , item .function , _pathsep )
243+
244+ if _is_legacy_wrapper (srcfile , _pathsep ):
245+ srcfile = relfile
246+ unwrapped = _unwrap_decorator (item .function )
247+ if unwrapped is None :
248+ # It was an invalid legacy wrapper so we just say
249+ # "somewhere in relfile".
250+ lineno = None
251+ else :
252+ _srcfile , lineno = unwrapped
253+ if not _matches_relfile (_srcfile , testroot , relfile , _normcase , _pathsep ):
254+ # For legacy wrappers we really expect the wrapped
255+ # function to be in relfile. So here we ignore any
256+ # other file and just say "somewhere in relfile".
257+ lineno = None
258+ if lineno is None :
259+ lineno = - 1 # i.e. "unknown"
260+ elif _matches_relfile (srcfile , testroot , relfile , _normcase , _pathsep ):
261+ srcfile = relfile
262+ # Otherwise we just return the info from item.location as-is.
263+
233264 if not srcfile .startswith ('.' + _pathsep ):
234265 srcfile = '.' + _pathsep + srcfile
235266 # from pytest, line numbers are 0-based
236267 location = '{}:{}' .format (srcfile , int (lineno ) + 1 )
237268 return location , fullname
238269
239270
240- def _find_location (srcfile , lineno , relfile , func , _pathsep ):
241- """Return (filename, lno) for the given location info."""
242- if sys .version_info > (3 ,):
243- return srcfile , lineno
271+ def _matches_relfile (srcfile , testroot , relfile , _normcase , _pathsep ):
272+ """Return True if "srcfile" matches the given relfile."""
273+ srcfile = _normcase (srcfile )
274+ relfile = _normcase (relfile )
275+ if srcfile == relfile :
276+ return True
277+ elif srcfile == relfile [len (_pathsep ) + 1 :]:
278+ return True
279+ elif srcfile == testroot + relfile [1 :]:
280+ return True
281+ else :
282+ return False
283+
284+
285+ def _is_legacy_wrapper (srcfile , _pathsep , _pyversion = sys .version_info ):
286+ """Return True if the test might be wrapped.
287+
288+ In Python 2 unittest's decorators (e.g. unittest.skip) do not wrap
289+ properly, so we must manually unwrap them.
290+ """
291+ if _pyversion > (3 ,):
292+ return False
244293 if (_pathsep + 'unittest' + _pathsep + 'case.py' ) not in srcfile :
245- return srcfile , lineno
294+ return False
295+ return True
296+
246297
247- # Unwrap the decorator (e.g. unittest.skip).
248- srcfile = relfile
249- lineno = - 1
298+ def _unwrap_decorator (func ):
299+ """Return (filename, lineno) for the func the given func wraps.
300+
301+ If the wrapped func cannot be identified then return None. Likewise
302+ for the wrapped filename. "lineno" is None if it cannot be found
303+ but the filename could.
304+ """
250305 try :
251306 func = func .__closure__ [0 ].cell_contents
252307 except (IndexError , AttributeError ):
253- return srcfile , lineno
308+ return None
254309 else :
255- if callable (func ) and func .__code__ .co_filename .endswith (relfile [1 :]):
256- lineno = func .__code__ .co_firstlineno - 1
257- return srcfile , lineno
310+ if not callable (func ):
311+ return None
312+ try :
313+ filename = func .__code__ .co_filename
314+ except AttributeError :
315+ return None
316+ else :
317+ try :
318+ lineno = func .__code__ .co_firstlineno - 1
319+ except AttributeError :
320+ return (filename , None )
321+ else :
322+ return filename , lineno
258323
259324
260325def _parse_node_id (testid , kind , _pathsep , _normcase ):
0 commit comments