Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pdb go to a frame in exception within exception

I am debugging a program named a.py using pdb

def f(x) :
    x / x

def g(x) :
    try :
        f(x)
    except Exception as e :
        assert 0

g(0)

When I run the program using python3 -m pdb a.py, the program stops at assert 0 line, and I get the following error information:

Traceback (most recent call last):
  File "/tmp/a.py", line 6, in g
    f(x)
  File "/tmp/a.py", line 2, in f
    x / x
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib64/python3.6/pdb.py", line 1667, in main
    pdb._runscript(mainpyfile)
  File "/usr/lib64/python3.6/pdb.py", line 1548, in _runscript
    self.run(statement)
  File "/usr/lib64/python3.6/bdb.py", line 434, in run
    exec(cmd, globals, locals)
  File "<string>", line 1, in <module>
  File "/tmp/a.py", line 11, in <module>
    g(0)
  File "/tmp/a.py", line 9, in g
    assert 0
AssertionError

and the stack is (shown using bt command):

(Pdb) bt
  /usr/lib64/python3.6/pdb.py(1667)main()
-> pdb._runscript(mainpyfile)
  /usr/lib64/python3.6/pdb.py(1548)_runscript()
-> self.run(statement)
  /usr/lib64/python3.6/bdb.py(434)run()
-> exec(cmd, globals, locals)
  <string>(1)<module>()->None
  /tmp/a.py(11)<module>()->None
-> g(0)
> /tmp/a.py(9)g()
-> assert 0
(Pdb) 

The problem is, I cannot go to function f to debug x / x simply using up and down, because my stack ends at the g function.

How should I debug such exceptions within exceptions? What about exceptions within exceptions within exceptions ...?

like image 881
Eric Stdlib Avatar asked Aug 29 '18 16:08

Eric Stdlib


People also ask

How do you break into pdb?

Optionally, you can also tell pdb to break only when a certain condition is true. Use the command b (break) to set a breakpoint. You can specify a line number or a function name where execution is stopped. If filename: is not specified before the line number lineno , then the current source file is used.

Can you go back in pdb?

Nope. PDB cannot turn back time.

How do I use pdb post mortem?

Post-mortem debugging From here, you are dropped into a (Pdb) prompt. To start execution, you use the continue or c command. If the program executes successfully, you will be taken back to the (Pdb) prompt where you can restart the execution again. At this point, you can use quit / q or Ctrl+D to exit the debugger.

How do I watch a variable in pdb?

For watching a variable when you are hitting a breakpoint, you can use the commands command. E.g. printing some_variable when hitting breakpoint #1 (canonical example from pdb doc). Additionally, you can use the condition command to ensure the breakpoint is only hit whenever the variable takes a certain value.


2 Answers

tl;dr: You can debug the inner exception even after you've already entered post-mortem debugging for the outer exception. Here's how to do it:

  1. Enter interactive mode from pdb (type interact into the pdb prompt).
  2. Run:
    import pdb, sys; pdb.post_mortem(sys.last_value.__context__.__traceback__)
    
    Note:
    • Replace __context__ with __cause__ if your exception is explicitly chained; also append more __context__s or __cause__s if it's nested deeper.
    • If you're inspecting a handled exception (one that was caught in a try-catch), replace sys.last_value with sys.exc_info()[1]. If you're unsure, inspect the exception values before proceeding. (Thanks @The Doctor for pointing this out in comments)
  3. This starts a new pdb session that allows you to debug the inner exception.

The following is a long and detailed explanation of why this work. Before diving into the solution, I'll first explain some related concepts:


Chained Exceptions

The "exception within exception" here is known as chained exceptions. Exceptions can be chained explicitly or implicitly:

>>>: try:
...:     raise ZeroDivisionError
...: except Exception as inner_exc:
...:     raise ValueError  # implicit chaining
...:
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-6-1ae22e81c853> in <module>
      1 try:
----> 2     raise ZeroDivisionError
      3 except Exception as inner_exc:

ZeroDivisionError:

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-6-1ae22e81c853> in <module>
      2     raise ZeroDivisionError
      3 except Exception as inner_exc:
----> 4     raise ValueError  # implicit chaining

ValueError:


>>>: try:
...:     raise ZeroDivisionError
...: except Exception as inner_exc:
...:     raise ValueError from inner_exc  # explicit chaining
...:
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-5-63c49fcb10a2> in <module>
      1 try:
----> 2     raise ZeroDivisionError
      3 except Exception as inner_exc:

ZeroDivisionError:

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
<ipython-input-5-63c49fcb10a2> in <module>
      2     raise ZeroDivisionError
      3 except Exception as inner_exc:
----> 4     raise ValueError from inner_exc  # explicit chaining

ValueError:

If we catch the outer exception as outer_exc, then we can inspect the inner exception through outer_exc.__cause__ (if explicitly chained) or outer_exc.__context__ (if implicitly chained).


Post-mortem Debugging

Running a script with python -m pdb allows the Python debugger to enter post-mortem debugging mode on exception. "Post-mortem" here means "after the exception has occurred". You can do the same from an IPython console or within Jupyter notebooks by running the %debug magic.

You can also manually enter post-mortem debugging mode if you have access to the traceback object. Luckily, traceback objects are stored on the exception object itself as the __traceback__ attribute:

>>> try:
...     raise ZeroDivisionError:
... except Exception as e:
...     # Variable `e` is local to this block, so we store it in another variable
...     # to extend its lifetime.
...     exc = e

>>> import pdb
>>> pdb.post_mortem(exc.__traceback__)
> <ipython-input-8-e5b5ed89e466>(2)<module>()
-> raise ZeroDivisionError
(Pdb)

Debugging Chained Exceptions

Now we can try debugging the chained exceptions! Assume we're already in post-mortem debugging mode for the outer exception. What we need to do is:

  1. Get the outer exception object;
  2. Access the inner exception object, and get its traceback;
  3. Call pdb.post_mortem() on that traceback object. Here's what we do:
# First, enter interactive mode to execute commands.
(Pdb) interact
*interactive*
# The current exception is stored in `sys.exc_info()`.  This gives back a tuple
# of (exception type, exception value, traceback).
>>> import sys
>>> sys.exc_info()
(<class 'AssertionError'>, AssertionError(), <traceback object at 0x10c683e00>)
>>> sys.exc_info()[1]
AssertionError()
# In our case, the inner exception is implicitly chained.  Access it through
# the `__context__` attribute.
>>> sys.exc_info()[1].__context__
ZeroDivisionError('division by zero')
# Get its traceback, and enter post-mortem debugging.
>>> sys.exc_info()[1].__context__.__traceback__
<traceback object at 0x10c683c80>
>>> import pdb
>>> pdb.post_mortem(sys.exc_info()[1].__context__.__traceback__)
> /Volumes/hrt/trunk-3/a.py(2)f()
-> x / x
(Pdb)

There you have it! You can now debug the inner exception using normal pdb commands, such as walking through the stack or inspecting local variables.

like image 138
Zecong Hu Avatar answered Nov 24 '22 23:11

Zecong Hu


Actually re-raising the exception (option 3 in wholevinski's answer) solves my problem, since it does not require me to modify function f. Here is the code:

'''Option 3'''
def f(x) :
    x -= 1
    x / x

def g(x) :
    try :
        for i in range(x, 0, -1) :
            print(f(i))
    except Exception as e :
        raise e

g(10)

Pdb output of the stack:

(Pdb) bt
  /usr/lib64/python3.6/pdb.py(1667)main()
-> pdb._runscript(mainpyfile)
  /usr/lib64/python3.6/pdb.py(1548)_runscript()
-> self.run(statement)
  /usr/lib64/python3.6/bdb.py(434)run()
-> exec(cmd, globals, locals)
  <string>(1)<module>()->None
  /tmp/a.py(13)<module>()->None
-> g(10)
  /tmp/a.py(11)g()
-> raise e
  /tmp/a.py(9)g()
-> print(f(i))
> /tmp/a.py(4)f()
-> x / x
(Pdb) 
like image 29
Eric Stdlib Avatar answered Nov 24 '22 23:11

Eric Stdlib