Correctly handle tracebackhide for chained exceptions (#10772)
This commit is contained in:
		
							parent
							
								
									eada68b2b3
								
							
						
					
					
						commit
						431ec6d34e
					
				
							
								
								
									
										1
									
								
								AUTHORS
								
								
								
								
							
							
						
						
									
										1
									
								
								AUTHORS
								
								
								
								
							|  | @ -128,6 +128,7 @@ Erik M. Bray | ||||||
| Evan Kepner | Evan Kepner | ||||||
| Fabien Zarifian | Fabien Zarifian | ||||||
| Fabio Zadrozny | Fabio Zadrozny | ||||||
|  | Felix Hofstätter | ||||||
| Felix Nieuwenhuizen | Felix Nieuwenhuizen | ||||||
| Feng Ma | Feng Ma | ||||||
| Florian Bruhin | Florian Bruhin | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Correctly handle ``__tracebackhide__`` for chained exceptions. | ||||||
|  | @ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]): | ||||||
|         """ |         """ | ||||||
|         return Traceback(filter(fn, self), self._excinfo) |         return Traceback(filter(fn, self), self._excinfo) | ||||||
| 
 | 
 | ||||||
|     def getcrashentry(self) -> TracebackEntry: |     def getcrashentry(self) -> Optional[TracebackEntry]: | ||||||
|         """Return last non-hidden traceback entry that lead to the exception of a traceback.""" |         """Return last non-hidden traceback entry that lead to the exception of a traceback.""" | ||||||
|         for i in range(-1, -len(self) - 1, -1): |         for i in range(-1, -len(self) - 1, -1): | ||||||
|             entry = self[i] |             entry = self[i] | ||||||
|             if not entry.ishidden(): |             if not entry.ishidden(): | ||||||
|                 return entry |                 return entry | ||||||
|         return self[-1] |         return None | ||||||
| 
 | 
 | ||||||
|     def recursionindex(self) -> Optional[int]: |     def recursionindex(self) -> Optional[int]: | ||||||
|         """Return the index of the frame/TracebackEntry where recursion originates if |         """Return the index of the frame/TracebackEntry where recursion originates if | ||||||
|  | @ -602,11 +602,13 @@ class ExceptionInfo(Generic[E]): | ||||||
|         """ |         """ | ||||||
|         return isinstance(self.value, exc) |         return isinstance(self.value, exc) | ||||||
| 
 | 
 | ||||||
|     def _getreprcrash(self) -> "ReprFileLocation": |     def _getreprcrash(self) -> Optional["ReprFileLocation"]: | ||||||
|         exconly = self.exconly(tryshort=True) |         exconly = self.exconly(tryshort=True) | ||||||
|         entry = self.traceback.getcrashentry() |         entry = self.traceback.getcrashentry() | ||||||
|  |         if entry: | ||||||
|             path, lineno = entry.frame.code.raw.co_filename, entry.lineno |             path, lineno = entry.frame.code.raw.co_filename, entry.lineno | ||||||
|             return ReprFileLocation(path, lineno + 1, exconly) |             return ReprFileLocation(path, lineno + 1, exconly) | ||||||
|  |         return None | ||||||
| 
 | 
 | ||||||
|     def getrepr( |     def getrepr( | ||||||
|         self, |         self, | ||||||
|  | @ -942,9 +944,14 @@ class FormattedExcinfo: | ||||||
|                     ) |                     ) | ||||||
|                 else: |                 else: | ||||||
|                     reprtraceback = self.repr_traceback(excinfo_) |                     reprtraceback = self.repr_traceback(excinfo_) | ||||||
|                 reprcrash: Optional[ReprFileLocation] = ( | 
 | ||||||
|                     excinfo_._getreprcrash() if self.style != "value" else None |                 # will be None if all traceback entries are hidden | ||||||
|                 ) |                 reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash() | ||||||
|  |                 if reprcrash: | ||||||
|  |                     if self.style == "value": | ||||||
|  |                         repr_chain += [(reprtraceback, None, descr)] | ||||||
|  |                     else: | ||||||
|  |                         repr_chain += [(reprtraceback, reprcrash, descr)] | ||||||
|             else: |             else: | ||||||
|                 # Fallback to native repr if the exception doesn't have a traceback: |                 # Fallback to native repr if the exception doesn't have a traceback: | ||||||
|                 # ExceptionInfo objects require a full traceback to work. |                 # ExceptionInfo objects require a full traceback to work. | ||||||
|  | @ -952,8 +959,8 @@ class FormattedExcinfo: | ||||||
|                     traceback.format_exception(type(e), e, None) |                     traceback.format_exception(type(e), e, None) | ||||||
|                 ) |                 ) | ||||||
|                 reprcrash = None |                 reprcrash = None | ||||||
| 
 |  | ||||||
|                 repr_chain += [(reprtraceback, reprcrash, descr)] |                 repr_chain += [(reprtraceback, reprcrash, descr)] | ||||||
|  | 
 | ||||||
|             if e.__cause__ is not None and self.chain: |             if e.__cause__ is not None and self.chain: | ||||||
|                 e = e.__cause__ |                 e = e.__cause__ | ||||||
|                 excinfo_ = ( |                 excinfo_ = ( | ||||||
|  | @ -1044,7 +1051,7 @@ class ExceptionChainRepr(ExceptionRepr): | ||||||
| @dataclasses.dataclass(eq=False) | @dataclasses.dataclass(eq=False) | ||||||
| class ReprExceptionInfo(ExceptionRepr): | class ReprExceptionInfo(ExceptionRepr): | ||||||
|     reprtraceback: "ReprTraceback" |     reprtraceback: "ReprTraceback" | ||||||
|     reprcrash: "ReprFileLocation" |     reprcrash: Optional["ReprFileLocation"] | ||||||
| 
 | 
 | ||||||
|     def toterminal(self, tw: TerminalWriter) -> None: |     def toterminal(self, tw: TerminalWriter) -> None: | ||||||
|         self.reprtraceback.toterminal(tw) |         self.reprtraceback.toterminal(tw) | ||||||
|  |  | ||||||
|  | @ -347,6 +347,10 @@ class TestReport(BaseReport): | ||||||
|             elif isinstance(excinfo.value, skip.Exception): |             elif isinstance(excinfo.value, skip.Exception): | ||||||
|                 outcome = "skipped" |                 outcome = "skipped" | ||||||
|                 r = excinfo._getreprcrash() |                 r = excinfo._getreprcrash() | ||||||
|  |                 if r is None: | ||||||
|  |                     raise ValueError( | ||||||
|  |                         "There should always be a traceback entry for skipping a test." | ||||||
|  |                     ) | ||||||
|                 if excinfo.value._use_item_location: |                 if excinfo.value._use_item_location: | ||||||
|                     path, line = item.reportinfo()[:2] |                     path, line = item.reportinfo()[:2] | ||||||
|                     assert line is not None |                     assert line is not None | ||||||
|  |  | ||||||
|  | @ -294,6 +294,7 @@ class TestTraceback_f_g_h: | ||||||
|         excinfo = pytest.raises(ValueError, f) |         excinfo = pytest.raises(ValueError, f) | ||||||
|         tb = excinfo.traceback |         tb = excinfo.traceback | ||||||
|         entry = tb.getcrashentry() |         entry = tb.getcrashentry() | ||||||
|  |         assert entry is not None | ||||||
|         co = _pytest._code.Code.from_function(h) |         co = _pytest._code.Code.from_function(h) | ||||||
|         assert entry.frame.code.path == co.path |         assert entry.frame.code.path == co.path | ||||||
|         assert entry.lineno == co.firstlineno + 1 |         assert entry.lineno == co.firstlineno + 1 | ||||||
|  | @ -311,10 +312,7 @@ class TestTraceback_f_g_h: | ||||||
|         excinfo = pytest.raises(ValueError, f) |         excinfo = pytest.raises(ValueError, f) | ||||||
|         tb = excinfo.traceback |         tb = excinfo.traceback | ||||||
|         entry = tb.getcrashentry() |         entry = tb.getcrashentry() | ||||||
|         co = _pytest._code.Code.from_function(g) |         assert entry is None | ||||||
|         assert entry.frame.code.path == co.path |  | ||||||
|         assert entry.lineno == co.firstlineno + 2 |  | ||||||
|         assert entry.frame.code.name == "g" |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_excinfo_exconly(): | def test_excinfo_exconly(): | ||||||
|  |  | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | def test_tbh_chained(testdir): | ||||||
|  |     """Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904).""" | ||||||
|  |     p = testdir.makepyfile( | ||||||
|  |         """ | ||||||
|  |         import pytest | ||||||
|  | 
 | ||||||
|  |         def f1(): | ||||||
|  |             __tracebackhide__ = True | ||||||
|  |             try: | ||||||
|  |                 return f1.meh | ||||||
|  |             except AttributeError: | ||||||
|  |                 pytest.fail("fail") | ||||||
|  | 
 | ||||||
|  |         @pytest.fixture | ||||||
|  |         def fix(): | ||||||
|  |             f1() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         def test(fix): | ||||||
|  |             pass | ||||||
|  |         """ | ||||||
|  |     ) | ||||||
|  |     result = testdir.runpytest(str(p)) | ||||||
|  |     assert "'function' object has no attribute 'meh'" not in result.stdout.str() | ||||||
|  |     assert result.ret == 1 | ||||||
		Loading…
	
		Reference in New Issue