diff --git a/Lib/bdb.py b/Lib/bdb.py index 50cf2b3f5b3e45..f8d39a690b95de 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -5,8 +5,9 @@ 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 +from types import CodeType __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -177,6 +178,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 isinstance(const, CodeType): + linenos |= _get_executable_linenos(const) + return linenos + + class Bdb: """Generic Python debugger base class. @@ -195,6 +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.frame_trace_lines_opcodes = {} self.frame_returning = None self.trace_opcodes = False @@ -671,6 +684,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) + 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) + 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 diff --git a/Lib/test/test_bdb.py b/Lib/test/test_bdb.py index f15dae13eb384e..caa26076859df8 100644 --- a/Lib/test/test_bdb.py +++ b/Lib/test/test_bdb.py @@ -976,43 +976,46 @@ 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]}) + # 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]}) 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): 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 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..81984c6db440f4 --- /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 report an error when setting breakpoint on a line with no +associated bytecode, such as :keyword:`global` or :keyword:`nonlocal` +statements.