Skip to content

Commit b220c96

Browse files
Merge pull request #1526 from The-Compiler/tracebackhide
Filter selectively with __tracebackhide__
2 parents 0f7aeaf + aa87395 commit b220c96

File tree

4 files changed

+80
-10
lines changed

4 files changed

+80
-10
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
Thanks `@omarkohl`_ for the complete PR (`#1502`_) and `@nicoddemus`_ for the
2424
implementation tips.
2525

26+
* ``__tracebackhide__`` can now also be set to a callable which then can decide
27+
whether to filter the traceback based on the ``ExceptionInfo`` object passed
28+
to it.
29+
2630
*
2731

2832
**Changes**

_pytest/_code/code.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ class TracebackEntry(object):
140140
_repr_style = None
141141
exprinfo = None
142142

143-
def __init__(self, rawentry):
143+
def __init__(self, rawentry, excinfo=None):
144+
self._excinfo = excinfo
144145
self._rawentry = rawentry
145146
self.lineno = rawentry.tb_lineno - 1
146147

@@ -221,16 +222,24 @@ def ishidden(self):
221222
""" return True if the current frame has a var __tracebackhide__
222223
resolving to True
223224
225+
If __tracebackhide__ is a callable, it gets called with the
226+
ExceptionInfo instance and can decide whether to hide the traceback.
227+
224228
mostly for internal use
225229
"""
226230
try:
227-
return self.frame.f_locals['__tracebackhide__']
231+
tbh = self.frame.f_locals['__tracebackhide__']
228232
except KeyError:
229233
try:
230-
return self.frame.f_globals['__tracebackhide__']
234+
tbh = self.frame.f_globals['__tracebackhide__']
231235
except KeyError:
232236
return False
233237

238+
if py.builtin.callable(tbh):
239+
return tbh(self._excinfo)
240+
else:
241+
return tbh
242+
234243
def __str__(self):
235244
try:
236245
fn = str(self.path)
@@ -254,12 +263,13 @@ class Traceback(list):
254263
access to Traceback entries.
255264
"""
256265
Entry = TracebackEntry
257-
def __init__(self, tb):
258-
""" initialize from given python traceback object. """
266+
def __init__(self, tb, excinfo=None):
267+
""" initialize from given python traceback object and ExceptionInfo """
268+
self._excinfo = excinfo
259269
if hasattr(tb, 'tb_next'):
260270
def f(cur):
261271
while cur is not None:
262-
yield self.Entry(cur)
272+
yield self.Entry(cur, excinfo=excinfo)
263273
cur = cur.tb_next
264274
list.__init__(self, f(tb))
265275
else:
@@ -283,7 +293,7 @@ def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None):
283293
not codepath.relto(excludepath)) and
284294
(lineno is None or x.lineno == lineno) and
285295
(firstlineno is None or x.frame.code.firstlineno == firstlineno)):
286-
return Traceback(x._rawentry)
296+
return Traceback(x._rawentry, self._excinfo)
287297
return self
288298

289299
def __getitem__(self, key):
@@ -302,7 +312,7 @@ def filter(self, fn=lambda x: not x.ishidden()):
302312
by default this removes all the TracebackItems which are hidden
303313
(see ishidden() above)
304314
"""
305-
return Traceback(filter(fn, self))
315+
return Traceback(filter(fn, self), self._excinfo)
306316

307317
def getcrashentry(self):
308318
""" return last non-hidden traceback entry that lead
@@ -366,7 +376,7 @@ def __init__(self, tup=None, exprinfo=None):
366376
#: the exception type name
367377
self.typename = self.type.__name__
368378
#: the exception traceback (_pytest._code.Traceback instance)
369-
self.traceback = _pytest._code.Traceback(self.tb)
379+
self.traceback = _pytest._code.Traceback(self.tb, excinfo=self)
370380

371381
def __repr__(self):
372382
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))

doc/en/example/simple.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,28 @@ Let's run our little function::
216216
test_checkconfig.py:8: Failed
217217
1 failed in 0.12 seconds
218218

219+
If you only want to hide certain exceptions, you can set ``__tracebackhide__``
220+
to a callable which gets the ``ExceptionInfo`` object. You can for example use
221+
this to make sure unexpected exception types aren't hidden::
222+
223+
import operator
224+
import pytest
225+
226+
class ConfigException(Exception):
227+
pass
228+
229+
def checkconfig(x):
230+
__tracebackhide__ = operator.methodcaller('errisinstance', ConfigException)
231+
if not hasattr(x, "config"):
232+
raise ConfigException("not configured: %s" %(x,))
233+
234+
def test_something():
235+
checkconfig(42)
236+
237+
This will avoid hiding the exception traceback on unrelated exceptions (i.e.
238+
bugs in assertion helpers).
239+
240+
219241
Detect if running from within a pytest run
220242
--------------------------------------------------------------
221243

testing/code/test_excinfo.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3+
import operator
34
import _pytest
45
import py
56
import pytest
@@ -144,6 +145,39 @@ def test_traceback_filter(self):
144145
ntraceback = traceback.filter()
145146
assert len(ntraceback) == len(traceback) - 1
146147

148+
@pytest.mark.parametrize('tracebackhide, matching', [
149+
(lambda info: True, True),
150+
(lambda info: False, False),
151+
(operator.methodcaller('errisinstance', ValueError), True),
152+
(operator.methodcaller('errisinstance', IndexError), False),
153+
])
154+
def test_traceback_filter_selective(self, tracebackhide, matching):
155+
def f():
156+
#
157+
raise ValueError
158+
#
159+
def g():
160+
#
161+
__tracebackhide__ = tracebackhide
162+
f()
163+
#
164+
def h():
165+
#
166+
g()
167+
#
168+
169+
excinfo = pytest.raises(ValueError, h)
170+
traceback = excinfo.traceback
171+
ntraceback = traceback.filter()
172+
print('old: {0!r}'.format(traceback))
173+
print('new: {0!r}'.format(ntraceback))
174+
175+
if matching:
176+
assert len(ntraceback) == len(traceback) - 2
177+
else:
178+
# -1 because of the __tracebackhide__ in pytest.raises
179+
assert len(ntraceback) == len(traceback) - 1
180+
147181
def test_traceback_recursion_index(self):
148182
def f(n):
149183
if n < 10:
@@ -442,7 +476,7 @@ class FakeFrame(object):
442476
f_globals = {}
443477

444478
class FakeTracebackEntry(_pytest._code.Traceback.Entry):
445-
def __init__(self, tb):
479+
def __init__(self, tb, excinfo=None):
446480
self.lineno = 5+3
447481

448482
@property

0 commit comments

Comments
 (0)