pytest-dev / pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing
https://pytest.org
MIT License
12.18k stars 2.7k forks source link

Retain execution context under handled exception #11512

Open jaraco opened 1 year ago

jaraco commented 1 year ago

What's the problem this feature will solve?

I've got a test similar to the following with a nested exception:

def raise_something():
  raise TypeError()

def test_something():
  try:
    raise_something()
  except TypeError:
    raise ValueError()

Running the test produces:

    def raise_something():
>     raise TypeError()
E     TypeError

test.py:2: TypeError

During handling of the above exception, another exception occurred:

    def test_something():
      try:
        raise_something()
      except TypeError:
>       raise ValueError()
E       ValueError

test.py:9: ValueError

I'd like to be able to inspect the stack where raise_something() failed. Best I can tell, that context is unreachable.

If I use --pdb, the code breaks at test.py:9 with the ValueError, but by the time pytest has handled the error, sys.exc_info() no longer points to the execution context where the failure occurred, but instead may be (None, None, None) or perhaps an unrelated context like (<class 'AttributeError'>, AttributeError("'PytestPdbWrapper' object has no attribute 'do_sys'"), <traceback object at 0x101f26c40>). Therefore, I'm unable to take advantages of techniques to trace to the inner exception stack.

Describe the solution you'd like

In the pdb context or in a global variable or in some other way, expose the original unhandled exception context.

Zac-HD commented 1 year ago

Can you inspect the .__context__ of the ValueError? That'd be the usual way of working with chained exceptions.

jaraco commented 1 year ago

Can you inspect the .__context__ of the ValueError?

Perhaps, but how do I get a handle on that exception instance? In the SO post, OP points out that sys.last_value is viable in my example:

> /Users/jaraco/draft/test_something.py(9)test_something()
-> raise ValueError()
(Pdb) import sys
(Pdb) sys.last_value
ValueError()
(Pdb) sys.last_value.__context__
TypeError()
(Pdb) import pdb, sys; pdb.post_mortem(sys.last_value.__context__.__traceback__)
> /Users/jaraco/draft/test_something.py(2)raise_something()
-> raise TypeError()
(Pdb) 

I'm 90% sure when I attempted this in a real-world case, sys.last_value wasn't viable (had been cleared), I suspect by the presence of a fixture or by the fixture itself. I don't recall now what I was working on when I stumbled into this issue, so I may struggle to replicate the situation I had encountered.

jaraco commented 1 year ago

Aha. The issue is with a handled exception:

def raise_something():
  raise TypeError()

def test_something():
  try:
    raise_something()
  except TypeError:
    try:
      raise ValueError()
    except ValueError:
      breakpoint()

In this example, after the breakpoint is reached, there's no longer a handle to the ValueError:

--Return--
> /Users/jaraco/draft/test_something.py(12)test_something()->None
-> breakpoint()
(Pdb) import sys
(Pdb) sys.last_value
*** AttributeError: module 'sys' has no attribute 'last_value'
(Pdb) sys.exc_info()
(<class 'AttributeError'>, AttributeError("'PytestPdbWrapper' object has no attribute 'do_sys'"), <traceback object at 0x103b2d7c0>)

I even tried changing the catch to except ValueError as exc:, but even then, exc appears not to be in the namespace:

--Return--
> /Users/jaraco/draft/test_something.py(12)test_something()->None
-> breakpoint()
(Pdb) exc.__context__
*** NameError: name 'exc' is not defined
(Pdb) l
  7         raise_something()
  8       except TypeError:
  9         try:
 10           raise ValueError()
 11         except ValueError as exc:
 12  ->       breakpoint()
[EOF]

Interestingly, the issue seems to be that if I'm handling the exception, I need a statement after the breakpoint in order for that block to still be active. Changing the code to the following allows the breakpoint to expose exc, from which __context__ can be used:

def raise_something():
  raise TypeError()

def test_something():
  try:
    raise_something()
  except TypeError:
    try:
      raise ValueError()
    except ValueError as exc:
      breakpoint()
      pass

Still, it would be nice if sys.exc_info() contained the exception context from when the exception occurred and not the AttributeError for PytestPdbWrapper.

jaraco commented 1 year ago

I even tried changing the catch to except ValueError as exc:, but even then, exc appears not to be in the namespace

That behavior appears to be a known issue (python/cpython/#111744).