From b607f6728f2eed51c9a7ce713d3b601dcd04f8db Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 Apr 2016 10:25:33 +0200 Subject: [PATCH 1/4] Filter selectively with __tracebackhide__ When __tracebackhide__ gets set to an exception type or list/tuple of exception types, only those exceptions get filtered, while the full traceback is shown if another exception (e.g. a bug in a assertion helper) happens. --- CHANGELOG.rst | 3 +++ _pytest/_code/code.py | 37 +++++++++++++++++++++++++++--------- doc/en/example/simple.rst | 20 +++++++++++++++++++ testing/code/test_excinfo.py | 35 +++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dfd5c2236ab..a58e1dc992c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +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. + * **Changes** diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 79fcf9f1c9d..5fe81bc13c0 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -140,7 +140,8 @@ class TracebackEntry(object): _repr_style = None exprinfo = None - def __init__(self, rawentry): + def __init__(self, rawentry, exctype=None): + self._exctype = exctype self._rawentry = rawentry self.lineno = rawentry.tb_lineno - 1 @@ -217,20 +218,37 @@ def getsource(self, astcache=None): 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). + 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 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)) + else: + return tbh + def __str__(self): try: fn = str(self.path) @@ -254,12 +272,13 @@ class Traceback(list): access to Traceback entries. """ Entry = TracebackEntry - def __init__(self, tb): - """ initialize from given python traceback object. """ + def __init__(self, tb, exctype=None): + """ initialize from given python traceback object and exc type. """ + self._exctype = exctype if hasattr(tb, 'tb_next'): def f(cur): while cur is not None: - yield self.Entry(cur) + yield self.Entry(cur, exctype=exctype) cur = cur.tb_next list.__init__(self, f(tb)) else: @@ -283,7 +302,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._exctype) return self def __getitem__(self, key): @@ -302,7 +321,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._exctype) def getcrashentry(self): """ return last non-hidden traceback entry that lead @@ -366,7 +385,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, exctype=self.type) def __repr__(self): return "" % (self.typename, len(self.traceback)) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index be12d2afe41..ff06c76727d 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -216,6 +216,26 @@ 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:: + + import pytest + + class ConfigException(Exception): + pass + + def checkconfig(x): + __tracebackhide__ = 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 -------------------------------------------------------------- diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 0280d1aa386..8cf79166b65 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -144,6 +144,39 @@ def test_traceback_filter(self): ntraceback = traceback.filter() assert len(ntraceback) == len(traceback) - 1 + @pytest.mark.parametrize('tracebackhide, matching', [ + (ValueError, True), + (IndexError, False), + ([ValueError, IndexError], True), + ((ValueError, IndexError), True), + ]) + 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: {!r}'.format(traceback)) + print('new: {!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: @@ -442,7 +475,7 @@ class FakeFrame(object): f_globals = {} class FakeTracebackEntry(_pytest._code.Traceback.Entry): - def __init__(self, tb): + def __init__(self, tb, exctype=None): self.lineno = 5+3 @property From 4c552d4ef78a5dbc27730069206816f35c34a9ac Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 Apr 2016 10:36:13 +0200 Subject: [PATCH 2/4] Fix tests for python 2.6 --- testing/code/test_excinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 8cf79166b65..5c47feed698 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -168,8 +168,8 @@ def h(): excinfo = pytest.raises(ValueError, h) traceback = excinfo.traceback ntraceback = traceback.filter() - print('old: {!r}'.format(traceback)) - print('new: {!r}'.format(ntraceback)) + print('old: {0!r}'.format(traceback)) + print('new: {0!r}'.format(ntraceback)) if matching: assert len(ntraceback) == len(traceback) - 2 From 75160547f292266f46f598e362064c520ff945e2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 Apr 2016 11:07:34 +0200 Subject: [PATCH 3/4] Use a callable __tracebackhide__ for filtering While this leads to slightly more complicated user code for the common case (checking if the exception is of a given type) it's easier to implement and more flexible. --- CHANGELOG.rst | 5 +++-- _pytest/_code/code.py | 35 +++++++++++++---------------------- doc/en/example/simple.rst | 8 +++++--- testing/code/test_excinfo.py | 11 ++++++----- 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a58e1dc992c..18e0e3c6457 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 5fe81bc13c0..14c38da1cc9 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 @@ def getsource(self, astcache=None): 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 @@ def ishidden(self): 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 @@ 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, self._exctype) + return Traceback(x._rawentry, self._excinfo) return self def __getitem__(self, key): @@ -321,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), self._exctype) + return Traceback(filter(fn, self), self._excinfo) def getcrashentry(self): """ return last non-hidden traceback entry that lead @@ -385,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, 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 ff06c76727d..0bf1d7bfa35 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 5c47feed698..1d15a852b00 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 @@ def test_traceback_filter(self): 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 @@ class FakeFrame(object): 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 From aa87395c397921d3ebb465275b2f9e4bc8496a29 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 20 Apr 2016 11:18:47 +0200 Subject: [PATCH 4/4] Use py.builtin.callable This restores compatibility with Python 3.0/3.1 --- _pytest/_code/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 14c38da1cc9..feb69f9e65c 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -235,7 +235,7 @@ def ishidden(self): except KeyError: return False - if callable(tbh): + if py.builtin.callable(tbh): return tbh(self._excinfo) else: return tbh