Skip to content
Open
78 changes: 77 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,62 @@ 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


# filename: (size, mtime, executable_linenos, fullname)
_executable_linenos_cache = {}


def _check_executable_linenos_cache(filename=None):
if filename is None:
filenames = tuple(_executable_linenos_cache)
else:
filenames = tuple(filename)

for filename in filenames:
if (entry := _executable_linenos_cache.get(filename)) is None:
continue
size, mtime, _, fullname = entry
if mtime is None:
continue
try:
stat = os.stat(fullname)
except (OSError, ValueError):
_executable_linenos_cache.pop(filename, None)
continue
if size != stat.st_size or mtime != stat.st_mtime:
_executable_linenos_cache.pop(filename, None)


def _set_executable_linenos_cache_entry(
filename, executable_linenos, source, linecache_entry
):
if linecache_entry is not None and len(linecache_entry) != 1:
size, mtime, _, fullname = linecache_entry
else:
fullname = filename
try:
stat = os.stat(filename)
except (OSError, ValueError):
size = len(source)
mtime = None
else:
size = stat.st_size
mtime = stat.st_mtime
_executable_linenos_cache[filename] = (
size, mtime, executable_linenos, fullname
)


class Bdb:
"""Generic Python debugger base class.

Expand Down Expand Up @@ -245,6 +302,7 @@ def reset(self):
"""Set values of attributes as ready to start debugging."""
import linecache
linecache.checkcache()
_check_executable_linenos_cache()
self.botframe = None
self._set_stopinfo(None, None)

Expand Down Expand Up @@ -668,9 +726,27 @@ def set_break(self, filename, lineno, temporary=False, cond=None,
"""
filename = self.canonic(filename)
import linecache # Import as late as possible
linecache.checkcache(filename)
_check_executable_linenos_cache(filename)
line = linecache.getline(filename, lineno)
if not line:
return 'Line %s:%d does not exist' % (filename, lineno)
if filename not in _executable_linenos_cache:
source = ''.join(linecache.getlines(filename))
if source:
with suppress(SyntaxError):
code = compile(source, filename, 'exec')
executable_lines = _get_executable_linenos(code)
_set_executable_linenos_cache_entry(
filename,
executable_lines,
source,
linecache.cache.get(filename),
)
cache_entry = _executable_linenos_cache.get(filename)
executable_lines = cache_entry[2] if cache_entry else None
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