3

Can I show the context any time I step through code using Python's pdb debugger without having to explicitly call the list command?

I've tried chaining commands with something like n & l or n && l or nl or n + l or n; l. I can't find any documentation regarding this.

The trouble is, any time I step through the code, I end up typing n RET and then either l RET or l l RET...every..single..time to see some context.

Steve Ferg's guide describes the apparent intended pdb workflow accurately:

So a typical interaction with pdb might go like this

  • The pdb.set_trace() statement is encountered, and you start tracing with the (Pdb) prompt
  • You press “n” and then ENTER, to start stepping through your code.
  • You just press ENTER to step again.
  • You just press ENTER to step again.
  • You just press ENTER to step again. etc. etc. etc.
  • Eventually, you realize that you are a bit lost. You’re not exactly sure where you are in your program any more. So…
  • You press “l” and then ENTER. This lists the area of your program that is currently being executed.
  • You inspect the display, get your bearings, and are ready to start again. So….
  • You press “n” and then ENTER, to start stepping through your code.
  • You just press ENTER to step again.
  • You just press ENTER to step again. etc. etc. etc.

It seems obvious to me that showing the context after every move would be useful and desired by the user. However, since there doesn't seem to be a simple option to do so, it makes me think that I'm using pdb incorrectly. Maybe my constant need to see the context is an indication of misuse? But how else would I use pdb?

2 Answers 2

3

This can be done by creating a .pdbrc file and using the alias command.

In your .pdbrc file put,

alias n next ;; l
alias s step ;; l
alias u up ;; l
alias d down ;; l

Then when you press n, the command next ;; l will be issued instead and similarly for s, u, and d (thanks @Mathias for suggesting u and d). The double-semi colons separate commands so that it's like you pressed next RET l RET.

You can read more about the alias command, and others, in the pdb documentation.

Be warned that getting pdb to read the .pdbrc file is a bit of a hassle on Windows. pdb looks for the HOME system variable which is not available by default on Windows. You'll have to manually create a HOME system variable and put the folder containing .pdcrc into it. I have documented specifically how to do this in another response: How can I define .pdbrc on a Windows machine?

1
  • The one downside of this is now the l command is at the top of the command stack. So pressing RET after an n command will not go to the next instruction as expected, but will list the next block in the file.
    – Hakan Baba
    Commented Jun 3, 2023 at 5:24
1

Try this.

In your .pdbrc file

import os
exec(open(os.path.expanduser("~/.pdbrc.py")).read())


alias n next ;; list_restore_lastcmd .
alias s step ;; list_restore_lastcmd .
alias u up ;; list_restore_lastcmd .
alias d down ;; list_restore_lastcmd .
alias r return ;; list_restore_lastcmd .

Then in your .pdbrc.py file



import pdb

import inspect

actual_precmd_source = inspect.getsource(pdb.Pdb.precmd)
# Store this source code string in pdb.Pdb object, so that it can be used later
# on by the monkeypatch functions.
pdb.Pdb._reserverd_actual_precmd_source = actual_precmd_source


def new_precmd(self, line, *args, **kwargs):
    # This function calls the original precmd function,
    # but also stores the `self.lastcmd` at the time of the call in a new member variable.
    # This new state end up being the last-last-command.

    # This function is monkeypatched in the most bizarre way.
    # Since this function ends up calling its own original implementation, we
    # cannot simply override it.
    # If we do, it becomes an infinite recursion.

    # So instead, we read the code of the original function in a string with
    # `inspect` package,
    # do some string manipulation to build a trampoline function's source code
    # and then execute that function.

    # These re-imports are required because otherwise falling into pdb with a
    # breakpoint() instruction does not see the top-of-file imports.
    import pdb
    original_source = pdb.Pdb._reserverd_actual_precmd_source

    ss = f"""
def trampoline_precmd(self, line, *args, **kwargs):
{original_source}

    # Store the last-last-command
    self._reserved_last_last_cmd = self.lastcmd
    return precmd(self, line, *args, **kwargs)
"""

    d = {}
    exec(ss, d)
    return d["trampoline_precmd"](self, line, *args, **kwargs)


def do_list_restore_lastcmd(self, arg):
    # A simple monkeypatch function to call the actual list function,
    # but without recording the execution in the self.lastcmd state.

    # At the end, it restores the lastcmd state to the actual previously
    # executed command that was specified by the user.

    # To do that, it uses the newly added _reserved_last_last_cmd state, which
    # is now maintained in the monkeypatched precmd function.
    self.do_list(arg)
    self.lastcmd = self._reserved_last_last_cmd


def new_emptyline(self):
    # This monkeypatch function handles the use case where the user merely
    # presses RET on an empty line to execute the previous command as is.
    # That is a pretty common use case in command line debuggers.

    # We only support the `next` command and whenever we encounter the next
    # command that is indirectly executed via an empty line and RET, we enqueue
    # a `list` command right after.

    # The `list` command we enqueue does not record itself in the self.lastcmd history,
    # so this becomes hidden from the user.
    # Hence the user can press RET again to execute the original `next` command again.

    # These re-imports are required because otherwise falling into pdb with a
    # breakpoint() instruction does not see the top-of-file imports.
    import pdb
    rv = super(pdb.Pdb, self).emptyline()

    # If the repeated command is next, then also print the context
    if self.lastcmd in ["next"]:
        self.cmdqueue.append("list_restore_lastcmd .")

    return rv


# Monkeypatch the functions, using the approach here https://stackoverflow.com/a/19546169/5771861
pdb.Pdb.precmd = new_precmd
pdb.Pdb.do_list_restore_lastcmd = do_list_restore_lastcmd
pdb.Pdb.emptyline = new_emptyline

This ends up being quite a nuanced monkeypatch that becomes somewhat implementation dependent of the pdb.Pdb class. So this may not fit everyone's expectations. Definitely not a zero-maintenance approach.

But this leaves a reasonable experience with command repeats and doing the context printing in an invisible way.


For example debugging the following script

def complex_calculation(a, b, c, d, e):
    f = a + b
    g = c * d
    h = e - f
    i = g / h
    j = i ** 2
    k = abs(j - g)
    return k



def bubble_sort(array):
    n = len(array)

    for i in range(n):
        already_sorted = True

        for j in range(n - i - 1):
            if array[j] > array[j + 1]:
                array[j], array[j + 1] = array[j + 1], array[j]
                already_sorted = False

        if already_sorted:
            break

    return array


out = complex_calculation(1, 2, 3, 4, 5)
out = bubble_sort([1, 5, 2, 7, 3, 9, 4, 6, 8])

Looks like the following in the pdb command line

hakan@hakan:~/misc$ python3 -m pdb dummy.py
> /Users/hakan/misc/dummy.py(2)<module>()
-> def complex_calculation(a, b, c, d, e):
(Pdb) n
> /Users/hakan/misc/dummy.py(13)<module>()
-> def bubble_sort(array):
  8         k = abs(j - g)
  9         return k
 10
 11
 12
 13  -> def bubble_sort(array):
 14         n = len(array)
 15
 16         for i in range(n):
 17             already_sorted = True
 18
(Pdb) n
> /Users/hakan/misc/dummy.py(30)<module>()
-> out = complex_calculation(1, 2, 3, 4, 5)
 25                 break
 26
 27         return array
 28
 29
 30  -> out = complex_calculation(1, 2, 3, 4, 5)
 31     out = bubble_sort([1, 5, 2, 7, 3, 9, 4, 6, 8])
 32
 33
 34
 35
(Pdb) s
--Call--
> /Users/hakan/misc/dummy.py(2)complex_calculation()
-> def complex_calculation(a, b, c, d, e):
  1
  2  -> def complex_calculation(a, b, c, d, e):
  3         f = a + b
  4         g = c * d
  5         h = e - f
  6         i = g / h
  7         j = i ** 2
  8         k = abs(j - g)
  9         return k
 10
 11
(Pdb) n
> /Users/hakan/misc/dummy.py(3)complex_calculation()
-> f = a + b
  1
  2     def complex_calculation(a, b, c, d, e):
  3  ->     f = a + b
  4         g = c * d
  5         h = e - f
  6         i = g / h
  7         j = i ** 2
  8         k = abs(j - g)
  9         return k
 10
 11
(Pdb)
> /Users/hakan/misc/dummy.py(4)complex_calculation()
-> g = c * d
  1
  2     def complex_calculation(a, b, c, d, e):
  3         f = a + b
  4  ->     g = c * d
  5         h = e - f
  6         i = g / h
  7         j = i ** 2
  8         k = abs(j - g)
  9         return k
 10
 11
(Pdb)
> /Users/hakan/misc/dummy.py(5)complex_calculation()
-> h = e - f
  1
  2     def complex_calculation(a, b, c, d, e):
  3         f = a + b
  4         g = c * d
  5  ->     h = e - f
  6         i = g / h
  7         j = i ** 2
  8         k = abs(j - g)
  9         return k
 10
 11
(Pdb)
> /Users/hakan/misc/dummy.py(6)complex_calculation()
-> i = g / h
  1
  2     def complex_calculation(a, b, c, d, e):
  3         f = a + b
  4         g = c * d
  5         h = e - f
  6  ->     i = g / h
  7         j = i ** 2
  8         k = abs(j - g)
  9         return k
 10
 11
(Pdb) r
--Return--
> /Users/hakan/misc/dummy.py(9)complex_calculation()->24.0
-> return k
  4         g = c * d
  5         h = e - f
  6         i = g / h
  7         j = i ** 2
  8         k = abs(j - g)
  9  ->     return k
 10
 11
 12
 13     def bubble_sort(array):
 14         n = len(array)
(Pdb) n
> /Users/hakan/misc/dummy.py(31)<module>()
-> out = bubble_sort([1, 5, 2, 7, 3, 9, 4, 6, 8])
 26
 27         return array
 28
 29
 30     out = complex_calculation(1, 2, 3, 4, 5)
 31  -> out = bubble_sort([1, 5, 2, 7, 3, 9, 4, 6, 8])
 32
 33
 34
 35
 36     print(out)
(Pdb) s
--Call--
> /Users/hakan/misc/dummy.py(13)bubble_sort()
-> def bubble_sort(array):
  8         k = abs(j - g)
  9         return k
 10
 11
 12
 13  -> def bubble_sort(array):
 14         n = len(array)
 15
 16         for i in range(n):
 17             already_sorted = True
 18
(Pdb) n
> /Users/hakan/misc/dummy.py(14)bubble_sort()
-> n = len(array)
  9         return k
 10
 11
 12
 13     def bubble_sort(array):
 14  ->     n = len(array)
 15
 16         for i in range(n):
 17             already_sorted = True
 18
 19             for j in range(n - i - 1):
(Pdb)
> /Users/hakan/misc/dummy.py(16)bubble_sort()
-> for i in range(n):
 11
 12
 13     def bubble_sort(array):
 14         n = len(array)
 15
 16  ->     for i in range(n):
 17             already_sorted = True
 18
 19             for j in range(n - i - 1):
 20                 if array[j] > array[j + 1]:
 21                     array[j], array[j + 1] = array[j + 1], array[j]
(Pdb)
> /Users/hakan/misc/dummy.py(17)bubble_sort()
-> already_sorted = True
 12
 13     def bubble_sort(array):
 14         n = len(array)
 15
 16         for i in range(n):
 17  ->         already_sorted = True
 18
 19             for j in range(n - i - 1):
 20                 if array[j] > array[j + 1]:
 21                     array[j], array[j + 1] = array[j + 1], array[j]
 22                     already_sorted = False
(Pdb)
> /Users/hakan/misc/dummy.py(19)bubble_sort()
-> for j in range(n - i - 1):
 14         n = len(array)
 15
 16         for i in range(n):
 17             already_sorted = True
 18
 19  ->         for j in range(n - i - 1):
 20                 if array[j] > array[j + 1]:
 21                     array[j], array[j + 1] = array[j + 1], array[j]
 22                     already_sorted = False
 23
 24             if already_sorted:
(Pdb)
> /Users/hakan/misc/dummy.py(20)bubble_sort()
-> if array[j] > array[j + 1]:
 15
 16         for i in range(n):
 17             already_sorted = True
 18
 19             for j in range(n - i - 1):
 20  ->             if array[j] > array[j + 1]:
 21                     array[j], array[j + 1] = array[j + 1], array[j]
 22                     already_sorted = False
 23
 24             if already_sorted:
 25                 break
(Pdb)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .