Skip to content

Commit

Permalink
Merge pull request #1526 from The-Compiler/tracebackhide
Browse files Browse the repository at this point in the history
Filter selectively with __tracebackhide__
  • Loading branch information
RonnyPfannschmidt committed Apr 20, 2016
2 parents 0f7aeaf + aa87395 commit b220c96
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
Thanks `@omarkohl`_ for the complete PR (`#1502`_) and `@nicoddemus`_ for the
implementation tips.

* ``__tracebackhide__`` can now also be set to a callable which then can decide
whether to filter the traceback based on the ``ExceptionInfo`` object passed
to it.

*

**Changes**
Expand Down
28 changes: 19 additions & 9 deletions _pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ class TracebackEntry(object):
_repr_style = None
exprinfo = None

def __init__(self, rawentry):
def __init__(self, rawentry, excinfo=None):
self._excinfo = excinfo
self._rawentry = rawentry
self.lineno = rawentry.tb_lineno - 1

Expand Down Expand Up @@ -221,16 +222,24 @@ def ishidden(self):
""" return True if the current frame has a var __tracebackhide__
resolving to True
If __tracebackhide__ is a callable, it gets called with the
ExceptionInfo instance and can decide whether to hide the traceback.
mostly for internal use
"""
try:
return self.frame.f_locals['__tracebackhide__']
tbh = self.frame.f_locals['__tracebackhide__']
except KeyError:
try:
return self.frame.f_globals['__tracebackhide__']
tbh = self.frame.f_globals['__tracebackhide__']
except KeyError:
return False

if py.builtin.callable(tbh):
return tbh(self._excinfo)
else:
return tbh

def __str__(self):
try:
fn = str(self.path)
Expand All @@ -254,12 +263,13 @@ class Traceback(list):
access to Traceback entries.
"""
Entry = TracebackEntry
def __init__(self, tb):
""" initialize from given python traceback object. """
def __init__(self, tb, excinfo=None):
""" initialize from given python traceback object and ExceptionInfo """
self._excinfo = excinfo
if hasattr(tb, 'tb_next'):
def f(cur):
while cur is not None:
yield self.Entry(cur)
yield self.Entry(cur, excinfo=excinfo)
cur = cur.tb_next
list.__init__(self, f(tb))
else:
Expand All @@ -283,7 +293,7 @@ def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None):
not codepath.relto(excludepath)) and
(lineno is None or x.lineno == lineno) and
(firstlineno is None or x.frame.code.firstlineno == firstlineno)):
return Traceback(x._rawentry)
return Traceback(x._rawentry, self._excinfo)
return self

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

def getcrashentry(self):
""" return last non-hidden traceback entry that lead
Expand Down Expand Up @@ -366,7 +376,7 @@ def __init__(self, tup=None, exprinfo=None):
#: the exception type name
self.typename = self.type.__name__
#: the exception traceback (_pytest._code.Traceback instance)
self.traceback = _pytest._code.Traceback(self.tb)
self.traceback = _pytest._code.Traceback(self.tb, excinfo=self)

def __repr__(self):
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
Expand Down
22 changes: 22 additions & 0 deletions doc/en/example/simple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,28 @@ Let's run our little function::
test_checkconfig.py:8: Failed
1 failed in 0.12 seconds

If you only want to hide certain exceptions, you can set ``__tracebackhide__``
to a callable which gets the ``ExceptionInfo`` object. You can for example use
this to make sure unexpected exception types aren't hidden::

import operator
import pytest

class ConfigException(Exception):
pass

def checkconfig(x):
__tracebackhide__ = operator.methodcaller('errisinstance', ConfigException)
if not hasattr(x, "config"):
raise ConfigException("not configured: %s" %(x,))

def test_something():
checkconfig(42)

This will avoid hiding the exception traceback on unrelated exceptions (i.e.
bugs in assertion helpers).


Detect if running from within a pytest run
--------------------------------------------------------------

Expand Down
36 changes: 35 additions & 1 deletion testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-

import operator
import _pytest
import py
import pytest
Expand Down Expand Up @@ -144,6 +145,39 @@ def test_traceback_filter(self):
ntraceback = traceback.filter()
assert len(ntraceback) == len(traceback) - 1

@pytest.mark.parametrize('tracebackhide, matching', [
(lambda info: True, True),
(lambda info: False, False),
(operator.methodcaller('errisinstance', ValueError), True),
(operator.methodcaller('errisinstance', IndexError), False),
])
def test_traceback_filter_selective(self, tracebackhide, matching):
def f():
#
raise ValueError
#
def g():
#
__tracebackhide__ = tracebackhide
f()
#
def h():
#
g()
#

excinfo = pytest.raises(ValueError, h)
traceback = excinfo.traceback
ntraceback = traceback.filter()
print('old: {0!r}'.format(traceback))
print('new: {0!r}'.format(ntraceback))

if matching:
assert len(ntraceback) == len(traceback) - 2
else:
# -1 because of the __tracebackhide__ in pytest.raises
assert len(ntraceback) == len(traceback) - 1

def test_traceback_recursion_index(self):
def f(n):
if n < 10:
Expand Down Expand Up @@ -442,7 +476,7 @@ class FakeFrame(object):
f_globals = {}

class FakeTracebackEntry(_pytest._code.Traceback.Entry):
def __init__(self, tb):
def __init__(self, tb, excinfo=None):
self.lineno = 5+3

@property
Expand Down

0 comments on commit b220c96

Please sign in to comment.