Skip to content

gh-103693: Add convenience variable feature to pdb #103694

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Doc/library/pdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,21 @@ the commands; the input is split at the first ``;;`` pair, even if it is in the
middle of a quoted string. A workaround for strings with double semicolons
is to use implicit string concatenation ``';'';'`` or ``";"";"``.

To set a temporary global variable, use a *convenience variable*. A *convenience
variable* is a variable whose name starts with ``$``. For example, ``$foo = 1``
sets a global variable ``$foo`` which you can use in the debugger session. The
*convenience variables* are cleared when the program resumes execution so it's
less likely to interfere with your program compared to using normal variables
like ``foo = 1``.

There are three preset *convenience variables*:

* ``$_frame``: the current frame you are debugging
* ``$_retval``: the return value if the frame is returning
* ``$_exception``: the exception if the frame is raising an exception

.. versionadded:: 3.12

.. index::
pair: .pdbrc; file
triple: debugger; configuration; file
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ os.path
* Add :func:`os.path.splitroot` to split a path into a triad
``(drive, root, tail)``. (Contributed by Barney Gale in :gh:`101000`.)

pdb
---

* Add convenience variables to hold values temporarily for debug session
and provide quick access to values like the current frame or the return
value.
(Contributed by Tian Gao in :gh:`103693`.)

shutil
------

Expand Down
17 changes: 17 additions & 0 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ def forget(self):
self.lineno = None
self.stack = []
self.curindex = 0
if hasattr(self, 'curframe') and self.curframe:
self.curframe.f_globals.pop('__pdb_convenience_variables', None)
self.curframe = None
self.tb_lineno.clear()

Expand All @@ -288,6 +290,7 @@ def setup(self, f, tb):
# locals whenever the .f_locals accessor is called, so we
# cache it here to ensure that modifications are not overwritten.
self.curframe_locals = self.curframe.f_locals
self.set_convenience_variable(self.curframe, '_frame', self.curframe)
return self.execRcLines()

# Can be executed earlier than 'setup' if desired
Expand Down Expand Up @@ -359,6 +362,7 @@ def user_return(self, frame, return_value):
if self._wait_for_mainpyfile:
return
frame.f_locals['__return__'] = return_value
self.set_convenience_variable(frame, '_retval', return_value)
self.message('--Return--')
self.interaction(frame, None)

Expand All @@ -369,6 +373,7 @@ def user_exception(self, frame, exc_info):
return
exc_type, exc_value, exc_traceback = exc_info
frame.f_locals['__exception__'] = exc_type, exc_value
self.set_convenience_variable(frame, '_exception', exc_value)

# An 'Internal StopIteration' exception is an exception debug event
# issued by the interpreter when handling a subgenerator run with
Expand All @@ -394,6 +399,7 @@ def _cmdloop(self):
self.message('--KeyboardInterrupt--')

# Called before loop, handles display expressions
# Set up convenience variable containers
def preloop(self):
displaying = self.displaying.get(self.curframe)
if displaying:
Expand Down Expand Up @@ -477,6 +483,9 @@ def precmd(self, line):
next = line[marker+2:].lstrip()
self.cmdqueue.append(next)
line = line[:marker].rstrip()

# Replace all the convenience variables
line = re.sub(r'\$([a-zA-Z_][a-zA-Z0-9_]*)', r'__pdb_convenience_variables["\1"]', line)
return line

def onecmd(self, line):
Expand Down Expand Up @@ -527,6 +536,13 @@ def message(self, msg):
def error(self, msg):
print('***', msg, file=self.stdout)

# convenience variables

def set_convenience_variable(self, frame, name, value):
if '__pdb_convenience_variables' not in frame.f_globals:
frame.f_globals['__pdb_convenience_variables'] = {}
frame.f_globals['__pdb_convenience_variables'][name] = value

# Generic completion functions. Individual complete_foo methods can be
# assigned below to one of these functions.

Expand Down Expand Up @@ -1018,6 +1034,7 @@ def _select_frame(self, number):
self.curindex = number
self.curframe = self.stack[self.curindex][0]
self.curframe_locals = self.curframe.f_locals
self.set_convenience_variable(self.curframe, '_frame', self.curframe)
self.print_stack_entry(self.stack[self.curindex])
self.lineno = None

Expand Down
78 changes: 78 additions & 0 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,84 @@ def test_pdb_where_command():
(Pdb) continue
"""

def test_convenience_variables():
"""Test convenience variables

>>> def util_function():
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
... try:
... raise Exception('test')
... except:
... pass
... return 1

>>> def test_function():
... util_function()

>>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... '$_frame.f_lineno', # Check frame convenience variable
... '$a = 10', # Set a convenience variable
... '$a', # Print its value
... 'p $a + 2', # Do some calculation
... 'u', # Switch frame
... '$_frame.f_lineno', # Make sure the frame changed
... '$a', # Make sure the value persists
... 'd', # Go back to the original frame
... 'next',
... '$a', # The value should be gone
... 'next',
... '$_exception', # Check exception convenience variable
... 'next',
... '$_exception', # Exception should be gone
... 'return',
... '$_retval', # Check return convenience variable
... 'continue',
... ]):
... test_function()
> <doctest test.test_pdb.test_convenience_variables[0]>(3)util_function()
-> try:
(Pdb) $_frame.f_lineno
3
(Pdb) $a = 10
(Pdb) $a
10
(Pdb) p $a + 2
12
(Pdb) u
> <doctest test.test_pdb.test_convenience_variables[1]>(2)test_function()
-> util_function()
(Pdb) $_frame.f_lineno
2
(Pdb) $a
10
(Pdb) d
> <doctest test.test_pdb.test_convenience_variables[0]>(3)util_function()
-> try:
(Pdb) next
> <doctest test.test_pdb.test_convenience_variables[0]>(4)util_function()
-> raise Exception('test')
(Pdb) $a
*** KeyError: 'a'
(Pdb) next
Exception: test
> <doctest test.test_pdb.test_convenience_variables[0]>(4)util_function()
-> raise Exception('test')
(Pdb) $_exception
Exception('test')
(Pdb) next
> <doctest test.test_pdb.test_convenience_variables[0]>(5)util_function()
-> except:
(Pdb) $_exception
*** KeyError: '_exception'
(Pdb) return
--Return--
> <doctest test.test_pdb.test_convenience_variables[0]>(7)util_function()->1
-> return 1
(Pdb) $_retval
1
(Pdb) continue
"""

def test_post_mortem():
"""Test post mortem traceback debugging.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add convenience variable feature to :mod:`pdb`