Skip to content
24 changes: 23 additions & 1 deletion Lib/bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
59 changes: 31 additions & 28 deletions Lib/test/test_bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading