From 7df087ee86f1d4dbd1ade3bf78499a4abc4723f1 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 00:56:17 +0900 Subject: [PATCH 01/11] Check bytecode before accepting a breakpoint in pdb --- Lib/bdb.py | 20 ++++++++++++++++++++ Lib/test/test_pdb.py | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Lib/bdb.py b/Lib/bdb.py index 50cf2b3f5b3e45..e3a67cc389343d 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -177,6 +177,17 @@ def _get_lineno(self, code, offset): return last_lineno +def _get_executable_linenos(code): + linenos = set() + for _, _, lineno in code.co_lines(): + if lineno is not None: + linenos.add(lineno) + for const in code.co_consts: + if hasattr(const, 'co_lines'): + linenos |= _get_executable_linenos(const) + return linenos + + class Bdb: """Generic Python debugger base class. @@ -671,6 +682,15 @@ def set_break(self, filename, lineno, temporary=False, cond=None, line = linecache.getline(filename, lineno) if not line: return 'Line %s:%d does not exist' % (filename, lineno) + source = ''.join(linecache.getlines(filename)) + if source: + try: + code = compile(source, filename, 'exec') + executable_lines = _get_executable_linenos(code) + if executable_lines and lineno not in executable_lines: + return 'Line %d has no code associated with it' % lineno + except SyntaxError: + pass self._add_to_breaks(filename, lineno) bp = Breakpoint(filename, lineno, temporary, cond, funcname) # After we set a new breakpoint, we need to search through all frames diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 0e23cd6604379c..1cc703d83ada00 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -4188,6 +4188,22 @@ def test_breakpoint(self): self.assertTrue(any("Breakpoint 1 at" in l for l in stdout.splitlines()), stdout) self.assertTrue(all("SUCCESS" not in l for l in stdout.splitlines()), stdout) + def test_breakpoint_on_no_bytecode_line(self): + script = """ + x = 1 + def f(): + global x # line 4: no bytecode + x = 2 + f() + """ + commands = """ + b 4 + c + quit + """ + stdout, _ = self.run_pdb_module(script, commands) + self.assertIn('no code', '\n'.join(stdout.splitlines())) + def test_run_pdb_with_pdb(self): commands = """ c From 27054634b98adbee94a10b229881e5b91f5e91d6 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 00:58:33 +0900 Subject: [PATCH 02/11] Add news entry --- .../next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst diff --git a/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst b/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst new file mode 100644 index 00000000000000..a469844feffc5f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst @@ -0,0 +1,3 @@ +:mod:`bdb` will reports an error when set breakpoint on a line with no +associated bytecode, such as :keyword:`global` or :keyword:`nonlocal` +statements. From 27fc63f7f262f13ff8157faa2356b8620a1fe82f Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 01:06:34 +0900 Subject: [PATCH 03/11] Using the fancy contextlib.suppress to suppress error --- Lib/bdb.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index e3a67cc389343d..a1090124473634 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -5,7 +5,7 @@ import threading import os import weakref -from contextlib import contextmanager +from contextlib import contextmanager, suppress from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -684,13 +684,11 @@ def set_break(self, filename, lineno, temporary=False, cond=None, return 'Line %s:%d does not exist' % (filename, lineno) source = ''.join(linecache.getlines(filename)) if source: - try: + with suppress(SyntaxError): code = compile(source, filename, 'exec') executable_lines = _get_executable_linenos(code) if executable_lines and lineno not in executable_lines: return 'Line %d has no code associated with it' % lineno - except SyntaxError: - pass self._add_to_breaks(filename, lineno) bp = Breakpoint(filename, lineno, temporary, cond, funcname) # After we set a new breakpoint, we need to search through all frames From c631e9f226b56884ae559214e36da65f7e7ea51a Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 01:45:20 +0900 Subject: [PATCH 04/11] Fix tests --- Lib/test/test_bdb.py | 56 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_bdb.py b/Lib/test/test_bdb.py index f15dae13eb384e..b04dee795b320d 100644 --- a/Lib/test/test_bdb.py +++ b/Lib/test/test_bdb.py @@ -976,43 +976,43 @@ def test_load_bps_from_previous_Bdb_instance(self): reset_Breakpoint() db1 = Bdb() fname = db1.canonic(__file__) - db1.set_break(__file__, 1) - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) + db1.set_break(__file__, 51) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) db2 = Bdb() - db2.set_break(__file__, 2) - db2.set_break(__file__, 3) - db2.set_break(__file__, 4) - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) - self.assertEqual(db2.get_all_breaks(), {fname: [1, 2, 3, 4]}) - db2.clear_break(__file__, 1) - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) - self.assertEqual(db2.get_all_breaks(), {fname: [2, 3, 4]}) + db2.set_break(__file__, 52) + db2.set_break(__file__, 53) + db2.set_break(__file__, 54) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) + self.assertEqual(db2.get_all_breaks(), {fname: [51, 52, 53, 54]}) + db2.clear_break(__file__, 51) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) + self.assertEqual(db2.get_all_breaks(), {fname: [52, 53, 54]}) db3 = Bdb() - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) - self.assertEqual(db2.get_all_breaks(), {fname: [2, 3, 4]}) - self.assertEqual(db3.get_all_breaks(), {fname: [2, 3, 4]}) - db2.clear_break(__file__, 2) - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) - self.assertEqual(db2.get_all_breaks(), {fname: [3, 4]}) - self.assertEqual(db3.get_all_breaks(), {fname: [2, 3, 4]}) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) + self.assertEqual(db2.get_all_breaks(), {fname: [52, 53, 54]}) + self.assertEqual(db3.get_all_breaks(), {fname: [52, 53, 54]}) + db2.clear_break(__file__, 52) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) + self.assertEqual(db2.get_all_breaks(), {fname: [53, 54]}) + self.assertEqual(db3.get_all_breaks(), {fname: [52, 53, 54]}) db4 = Bdb() - db4.set_break(__file__, 5) - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) - self.assertEqual(db2.get_all_breaks(), {fname: [3, 4]}) - self.assertEqual(db3.get_all_breaks(), {fname: [2, 3, 4]}) - self.assertEqual(db4.get_all_breaks(), {fname: [3, 4, 5]}) + db4.set_break(__file__, 55) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) + self.assertEqual(db2.get_all_breaks(), {fname: [53, 54]}) + self.assertEqual(db3.get_all_breaks(), {fname: [52, 53, 54]}) + self.assertEqual(db4.get_all_breaks(), {fname: [53, 54, 55]}) reset_Breakpoint() db5 = Bdb() - db5.set_break(__file__, 6) - self.assertEqual(db1.get_all_breaks(), {fname: [1]}) - self.assertEqual(db2.get_all_breaks(), {fname: [3, 4]}) - self.assertEqual(db3.get_all_breaks(), {fname: [2, 3, 4]}) - self.assertEqual(db4.get_all_breaks(), {fname: [3, 4, 5]}) - self.assertEqual(db5.get_all_breaks(), {fname: [6]}) + db5.set_break(__file__, 56) + self.assertEqual(db1.get_all_breaks(), {fname: [51]}) + self.assertEqual(db2.get_all_breaks(), {fname: [53, 54]}) + self.assertEqual(db3.get_all_breaks(), {fname: [52, 53, 54]}) + self.assertEqual(db4.get_all_breaks(), {fname: [53, 54, 55]}) + self.assertEqual(db5.get_all_breaks(), {fname: [56]}) class RunTestCase(BaseTestCase): From d2448560de6bc2cdec31ddb7b48e99e3df523aa4 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 23:30:25 +0900 Subject: [PATCH 05/11] Update Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst Co-authored-by: Tian Gao --- .../next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst b/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst index a469844feffc5f..81984c6db440f4 100644 --- a/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst +++ b/Misc/NEWS.d/next/Library/2026-02-23-00-58-24.gh-issue-50571.jZIR3T.rst @@ -1,3 +1,3 @@ -:mod:`bdb` will reports an error when set breakpoint on a line with no +:mod:`bdb` will report an error when setting breakpoint on a line with no associated bytecode, such as :keyword:`global` or :keyword:`nonlocal` statements. From 2c291f0c5c7c80070f16d512ad2def0cd3ab7a1c Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 23:30:54 +0900 Subject: [PATCH 06/11] Update comments in test_bdb.py --- Lib/test/test_bdb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_bdb.py b/Lib/test/test_bdb.py index b04dee795b320d..caa26076859df8 100644 --- a/Lib/test/test_bdb.py +++ b/Lib/test/test_bdb.py @@ -976,6 +976,9 @@ def test_load_bps_from_previous_Bdb_instance(self): reset_Breakpoint() db1 = Bdb() fname = db1.canonic(__file__) + # These line numbers are sensitive to this test file itself. + # They must have associated bytecode, so update them if the file header + # changes. db1.set_break(__file__, 51) self.assertEqual(db1.get_all_breaks(), {fname: [51]}) From c4d4d7d0def0818de5c6be037c15169d5d0148a5 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 23:31:16 +0900 Subject: [PATCH 07/11] Check code object by isinstance --- Lib/bdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index a1090124473634..9d136f96e9c753 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -7,6 +7,7 @@ import weakref from contextlib import contextmanager, suppress from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR +from types import CodeType __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -183,7 +184,7 @@ def _get_executable_linenos(code): if lineno is not None: linenos.add(lineno) for const in code.co_consts: - if hasattr(const, 'co_lines'): + if isinstance(const, CodeType): linenos |= _get_executable_linenos(const) return linenos From 026f62a8b557d055a29034bbe17359d38bdf5a12 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 23 Feb 2026 23:47:12 +0900 Subject: [PATCH 08/11] Add _executable_linenos_cache --- Lib/bdb.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 9d136f96e9c753..8ceced3fc95416 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -189,6 +189,9 @@ def _get_executable_linenos(code): return linenos +_executable_linenos_cache = {} + + class Bdb: """Generic Python debugger base class. @@ -683,13 +686,15 @@ def set_break(self, filename, lineno, temporary=False, cond=None, line = linecache.getline(filename, lineno) if not line: return 'Line %s:%d does not exist' % (filename, lineno) - source = ''.join(linecache.getlines(filename)) - if source: - with suppress(SyntaxError): - code = compile(source, filename, 'exec') - executable_lines = _get_executable_linenos(code) - if executable_lines and lineno not in executable_lines: - return 'Line %d has no code associated with it' % lineno + if filename not in _executable_linenos_cache: + source = ''.join(linecache.getlines(filename)) + if source: + with suppress(SyntaxError): + code = compile(source, filename, 'exec') + _executable_linenos_cache[filename] = _get_executable_linenos(code) + executable_lines = _executable_linenos_cache.get(filename) + if executable_lines and lineno not in executable_lines: + return 'Line %d has no code associated with it' % lineno self._add_to_breaks(filename, lineno) bp = Breakpoint(filename, lineno, temporary, cond, funcname) # After we set a new breakpoint, we need to search through all frames From 0c0f4fdadb87bc131cc404b6cf0e318841c0696f Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 24 Feb 2026 00:13:33 +0900 Subject: [PATCH 09/11] Move the _executable_linenos_cache to Bdb instance --- Lib/bdb.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 8ceced3fc95416..e373490c2b8036 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -189,8 +189,6 @@ def _get_executable_linenos(code): return linenos -_executable_linenos_cache = {} - class Bdb: """Generic Python debugger base class. @@ -210,6 +208,7 @@ def __init__(self, skip=None, backend='settrace'): self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} + self._executable_linenos_cache = {} self.frame_trace_lines_opcodes = {} self.frame_returning = None self.trace_opcodes = False @@ -686,13 +685,13 @@ def set_break(self, filename, lineno, temporary=False, cond=None, line = linecache.getline(filename, lineno) if not line: return 'Line %s:%d does not exist' % (filename, lineno) - if filename not in _executable_linenos_cache: + if filename not in self._executable_linenos_cache: source = ''.join(linecache.getlines(filename)) if source: with suppress(SyntaxError): code = compile(source, filename, 'exec') - _executable_linenos_cache[filename] = _get_executable_linenos(code) - executable_lines = _executable_linenos_cache.get(filename) + self._executable_linenos_cache[filename] = _get_executable_linenos(code) + executable_lines = self._executable_linenos_cache.get(filename) if executable_lines and lineno not in executable_lines: return 'Line %d has no code associated with it' % lineno self._add_to_breaks(filename, lineno) From 346eef276fffd06c85510a865142f9aa714b2b7c Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 24 Feb 2026 00:46:20 +0900 Subject: [PATCH 10/11] Fix code style --- Lib/bdb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index e373490c2b8036..976d048fc5fbc4 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -189,7 +189,6 @@ def _get_executable_linenos(code): return linenos - class Bdb: """Generic Python debugger base class. From eaf71ac4f57ed9ae7b91855bef4471798cf0ac9b Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 24 Feb 2026 00:47:45 +0900 Subject: [PATCH 11/11] Remove the _ prefix for executable_linenos_cache --- Lib/bdb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 976d048fc5fbc4..f8d39a690b95de 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -207,7 +207,7 @@ def __init__(self, skip=None, backend='settrace'): self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} - self._executable_linenos_cache = {} + self.executable_linenos_cache = {} self.frame_trace_lines_opcodes = {} self.frame_returning = None self.trace_opcodes = False @@ -684,13 +684,13 @@ def set_break(self, filename, lineno, temporary=False, cond=None, line = linecache.getline(filename, lineno) if not line: return 'Line %s:%d does not exist' % (filename, lineno) - if filename not in self._executable_linenos_cache: + if filename not in self.executable_linenos_cache: source = ''.join(linecache.getlines(filename)) if source: with suppress(SyntaxError): code = compile(source, filename, 'exec') - self._executable_linenos_cache[filename] = _get_executable_linenos(code) - executable_lines = self._executable_linenos_cache.get(filename) + self.executable_linenos_cache[filename] = _get_executable_linenos(code) + executable_lines = self.executable_linenos_cache.get(filename) if executable_lines and lineno not in executable_lines: return 'Line %d has no code associated with it' % lineno self._add_to_breaks(filename, lineno)