diff --git a/CHANGELOG b/CHANGELOG index 0b5ee2fef..b1a102eda 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Changes between 1.2.1 and 1.2.0 ===================================== +- display a short and concise traceback if a funcarg lookup fails - early-load "test*/conftest.py" files, i.e. conftest.py files in directories starting with 'test'. allows to conveniently keep and access test-related options without having to put a conftest.py into the package root dir. diff --git a/py/_test/funcargs.py b/py/_test/funcargs.py index 52eea0762..24f3c1333 100644 --- a/py/_test/funcargs.py +++ b/py/_test/funcargs.py @@ -64,7 +64,7 @@ class FuncargRequest: _argprefix = "pytest_funcarg__" _argname = None - class Error(LookupError): + class LookupError(LookupError): """ error on performing funcarg request. """ def __init__(self, pyfuncitem): @@ -170,7 +170,6 @@ class FuncargRequest: if name not in available: available.append(name) fspath, lineno, msg = self._pyfuncitem.reportinfo() - line = "%s:%s" %(fspath, lineno) - msg = "funcargument %r not found for: %s" %(argname, line) + msg = "LookupError: no factory found for function argument %r" % (argname,) msg += "\n available funcargs: %s" %(", ".join(available),) - raise self.Error(msg) + raise self.LookupError(msg) diff --git a/py/_test/pycollect.py b/py/_test/pycollect.py index 34f5f6a5c..eb6702b89 100644 --- a/py/_test/pycollect.py +++ b/py/_test/pycollect.py @@ -5,6 +5,7 @@ import py import inspect from py._test.collect import configproperty, warnoldcollect from py._test import funcargs +from py._code.code import TerminalRepr class PyobjMixin(object): def obj(): @@ -252,12 +253,38 @@ class FunctionMixin(PyobjMixin): traceback = ntraceback.filter() return traceback + def _repr_failure_py(self, excinfo): + if excinfo.errisinstance(funcargs.FuncargRequest.LookupError): + fspath, lineno, msg = self.reportinfo() + lines, _ = inspect.getsourcelines(self.obj) + for i, line in enumerate(lines): + if line.strip().startswith('def'): + return FuncargLookupErrorRepr(fspath, lineno, + lines[:i+1], str(excinfo.value)) + return super(FunctionMixin, self)._repr_failure_py(excinfo) + def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" return self._repr_failure_py(excinfo) shortfailurerepr = "F" +class FuncargLookupErrorRepr(TerminalRepr): + def __init__(self, filename, firstlineno, deflines, errorstring): + self.deflines = deflines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + + def toterminal(self, tw): + tw.line() + for line in self.deflines: + tw.line(" " + line.strip()) + for line in self.errorstring.split("\n"): + tw.line(" " + line.strip(), red=True) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + class Generator(FunctionMixin, PyCollectorMixin, py.test.collect.Collector): def collect(self): # test generators are seen as collectors but they also diff --git a/testing/test_funcargs.py b/testing/test_funcargs.py index 6a621cc2e..f059a3a06 100644 --- a/testing/test_funcargs.py +++ b/testing/test_funcargs.py @@ -30,7 +30,8 @@ class TestFillFuncArgs: return 42 """) item = testdir.getitem("def test_func(some): pass") - exc = py.test.raises(LookupError, "funcargs.fillfuncargs(item)") + exc = py.test.raises(funcargs.FuncargRequest.LookupError, + "funcargs.fillfuncargs(item)") s = str(exc.value) assert s.find("xyzsomething") != -1 @@ -560,3 +561,18 @@ def test_funcarg_non_pycollectobj(testdir): # rough jstests usage funcargs.fillfuncargs(clscol) assert clscol.funcargs['arg1'] == 42 + +def test_funcarg_lookup_error(testdir): + p = testdir.makepyfile(""" + def test_lookup_error(unknown): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ERROR at setup of test_lookup_error*", + "*def test_lookup_error(unknown):*", + "*LookupError: no factory found*unknown*", + "*available funcargs*", + "*1 error*", + ]) + assert "INTERNAL" not in result.stdout.str()