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 |   Thanks `@omarkohl`_ for the complete PR (`#1502`_) and `@nicoddemus`_ for the | ||||||
|   implementation tips. |   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** | **Changes** | ||||||
|  |  | ||||||
|  | @ -140,7 +140,8 @@ class TracebackEntry(object): | ||||||
|     _repr_style = None |     _repr_style = None | ||||||
|     exprinfo = None |     exprinfo = None | ||||||
| 
 | 
 | ||||||
|     def __init__(self, rawentry): |     def __init__(self, rawentry, excinfo=None): | ||||||
|  |         self._excinfo = excinfo | ||||||
|         self._rawentry = rawentry |         self._rawentry = rawentry | ||||||
|         self.lineno = rawentry.tb_lineno - 1 |         self.lineno = rawentry.tb_lineno - 1 | ||||||
| 
 | 
 | ||||||
|  | @ -221,16 +222,24 @@ class TracebackEntry(object): | ||||||
|         """ return True if the current frame has a var __tracebackhide__ |         """ return True if the current frame has a var __tracebackhide__ | ||||||
|             resolving to True |             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 |             mostly for internal use | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             return self.frame.f_locals['__tracebackhide__'] |             tbh = self.frame.f_locals['__tracebackhide__'] | ||||||
|         except KeyError: |         except KeyError: | ||||||
|             try: |             try: | ||||||
|                 return self.frame.f_globals['__tracebackhide__'] |                 tbh = self.frame.f_globals['__tracebackhide__'] | ||||||
|             except KeyError: |             except KeyError: | ||||||
|                 return False |                 return False | ||||||
| 
 | 
 | ||||||
|  |         if py.builtin.callable(tbh): | ||||||
|  |             return tbh(self._excinfo) | ||||||
|  |         else: | ||||||
|  |             return tbh | ||||||
|  | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         try: |         try: | ||||||
|             fn = str(self.path) |             fn = str(self.path) | ||||||
|  | @ -254,12 +263,13 @@ class Traceback(list): | ||||||
|         access to Traceback entries. |         access to Traceback entries. | ||||||
|     """ |     """ | ||||||
|     Entry = TracebackEntry |     Entry = TracebackEntry | ||||||
|     def __init__(self, tb): |     def __init__(self, tb, excinfo=None): | ||||||
|         """ initialize from given python traceback object. """ |         """ initialize from given python traceback object and ExceptionInfo """ | ||||||
|  |         self._excinfo = excinfo | ||||||
|         if hasattr(tb, 'tb_next'): |         if hasattr(tb, 'tb_next'): | ||||||
|             def f(cur): |             def f(cur): | ||||||
|                 while cur is not None: |                 while cur is not None: | ||||||
|                     yield self.Entry(cur) |                     yield self.Entry(cur, excinfo=excinfo) | ||||||
|                     cur = cur.tb_next |                     cur = cur.tb_next | ||||||
|             list.__init__(self, f(tb)) |             list.__init__(self, f(tb)) | ||||||
|         else: |         else: | ||||||
|  | @ -283,7 +293,7 @@ class Traceback(list): | ||||||
|                  not codepath.relto(excludepath)) and |                  not codepath.relto(excludepath)) and | ||||||
|                 (lineno is None or x.lineno == lineno) and |                 (lineno is None or x.lineno == lineno) and | ||||||
|                 (firstlineno is None or x.frame.code.firstlineno == firstlineno)): |                 (firstlineno is None or x.frame.code.firstlineno == firstlineno)): | ||||||
|                 return Traceback(x._rawentry) |                 return Traceback(x._rawentry, self._excinfo) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def __getitem__(self, key): |     def __getitem__(self, key): | ||||||
|  | @ -302,7 +312,7 @@ class Traceback(list): | ||||||
|             by default this removes all the TracebackItems which are hidden |             by default this removes all the TracebackItems which are hidden | ||||||
|             (see ishidden() above) |             (see ishidden() above) | ||||||
|         """ |         """ | ||||||
|         return Traceback(filter(fn, self)) |         return Traceback(filter(fn, self), self._excinfo) | ||||||
| 
 | 
 | ||||||
|     def getcrashentry(self): |     def getcrashentry(self): | ||||||
|         """ return last non-hidden traceback entry that lead |         """ return last non-hidden traceback entry that lead | ||||||
|  | @ -366,7 +376,7 @@ class ExceptionInfo(object): | ||||||
|         #: the exception type name |         #: the exception type name | ||||||
|         self.typename = self.type.__name__ |         self.typename = self.type.__name__ | ||||||
|         #: the exception traceback (_pytest._code.Traceback instance) |         #: 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): |     def __repr__(self): | ||||||
|         return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback)) |         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 |     test_checkconfig.py:8: Failed | ||||||
|     1 failed in 0.12 seconds |     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 | Detect if running from within a pytest run | ||||||
| -------------------------------------------------------------- | -------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
|  | import operator | ||||||
| import _pytest | import _pytest | ||||||
| import py | import py | ||||||
| import pytest | import pytest | ||||||
|  | @ -144,6 +145,39 @@ class TestTraceback_f_g_h: | ||||||
|         ntraceback = traceback.filter() |         ntraceback = traceback.filter() | ||||||
|         assert len(ntraceback) == len(traceback) - 1 |         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 test_traceback_recursion_index(self): | ||||||
|         def f(n): |         def f(n): | ||||||
|             if n < 10: |             if n < 10: | ||||||
|  | @ -442,7 +476,7 @@ raise ValueError() | ||||||
|             f_globals = {} |             f_globals = {} | ||||||
| 
 | 
 | ||||||
|         class FakeTracebackEntry(_pytest._code.Traceback.Entry): |         class FakeTracebackEntry(_pytest._code.Traceback.Entry): | ||||||
|             def __init__(self, tb): |             def __init__(self, tb, excinfo=None): | ||||||
|                 self.lineno = 5+3 |                 self.lineno = 5+3 | ||||||
| 
 | 
 | ||||||
|             @property |             @property | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue