diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a58e1dc99..18e0e3c64 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,8 +23,9 @@ Thanks `@omarkohl`_ for the complete PR (`#1502`_) and `@nicoddemus`_ for the implementation tips. -* ``__tracebackhide__`` can now also be set to an exception type (or a list of - exception types) to only filter exceptions of that type. +* ``__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. * diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 5fe81bc13..14c38da1c 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -140,8 +140,8 @@ class TracebackEntry(object): _repr_style = None exprinfo = None - def __init__(self, rawentry, exctype=None): - self._exctype = exctype + def __init__(self, rawentry, excinfo=None): + self._excinfo = excinfo self._rawentry = rawentry self.lineno = rawentry.tb_lineno - 1 @@ -218,16 +218,12 @@ class TracebackEntry(object): source = property(getsource) - def _is_exception_type(self, obj): - return isinstance(obj, type) and issubclass(obj, Exception) - def ishidden(self): """ return True if the current frame has a var __tracebackhide__ resolving to True - If __tracebackhide__ is set to an exception type, or a list/tuple, - the traceback is only hidden if the exception which happened is of - the given type(s). + If __tracebackhide__ is a callable, it gets called with the + ExceptionInfo instance and can decide whether to hide the traceback. mostly for internal use """ @@ -239,13 +235,8 @@ class TracebackEntry(object): except KeyError: return False - if self._is_exception_type(tbh): - assert self._exctype is not None - return issubclass(self._exctype, tbh) - elif (isinstance(tbh, (list, tuple)) and - all(self._is_exception_type(e) for e in tbh)): - assert self._exctype is not None - return issubclass(self._exctype, tuple(tbh)) + if callable(tbh): + return tbh(self._excinfo) else: return tbh @@ -272,13 +263,13 @@ class Traceback(list): access to Traceback entries. """ Entry = TracebackEntry - def __init__(self, tb, exctype=None): - """ initialize from given python traceback object and exc type. """ - self._exctype = exctype + 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, exctype=exctype) + yield self.Entry(cur, excinfo=excinfo) cur = cur.tb_next list.__init__(self, f(tb)) else: @@ -302,7 +293,7 @@ class Traceback(list): 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, self._exctype) + return Traceback(x._rawentry, self._excinfo) return self def __getitem__(self, key): @@ -321,7 +312,7 @@ class Traceback(list): by default this removes all the TracebackItems which are hidden (see ishidden() above) """ - return Traceback(filter(fn, self), self._exctype) + return Traceback(filter(fn, self), self._excinfo) def getcrashentry(self): """ return last non-hidden traceback entry that lead @@ -385,7 +376,7 @@ class ExceptionInfo(object): #: the exception type name self.typename = self.type.__name__ #: the exception traceback (_pytest._code.Traceback instance) - self.traceback = _pytest._code.Traceback(self.tb, exctype=self.type) + self.traceback = _pytest._code.Traceback(self.tb, excinfo=self) def __repr__(self): return "" % (self.typename, len(self.traceback)) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index ff06c7672..0bf1d7bfa 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -216,16 +216,18 @@ Let's run our little function:: test_checkconfig.py:8: Failed 1 failed in 0.12 seconds -If you only want to hide certain exception classes, you can also set -``__tracebackhide__`` to an exception type or a list of exception types:: +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__ = ConfigException + __tracebackhide__ = operator.methodcaller('errisinstance', ConfigException) if not hasattr(x, "config"): raise ConfigException("not configured: %s" %(x,)) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 5c47feed6..1d15a852b 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import operator import _pytest import py import pytest @@ -145,10 +146,10 @@ class TestTraceback_f_g_h: assert len(ntraceback) == len(traceback) - 1 @pytest.mark.parametrize('tracebackhide, matching', [ - (ValueError, True), - (IndexError, False), - ([ValueError, IndexError], True), - ((ValueError, IndexError), True), + (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(): @@ -475,7 +476,7 @@ raise ValueError() f_globals = {} class FakeTracebackEntry(_pytest._code.Traceback.Entry): - def __init__(self, tb, exctype=None): + def __init__(self, tb, excinfo=None): self.lineno = 5+3 @property