diff --git a/Lib/pdb.py b/Lib/pdb.py index fc83728fb6dc94..ed13de2f587cd9 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -2647,6 +2647,16 @@ def set_trace(*, header=None, commands=None): just before debugging begins. *commands* is an optional list of pdb commands to run when the debugger starts. """ + # gh-138641: Check if we're already in a pdb session. + frame = sys._getframe() + while frame: + if (frame.f_code.co_name == 'interaction' + and frame.f_code.co_filename.endswith('pdb.py')): + print("Nested breakpoint calls are not supported. " + "Already running in the debugger.", file=sys.stderr) + return + frame = frame.f_back + if Pdb._last_pdb_instance is not None: pdb = Pdb._last_pdb_instance else: @@ -2662,6 +2672,17 @@ async def set_trace_async(*, header=None, commands=None): if they enter the debugger with this function. Otherwise it's the same as set_trace(). """ + # gh-138641: Check if we're already in a pdb session. + frame = sys._getframe() + while frame: + if (frame.f_code.co_name == 'interaction' and + frame.f_code.co_filename.endswith('pdb.py')): + # We're already in a pdb session, just print a message and return + print("Nested breakpoint calls are not supported. " + "Already running in the debugger.", file=sys.stderr) + return + frame = frame.f_back + if Pdb._last_pdb_instance is not None: pdb = Pdb._last_pdb_instance else: diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 6b74e21ad73d1a..056949325a1399 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -17,14 +17,14 @@ import zipfile from asyncio.events import _set_event_loop_policy -from contextlib import ExitStack, redirect_stdout +from contextlib import ExitStack, redirect_stdout, redirect_stderr from io import StringIO from test import support from test.support import has_socket_support, os_helper from test.support.import_helper import import_module from test.support.pty_helper import run_pty, FakeInput from test.support.script_helper import kill_python -from unittest.mock import patch +from unittest.mock import Mock, patch SKIP_CORO_TESTS = False @@ -4539,6 +4539,18 @@ def bar(): ])) self.assertIn('break in bar', stdout) + def test_nested_breakpoint_calls(self): + # gh-138641 pdb.set_trace() called when already in the debugger + mock_frame = Mock() + mock_frame.f_code.co_name = 'interaction' + mock_frame.f_code.co_filename = '/path/to/pdb.py' + mock_frame.f_back = None + captured_stderr = io.StringIO() + with redirect_stderr(captured_stderr): + with patch('sys._getframe', return_value=mock_frame): + pdb.set_trace() + stderr_content = captured_stderr.getvalue() + self.assertIn("Nested breakpoint calls are not supported", stderr_content) class ChecklineTests(unittest.TestCase): def setUp(self): diff --git a/Misc/NEWS.d/next/Library/2025-09-08-17-50-33.gh-issue-138641.nBPvKe.rst b/Misc/NEWS.d/next/Library/2025-09-08-17-50-33.gh-issue-138641.nBPvKe.rst new file mode 100644 index 00000000000000..c0b0a743fe921c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-08-17-50-33.gh-issue-138641.nBPvKe.rst @@ -0,0 +1,2 @@ +Prevent error when calling :func:`breakpoint` inside a :mod:`pdb` debugging session. +Nested breakpoint calls now display a helpful error message instead of causing error.