Merge pull request #1526 from The-Compiler/tracebackhide
Filter selectively with __tracebackhide__
This commit is contained in:
		
						commit
						b220c96bf8
					
				|  | @ -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** | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  | @ -221,16 +222,24 @@ class TracebackEntry(object): | |||
|         """ 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) | ||||
|  | @ -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: | ||||
|  | @ -283,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) | ||||
|                 return Traceback(x._rawentry, self._excinfo) | ||||
|         return self | ||||
| 
 | ||||
|     def __getitem__(self, key): | ||||
|  | @ -302,7 +312,7 @@ class Traceback(list): | |||
|             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 | ||||
|  | @ -366,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) | ||||
|         self.traceback = _pytest._code.Traceback(self.tb, excinfo=self) | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback)) | ||||
|  |  | |||
|  | @ -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 | ||||
| -------------------------------------------------------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| 
 | ||||
| import operator | ||||
| import _pytest | ||||
| import py | ||||
| import pytest | ||||
|  | @ -144,6 +145,39 @@ class TestTraceback_f_g_h: | |||
|         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: | ||||
|  | @ -442,7 +476,7 @@ raise ValueError() | |||
|             f_globals = {} | ||||
| 
 | ||||
|         class FakeTracebackEntry(_pytest._code.Traceback.Entry): | ||||
|             def __init__(self, tb): | ||||
|             def __init__(self, tb, excinfo=None): | ||||
|                 self.lineno = 5+3 | ||||
| 
 | ||||
|             @property | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue