From 03bad227c19799ff521602153ac6a0364f3969a2 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 17:50:53 +0800 Subject: [PATCH 01/11] fix: breakpoint in pdb can cause crash Signed-off-by: yihong0618 --- Lib/pdb.py | 9 +++++++++ .../2025-09-08-17-50-33.gh-issue-138641.nBPvKe.rst | 1 + 2 files changed, 10 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-09-08-17-50-33.gh-issue-138641.nBPvKe.rst diff --git a/Lib/pdb.py b/Lib/pdb.py index fc83728fb6dc94..eb06fdd3ffbc3b 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -926,6 +926,9 @@ def _read_code(self, line): def default(self, line): if line[:1] == '!': line = line[1:].strip() + if not self.curframe: + self.error("No current frame.") + return locals = self.curframe.f_locals globals = self.curframe.f_globals try: @@ -2131,6 +2134,9 @@ def do_list(self, arg): first = self.lineno + 1 if last is None: last = first + 10 + if not self.curframe: + self.error('No current frame.') + return filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: @@ -2153,6 +2159,9 @@ def do_longlist(self, arg): if arg: self._print_invalid_arg(arg) return + if not self.curframe: + self.error('No current frame.') + return filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: 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..f214b9c4999b06 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-08-17-50-33.gh-issue-138641.nBPvKe.rst @@ -0,0 +1 @@ +Fix: breakpoint() in pdb can cause crash. From e4b64144b737f040db385728769e8e23bf363642 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 18:10:38 +0800 Subject: [PATCH 02/11] fix: address comments Signed-off-by: yihong0618 --- Lib/pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index eb06fdd3ffbc3b..36b061efb05d8a 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -925,10 +925,10 @@ def _read_code(self, line): return code, buffer, is_await_code def default(self, line): - if line[:1] == '!': line = line[1:].strip() if not self.curframe: self.error("No current frame.") return + if line[:1] == '!': line = line[1:].strip() locals = self.curframe.f_locals globals = self.curframe.f_globals try: From cb73cb0750fb975b321433cdeab24dec1a9d4b22 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 18:20:30 +0800 Subject: [PATCH 03/11] fix: follow the comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yihong0618 Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/pdb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 36b061efb05d8a..b14abf6771d382 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -2111,6 +2111,9 @@ def do_list(self, arg): exception was originally raised or propagated is indicated by ">>", if it differs from the current line. """ + if not self.curframe: + self.error('No current frame.') + return self.lastcmd = 'list' last = None if arg and arg != '.': @@ -2134,9 +2137,6 @@ def do_list(self, arg): first = self.lineno + 1 if last is None: last = first + 10 - if not self.curframe: - self.error('No current frame.') - return filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: From e1ba4208b97a6e962f8048c5dd6a3406513961a0 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 19:04:53 +0800 Subject: [PATCH 04/11] fix: follow another and co-author Signed-off-by: yihong0618 --- Lib/pdb.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/pdb.py b/Lib/pdb.py index b14abf6771d382..b1adc25dfd704a 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1332,6 +1332,9 @@ def do_break(self, arg, temporary=False): if bp: self.message(bp.bpformat()) return + if not self.curframe: + self.error('No current frame.') + return # parse arguments; comma has lowest precedence # and cannot occur in filename filename = None @@ -1411,6 +1414,7 @@ def do_break(self, arg, temporary=False): # To be overridden in derived debuggers def defaultFile(self): """Produce a reasonable default.""" + assert self.curframe is not None filename = self.curframe.f_code.co_filename if filename == '' and self.mainpyfile: filename = self.mainpyfile @@ -1949,6 +1953,9 @@ def do_debug(self, arg): if not arg: self._print_invalid_arg(arg) return + if not self.curframe: + self.error('No current frame.') + return self.stop_trace() globals = self.curframe.f_globals locals = self.curframe.f_locals From 7632a3c47fa472b7cfc4753f2975d88347447edb Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 19:14:14 +0800 Subject: [PATCH 05/11] fix: follow up again Signed-off-by: yihong0618 --- Lib/pdb.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index b1adc25dfd704a..930ecd7758fa86 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1184,6 +1184,7 @@ def complete_multiline_names(self, text, line, begidx, endidx): return self.completedefault(text, line, begidx, endidx) def completedefault(self, text, line, begidx, endidx): + assert self.curframe is not None if text.startswith("$"): # Complete convenience variables conv_vars = self.curframe.f_globals.get('__pdb_convenience_variables', {}) @@ -1332,9 +1333,6 @@ def do_break(self, arg, temporary=False): if bp: self.message(bp.bpformat()) return - if not self.curframe: - self.error('No current frame.') - return # parse arguments; comma has lowest precedence # and cannot occur in filename filename = None @@ -1414,7 +1412,6 @@ def do_break(self, arg, temporary=False): # To be overridden in derived debuggers def defaultFile(self): """Produce a reasonable default.""" - assert self.curframe is not None filename = self.curframe.f_code.co_filename if filename == '' and self.mainpyfile: filename = self.mainpyfile From a136458a481e46cbe7c15de80226da6e5eedd380 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 19:29:49 +0800 Subject: [PATCH 06/11] fix: follow up again Signed-off-by: yihong0618 --- Lib/pdb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/pdb.py b/Lib/pdb.py index 930ecd7758fa86..bca90200e7e6f8 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1412,6 +1412,9 @@ def do_break(self, arg, temporary=False): # To be overridden in derived debuggers def defaultFile(self): """Produce a reasonable default.""" + if self.curframe is None: + self.error("No current frame.") + return None filename = self.curframe.f_code.co_filename if filename == '' and self.mainpyfile: filename = self.mainpyfile From 601bc1be61dd5978573826c798fd4eb053f7ad16 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 19:35:21 +0800 Subject: [PATCH 07/11] fix: need to be string in defaultFile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yihong0618 Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/pdb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index bca90200e7e6f8..7bc3f30ff670b6 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1413,9 +1413,9 @@ def do_break(self, arg, temporary=False): def defaultFile(self): """Produce a reasonable default.""" if self.curframe is None: - self.error("No current frame.") - return None - filename = self.curframe.f_code.co_filename + filename = '' + else: + filename = self.curframe.f_code.co_filename if filename == '' and self.mainpyfile: filename = self.mainpyfile return filename From 884ee49c3ee27d2cbdc5430ae8b94458630d0204 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 21:20:07 +0800 Subject: [PATCH 08/11] fix: revert change --- Lib/pdb.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 7bc3f30ff670b6..97a85d6071edc8 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -925,9 +925,6 @@ def _read_code(self, line): return code, buffer, is_await_code def default(self, line): - if not self.curframe: - self.error("No current frame.") - return if line[:1] == '!': line = line[1:].strip() locals = self.curframe.f_locals globals = self.curframe.f_globals @@ -1184,7 +1181,6 @@ def complete_multiline_names(self, text, line, begidx, endidx): return self.completedefault(text, line, begidx, endidx) def completedefault(self, text, line, begidx, endidx): - assert self.curframe is not None if text.startswith("$"): # Complete convenience variables conv_vars = self.curframe.f_globals.get('__pdb_convenience_variables', {}) @@ -1953,9 +1949,6 @@ def do_debug(self, arg): if not arg: self._print_invalid_arg(arg) return - if not self.curframe: - self.error('No current frame.') - return self.stop_trace() globals = self.curframe.f_globals locals = self.curframe.f_locals @@ -2118,9 +2111,6 @@ def do_list(self, arg): exception was originally raised or propagated is indicated by ">>", if it differs from the current line. """ - if not self.curframe: - self.error('No current frame.') - return self.lastcmd = 'list' last = None if arg and arg != '.': @@ -2166,9 +2156,6 @@ def do_longlist(self, arg): if arg: self._print_invalid_arg(arg) return - if not self.curframe: - self.error('No current frame.') - return filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: From 31c8dbcd58490c57e82322f58f8257710064f4a2 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 21:20:49 +0800 Subject: [PATCH 09/11] fix: revert another change Signed-off-by: yihong0618 --- Lib/pdb.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 97a85d6071edc8..fc83728fb6dc94 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1408,10 +1408,7 @@ def do_break(self, arg, temporary=False): # To be overridden in derived debuggers def defaultFile(self): """Produce a reasonable default.""" - if self.curframe is None: - filename = '' - else: - filename = self.curframe.f_code.co_filename + filename = self.curframe.f_code.co_filename if filename == '' and self.mainpyfile: filename = self.mainpyfile return filename From 506bcf6676c7644e758cfaf1047f72e0b9d3f577 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 8 Sep 2025 21:22:08 +0800 Subject: [PATCH 10/11] fix: change with gaotian Signed-off-by: yihong0618 Co-authored-by: Tian Gao --- Lib/pdb.py | 20 +++++++++++++++++++ ...-09-08-17-50-33.gh-issue-138641.nBPvKe.rst | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index fc83728fb6dc94..9126438a7ca8fb 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. """ + # Check if we're already in a pdb session by examining the call stack + 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: @@ -2662,6 +2672,16 @@ 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(). """ + # Check if we're already in a pdb session by examining the call stack + 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/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 index f214b9c4999b06..c0b0a743fe921c 100644 --- 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 @@ -1 +1,2 @@ -Fix: breakpoint() in pdb can cause crash. +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. From 7905b0d95c23b1a81d3b660463bea9e2c5407b48 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Tue, 9 Sep 2025 09:03:07 +0800 Subject: [PATCH 11/11] fix: add unittest for it Signed-off-by: yihong0618 --- Lib/pdb.py | 15 ++++++++------- Lib/test/test_pdb.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 9126438a7ca8fb..ed13de2f587cd9 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -2647,12 +2647,12 @@ 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. """ - # Check if we're already in a pdb session by examining the call stack + # 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. " + 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 @@ -2672,12 +2672,13 @@ 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(). """ - # Check if we're already in a pdb session by examining the call stack + # 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'): + 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. " + print("Nested breakpoint calls are not supported. " "Already running in the debugger.", file=sys.stderr) return frame = frame.f_back 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):