| --- a/Lib/bdb.py |
| +++ b/Lib/bdb.py |
| @@ -3,12 +3,469 @@ |
| import fnmatch |
| import sys |
| import os |
| +import inspect |
| +import linecache |
| +import token |
| +import tokenize |
| +import imp |
| +import importlib |
| +import pprint |
| +from operator import itemgetter, attrgetter |
| __all__ = ["BdbQuit", "Bdb", "Breakpoint"] |
| +# A dictionary mapping a filename to a ModuleSource instance. |
| +_modules = {} |
| +_fncache = {} |
| + |
| +def canonic(filename): |
| + if filename == "<" + filename[1:-1] + ">": |
| + return filename |
| + canonic = _fncache.get(filename) |
| + if not canonic: |
| + canonic = os.path.abspath(filename) |
| + canonic = os.path.normcase(canonic) |
| + _fncache[filename] = canonic |
| + return canonic |
| + |
| +def getfilename(module_name, path=None, inpackage=None): |
| + """Return the file name of module_name.""" |
| + if module_name in sys.modules: |
| + return getattr(sys.modules[module_name], '__file__', None) |
| + |
| + if inpackage is not None: |
| + fullmodule = '{}.{}'.format(inpackage, module_name) |
| + else: |
| + fullmodule = module_name |
| + |
| + i = module_name.rfind('.') |
| + if i >= 0: |
| + package = module_name[:i] |
| + submodule = module_name[i+1:] |
| + parent = getfilename(package, path, inpackage) |
| + if parent is None: |
| + return None |
| + if inpackage is not None: |
| + package = '{}.{}'.format(inpackage, package) |
| + return getfilename(submodule, [os.path.dirname(parent)], package) |
| + |
| + if inpackage is not None: |
| + search_path = path |
| + else: |
| + search_path = sys.path |
| + try: |
| + loader = importlib.find_loader(fullmodule, search_path) |
| + return loader.get_filename(fullmodule) |
| + except (AttributeError, ImportError): |
| + return None |
| + |
| +def funcname_breakpoint(funcname, filename=None, frame=None): |
| + """Return the filename and the line of the first statement of funcname. |
| + |
| + Fail with BdbError when filename and line cannot be found. |
| + """ |
| + if not filename and not frame: |
| + raise BdbError('Error at funcname_breakpoint: invalid arguments.') |
| + |
| + if filename: |
| + filename = canonic(filename) |
| + if filename not in _modules: |
| + _modules[filename] = ModuleSource(filename) |
| + module_src = _modules[filename] |
| + func_lno = module_src.get_func_lno(funcname) |
| + lineno = module_src.get_actual_bp(func_lno)[1] |
| + if not lineno: |
| + raise BdbError('Bad function name: "{}".'.format(funcname)) |
| + return filename, lineno |
| + |
| + # frame is not None |
| + try: |
| + func = eval(funcname, frame.f_globals) |
| + except: |
| + # funcname is defined in a module not yet (fully) imported |
| + module = inspect.getmodule(frame) |
| + filename, lineno = FunctionQualifiedName(funcname, |
| + module).get_fileline() |
| + if not filename: |
| + raise BdbError('Bad name: "{}".'.format(funcname)) |
| + return filename, lineno |
| + else: |
| + try: |
| + filename = inspect.getfile(func) |
| + except TypeError: |
| + raise BdbError('Cannot set a breakpoint at "{}"'.format(funcname)) |
| + return funcname_breakpoint(funcname, filename) |
| + |
| + |
| +class BdbError(Exception): |
| + """Generic bdb exception.""" |
| + |
| class BdbQuit(Exception): |
| """Exception to give up completely.""" |
| +class LineSet: |
| + """A group of consecutive lines. |
| + |
| + LineSet is defined by (start, last, ltype, func_lno) where start is the |
| + first line number of the lineset, last the last line number, ltype its type |
| + and func_lno is the first line number of the function definition that the |
| + LineSet belongs to or zero when the group of lines is defined outside a |
| + function. |
| + |
| + """ |
| + |
| + # A LineSet type: a function definition possibly spanning multiple physical |
| + # lines, or a group of consecutive lines, or a logical line that spans |
| + # multiple physical lines. |
| + LINE_DEF, LINE_GROUP, LINE_MULTI = tuple(range(3)) |
| + |
| + def __init__(self): |
| + self.reset() |
| + |
| + def reset(self, start=0, ltype=LINE_GROUP, func_lno=0, last=0): |
| + self.start = start |
| + self.last = last |
| + self.ltype = ltype |
| + self.func_lno = func_lno |
| + |
| + def add(self, module): |
| + """Add the lineset to the ModuleSource instance.""" |
| + if self.start and self.last: |
| + lines = (module.functions_lines |
| + if self.func_lno else module.module_lines) |
| + lines.append((self.start, self.last, self.ltype, self.func_lno)) |
| + self.reset(func_lno=self.func_lno) |
| + |
| +class ModuleSource: |
| + """A parsed module source. |
| + |
| + Instance attributes: |
| + function_lineno: a dictionary mapping function and method names to the |
| + first line number of their definition |
| + firstlineno: a dictionary mapping the first line number of a callable to: |
| + - the first line number of the enclosing function, when the |
| + callable is nested in a function definition |
| + - zero (by convention, zero is the module first line number for |
| + breakpoints set at module level), when a class not nested in a |
| + function definition |
| + - the first line number of the callable otherwise |
| + last_line: the last line number of the module |
| + module_lines: a list of the module LineSet tuples |
| + functions_lines: a list of the functions LineSet tuples |
| + """ |
| + |
| + def __init__(self, filename): |
| + self.filename = filename |
| + self.stat = self.get_stat() |
| + self.parse() |
| + |
| + def reset(self): |
| + """Update ModuleSource after the file has been modified.""" |
| + stat = self.get_stat() |
| + if not self.stat or not stat: |
| + return |
| + if (self.stat.st_size == stat.st_size and |
| + self.stat.st_mtime == stat.st_mtime): |
| + return |
| + self.stat = stat |
| + |
| + # The file has been modified, attempt to reload its module. |
| + for module in sys.modules.values(): |
| + fname = getattr(module, '__file__', None) |
| + if fname and canonic(fname) == self.filename: |
| + try: |
| + imp.reload(module) |
| + except: |
| + pass |
| + break |
| + |
| + self.parse() |
| + |
| + def get_stat(self): |
| + try: |
| + return os.stat(self.filename) |
| + except os.error: |
| + return None |
| + |
| + def get_func_lno(self, funcname): |
| + """Return the first line number of function funcname.""" |
| + return self.function_lineno.get(funcname) |
| + |
| + def get_actual_bp(self, lineno, lines=None): |
| + """Return the actual breakpoint as (func_lno, actual_lno). |
| + |
| + func_lno: first line of the corresponding function definition or zero |
| + when the breakpoint would be set outside a function. |
| + actual_lno: line where the debugger would stop. |
| + """ |
| + if (not lineno or lineno < 0 or |
| + not self.last_line or lineno > self.last_line): |
| + return None, None |
| + |
| + if lines is None: |
| + lines = self.functions_lines |
| + i = 0 |
| + length = j = len(lines) |
| + # Find the index of the first lineset whose start is the first to be |
| + # strictly greater than lineno. |
| + while i < j: |
| + m = (i + j) // 2 |
| + if lineno < lines[m][0]: |
| + j = m |
| + else: |
| + i = m + 1 |
| + |
| + if i != 0: |
| + ltype = lines[i-1][2] |
| + # lineno inside previous LINE_GROUP or LINE_MULTI |
| + if lineno <= lines[i-1][1]: |
| + if ltype == LineSet.LINE_GROUP: |
| + return lines[i-1][3], lineno |
| + elif ltype == LineSet.LINE_MULTI: |
| + return lines[i-1][3], lines[i-1][0] |
| + if lines is self.functions_lines: |
| + if i != length: |
| + ltype = lines[i][2] |
| + # First statement of next group if not LINE_DEF |
| + if ltype != LineSet.LINE_DEF: |
| + return lines[i][3], lines[i][0] |
| + # Outside a function definition |
| + return self.get_actual_bp(lineno, self.module_lines) |
| + |
| + # A comment line, string statement line or empty line outside a |
| + # function definition. |
| + return None, lineno |
| + |
| + def _parse(self, tok_generator, lineset=LineSet(), cindent=0, clss=None): |
| + func_lno = 0 |
| + prev_tokentype = token.ENDMARKER |
| + try: |
| + for tokentype, tok, srowcol, _end, _line in tok_generator: |
| + if tokentype == token.DEDENT: |
| + # End of function definition |
| + if func_lno and srowcol[1] <= indent: |
| + lineset.add(self) |
| + func_lno = 0 |
| + # End of class definition |
| + if clss and srowcol[1] <= cindent: |
| + return |
| + elif tokentype == tokenize.NL: |
| + if lineset.start and lineset.ltype != lineset.LINE_DEF: |
| + lineset.add(self) |
| + # Start of a logical line with multiple physical lines |
| + if (lineset.ltype != lineset.LINE_MULTI and |
| + prev_tokentype != tokenize.NEWLINE and |
| + prev_tokentype != tokenize.COMMENT): |
| + lineset.reset(srowcol[0], lineset.LINE_MULTI, |
| + func_lno) |
| + elif tokentype == tokenize.NEWLINE: |
| + lineset.last = srowcol[0] |
| + if lineset.start: |
| + if (lineset.ltype == lineset.LINE_DEF or |
| + lineset.ltype == lineset.LINE_MULTI): |
| + lineset.add(self) |
| + elif tokentype == token.ENDMARKER: |
| + lineset.add(self) |
| + self.last_line = srowcol[0] - 1 |
| + elif tok == 'def' or tok == 'class': |
| + tokentype, name = next(tok_generator)[0:2] |
| + if tokentype != token.NAME: |
| + continue # syntax error |
| + # Nested def or class in a function |
| + if func_lno: |
| + self.firstlineno[srowcol[0]] = func_lno |
| + prev_tokentype = tokentype |
| + continue |
| + if clss: |
| + name = '{}.{}'.format(clss, name) |
| + lineno, indent = srowcol |
| + if tok == 'def': |
| + lineset.add(self) |
| + func_lno = lineno |
| + # Start of a function definition |
| + lineset.reset(lineno, lineset.LINE_DEF, func_lno) |
| + self.function_lineno[name] = lineno |
| + self.firstlineno[lineno] = lineno |
| + else: |
| + self.firstlineno[lineno] = 0 |
| + self._parse(tok_generator, lineset, indent, name) |
| + elif (not lineset.start and tokentype != tokenize.COMMENT and |
| + tokentype != token.INDENT and |
| + tokentype != token.STRING): |
| + # Start of a group of lines (possibly LINE_MULTI) |
| + lineset.reset(srowcol[0], lineset.LINE_GROUP, func_lno) |
| + |
| + prev_tokentype = tokentype |
| + except StopIteration: |
| + pass |
| + |
| + def parse(self): |
| + self.function_lineno = {} |
| + self.firstlineno = {} |
| + self.last_line = 0 |
| + self.module_lines = [] |
| + self.functions_lines = [] |
| + |
| + source_lines = linecache.getlines(self.filename) |
| + if source_lines: |
| + self._parse(tokenize.generate_tokens(iter(source_lines).__next__)) |
| + |
| + def __str__(self): |
| + def lines_per_func(lines): |
| + # Group lines per function |
| + func_lno = 0 |
| + group = [] |
| + for lineset in lines: |
| + if lineset[3] != func_lno: |
| + if group: |
| + yield pprint.pformat(group) |
| + func_lno = lineset[3] |
| + group = [lineset] |
| + else: |
| + group.append(lineset) |
| + if group: |
| + yield pprint.pformat(group) |
| + |
| + return ( |
| + 'Functions:\n{}\n' |
| + 'Functions and classes firstlineno:\n{}\n' |
| + 'Last line: {}\n' |
| + 'Module lines:\n{}\n' |
| + 'Functions lines:\n{}\n'.format( |
| + pprint.pformat( |
| + sorted(self.function_lineno.items(), key=itemgetter(1))), |
| + pprint.pformat( |
| + sorted(self.firstlineno.items(), key=itemgetter(0))), |
| + self.last_line, |
| + pprint.pformat(sorted(self.module_lines, key=itemgetter(0))), |
| + '\n'.join(lines_per_func(self.functions_lines)))) |
| + |
| +class ModuleBreakpoints: |
| + """The breakpoints of a module. |
| + |
| + The breakpts attribute is a dictionary that maps the first line of a |
| + function definition (or zero for all the lines outside a function |
| + definition) to a line_bps dictionary that maps each line of the function, |
| + where one or more breakpoints are set, to the list of corresponding |
| + Breakpoint instances. |
| + |
| + Note: |
| + A line in line_bps is the actual line of the breakpoint (the line where the |
| + debugger stops), this line may differ from the line attribute of the |
| + Breakpoint instance as set by the user. |
| + """ |
| + |
| + def __init__(self, filename): |
| + assert filename, ('Attempt to instantiate ModuleBreakpoints' |
| + ' with {}'.format(filename)) |
| + if filename not in _modules: |
| + _modules[filename] = ModuleSource(filename) |
| + self.module_src = _modules[filename] |
| + self.stat = self.module_src.stat |
| + self.breakpts = {} |
| + |
| + def reset(self): |
| + self.module_src.reset() |
| + stat = self.module_src.stat |
| + if not self.stat or not stat: |
| + return |
| + if (self.stat.st_size == stat.st_size and |
| + self.stat.st_mtime == stat.st_mtime): |
| + return |
| + |
| + # The file has been modified |
| + self.stat = stat |
| + bplist = self.all_breakpoints() |
| + self.breakpts = {} |
| + for bp in bplist: |
| + self.add_breakpoint(bp) |
| + |
| + def add_breakpoint(self, bp): |
| + func_lno, actual_lno = self.module_src.get_actual_bp(bp.line) |
| + # May happen after the file has been modified and a reset. |
| + if func_lno is None or not actual_lno: |
| + return |
| + if func_lno not in self.breakpts: |
| + self.breakpts[func_lno] = {} |
| + line_bps = self.breakpts[func_lno] |
| + if actual_lno not in line_bps: |
| + line_bps[actual_lno] = [] |
| + line_bps[actual_lno].append(bp) |
| + |
| + def delete_breakpoint(self, bp): |
| + func_lno, actual_lno = self.module_src.get_actual_bp(bp.line) |
| + # May happen after the file has been modified and a reset. |
| + if func_lno is None or not actual_lno: |
| + return |
| + try: |
| + line_bps = self.breakpts[func_lno] |
| + bplist = line_bps[actual_lno] |
| + bplist.remove(bp) |
| + except (KeyError, ValueError): |
| + assert False, ('Internal error: bpbynumber and breakpts' |
| + ' are inconsistent') |
| + if not bplist: |
| + del line_bps[actual_lno] |
| + if not line_bps: |
| + del self.breakpts[func_lno] |
| + |
| + def get_breakpoints(self, lineno): |
| + """Return the list of breakpoints set at lineno.""" |
| + func_lno, actual_lno = self.module_src.get_actual_bp(lineno) |
| + if func_lno not in self.breakpts: |
| + return [] |
| + line_bps = self.breakpts[func_lno] |
| + if actual_lno not in line_bps: |
| + return [] |
| + return [bp for bp in sorted(line_bps[actual_lno], |
| + key=attrgetter('number')) if bp.line == lineno] |
| + |
| + def all_breakpoints(self): |
| + bpts = [] |
| + for line_bps in self.breakpts.values(): |
| + for bplist in line_bps.values(): |
| + bpts.extend(bplist) |
| + return [bp for bp in sorted(bpts, key=attrgetter('number'))] |
| + |
| +class FunctionQualifiedName: |
| + """Utility to find where is defined a function from its qualified name.""" |
| + |
| + def __init__(self, name, module=None): |
| + self.name = name |
| + self.cur_fname = None |
| + if module: |
| + self.cur_fname = getfilename(module.__name__) |
| + self.fileline = None, None |
| + |
| + def get_fileline(self): |
| + i = self.name.rfind('.') |
| + if i >= 0: |
| + funcname = self.name[i+1:] |
| + prefix = self.name[:i] |
| + j = prefix.rfind('.') |
| + # Try first the current module for a class method, then a function |
| + # in prefix and last a method in prefix[:j]. |
| + if (not (prefix and j < 0 and |
| + self.lookup(self.cur_fname, self.name)) and |
| + not self.lookup(getfilename(prefix), funcname) and |
| + j > 0): |
| + method = '{}.{}'.format(prefix[j+1:], funcname) |
| + prefix = prefix[:j] |
| + self.lookup(getfilename(prefix), method) |
| + else: |
| + # a function in the current module |
| + self.lookup(self.cur_fname, self.name) |
| + |
| + return self.fileline |
| + |
| + def lookup(self, filename, funcname): |
| + if filename: |
| + try: |
| + self.fileline = funcname_breakpoint(funcname, filename) |
| + return True |
| + except BdbError: |
| + pass |
| + return False |
| class Bdb: |
| """Generic Python debugger base class. |
| @@ -20,25 +477,41 @@ |
| def __init__(self, skip=None): |
| self.skip = set(skip) if skip else None |
| - self.breaks = {} |
| - self.fncache = {} |
| self.frame_returning = None |
| + # A dictionary mapping a filename to a ModuleBreakpoints instance. |
| + self.breakpoints = {} |
| + # Backward compatibility |
| def canonic(self, filename): |
| - if filename == "<" + filename[1:-1] + ">": |
| - return filename |
| - canonic = self.fncache.get(filename) |
| - if not canonic: |
| - canonic = os.path.abspath(filename) |
| - canonic = os.path.normcase(canonic) |
| - self.fncache[filename] = canonic |
| - return canonic |
| + return canonic(filename) |
| def reset(self): |
| - import linecache |
| + # A dictionary mapping a code object to the first line of the enclosing |
| + # non-nested function. |
| + self.code_firstlineno = {} |
| linecache.checkcache() |
| self.botframe = None |
| self._set_stopinfo(None, None) |
| + for module_src in _modules.values(): |
| + module_src.reset() |
| + for module_bpts in self.breakpoints.values(): |
| + module_bpts.reset() |
| + |
| + def firstlineno(self, frame): |
| + """Return the first line of the enclosing non-nested function.""" |
| + lineno = frame.f_code.co_firstlineno |
| + if lineno == 1 and frame.f_code.co_name == '<module>': |
| + return 0 |
| + filename = canonic(frame.f_code.co_filename) |
| + if filename not in _modules: |
| + _modules[filename] = ModuleSource(filename) |
| + module_src = _modules[filename] |
| + return module_src.firstlineno.get(lineno, lineno) |
| + |
| + def _set_local_trace(self, frame): |
| + frame.f_trace = self.trace_dispatch |
| + if frame.f_code not in self.code_firstlineno: |
| + self.code_firstlineno[frame.f_code] = self.firstlineno(frame) |
| def trace_dispatch(self, frame, event, arg): |
| if self.quitting: |
| @@ -68,11 +541,13 @@ |
| def dispatch_call(self, frame, arg): |
| # XXX 'arg' is no longer used |
| + if frame.f_code not in self.code_firstlineno: |
| + self.code_firstlineno[frame.f_code] = self.firstlineno(frame) |
| if self.botframe is None: |
| # First call of dispatch since reset() |
| self.botframe = frame.f_back # (CT) Note that this may also be None! |
| return self.trace_dispatch |
| - if not (self.stop_here(frame) or self.break_anywhere(frame)): |
| + if not (self.stop_here(frame) or self.break_at_function(frame)): |
| # No need to trace this function |
| return # None |
| self.user_call(frame, arg) |
| @@ -122,32 +597,33 @@ |
| return False |
| def break_here(self, frame): |
| - filename = self.canonic(frame.f_code.co_filename) |
| - if filename not in self.breaks: |
| + filename = canonic(frame.f_code.co_filename) |
| + if filename not in self.breakpoints: |
| return False |
| - lineno = frame.f_lineno |
| - if lineno not in self.breaks[filename]: |
| - # The line itself has no breakpoint, but maybe the line is the |
| - # first line of a function with breakpoint set by function name. |
| - lineno = frame.f_code.co_firstlineno |
| - if lineno not in self.breaks[filename]: |
| - return False |
| + func_lno = self.code_firstlineno[frame.f_code] |
| + module_bpts = self.breakpoints[filename] |
| + if (func_lno not in module_bpts.breakpts or |
| + frame.f_lineno not in module_bpts.breakpts[func_lno]): |
| + return False |
| - # flag says ok to delete temp. bp |
| - (bp, flag) = effective(filename, lineno, frame) |
| - if bp: |
| - self.currentbp = bp.number |
| - if (flag and bp.temporary): |
| - self.do_clear(str(bp.number)) |
| - return True |
| - else: |
| - return False |
| + # Handle multiple breakpoints on the same line (issue 14789) |
| + self.effective_bp_list = [] |
| + for bp in module_bpts.breakpts[func_lno][frame.f_lineno]: |
| + stop, delete = bp.process_hit_event(frame) |
| + if stop: |
| + self.effective_bp_list.append(bp.number) |
| + if bp.temporary and delete: |
| + self.do_clear(str(bp.number)) |
| + return len(self.effective_bp_list) != 0 |
| def do_clear(self, arg): |
| raise NotImplementedError("subclass of bdb must implement do_clear()") |
| - def break_anywhere(self, frame): |
| - return self.canonic(frame.f_code.co_filename) in self.breaks |
| + def break_at_function(self, frame): |
| + filename = canonic(frame.f_code.co_filename) |
| + return (filename in self.breakpoints and |
| + self.code_firstlineno[frame.f_code] in |
| + self.breakpoints[filename].breakpts) |
| # Derived classes should override the user_* methods |
| # to gain control. |
| @@ -198,7 +674,7 @@ |
| if self.frame_returning: |
| caller_frame = self.frame_returning.f_back |
| if caller_frame and not caller_frame.f_trace: |
| - caller_frame.f_trace = self.trace_dispatch |
| + self._set_local_trace(caller_frame) |
| self._set_stopinfo(None, None) |
| def set_next(self, frame): |
| @@ -218,7 +694,7 @@ |
| frame = sys._getframe().f_back |
| self.reset() |
| while frame: |
| - frame.f_trace = self.trace_dispatch |
| + self._set_local_trace(frame) |
| self.botframe = frame |
| frame = frame.f_back |
| self.set_step() |
| @@ -227,7 +703,7 @@ |
| def set_continue(self): |
| # Don't stop except at breakpoints or when finished |
| self._set_stopinfo(self.botframe, None, -1) |
| - if not self.breaks: |
| + if not self.has_breaks(): |
| # no breakpoints; run without debugger overhead |
| sys.settrace(None) |
| frame = sys._getframe().f_back |
| @@ -244,39 +720,36 @@ |
| # Derived classes and clients can call the following methods |
| # to manipulate breakpoints. These methods return an |
| # error message is something went wrong, None if all is well. |
| - # Set_break prints out the breakpoint line and file:lineno. |
| # Call self.get_*break*() to see the breakpoints or better |
| # for bp in Breakpoint.bpbynumber: if bp: bp.bpprint(). |
| - def set_break(self, filename, lineno, temporary=False, cond=None, |
| - funcname=None): |
| - filename = self.canonic(filename) |
| - import linecache # Import as late as possible |
| - line = linecache.getline(filename, lineno) |
| - if not line: |
| - return 'Line %s:%d does not exist' % (filename, lineno) |
| - list = self.breaks.setdefault(filename, []) |
| - if lineno not in list: |
| - list.append(lineno) |
| - bp = Breakpoint(filename, lineno, temporary, cond, funcname) |
| - |
| - def _prune_breaks(self, filename, lineno): |
| - if (filename, lineno) not in Breakpoint.bplist: |
| - self.breaks[filename].remove(lineno) |
| - if not self.breaks[filename]: |
| - del self.breaks[filename] |
| + def set_break(self, fname, lineno, temporary=False, cond=None, |
| + funcname=None): |
| + # funcname is not used anymore and kept for backward compatibility |
| + filename = canonic(fname) |
| + if filename not in self.breakpoints: |
| + module_bps = ModuleBreakpoints(filename) |
| + module_src = module_bps.module_src |
| + if not module_src.functions_lines and not module_src.module_lines: |
| + return 'No lines in {}'.format(fname) |
| + self.breakpoints[filename] = module_bps |
| + else: |
| + module_bps = self.breakpoints[filename] |
| + func_lno, actual_lno = module_bps.module_src.get_actual_bp(lineno) |
| + if func_lno is None: |
| + if not actual_lno: |
| + return 'Line {}:{} does not exist'.format(fname, lineno) |
| + return ('A comment line, string statement line or empty line' |
| + ' outside a function definition {}:{}' |
| + .format(fname, lineno)) |
| + bp = Breakpoint(filename, lineno, module_bps, temporary, cond) |
| def clear_break(self, filename, lineno): |
| - filename = self.canonic(filename) |
| - if filename not in self.breaks: |
| - return 'There are no breakpoints in %s' % filename |
| - if lineno not in self.breaks[filename]: |
| + bplist = self.get_breaks(filename, lineno) |
| + if not bplist: |
| return 'There is no breakpoint at %s:%d' % (filename, lineno) |
| - # If there's only one bp in the list for that file,line |
| - # pair, then remove the breaks entry |
| - for bp in Breakpoint.bplist[filename, lineno][:]: |
| + for bp in bplist: |
| bp.deleteMe() |
| - self._prune_breaks(filename, lineno) |
| def clear_bpbynumber(self, arg): |
| try: |
| @@ -284,25 +757,21 @@ |
| except ValueError as err: |
| return str(err) |
| bp.deleteMe() |
| - self._prune_breaks(bp.file, bp.line) |
| - def clear_all_file_breaks(self, filename): |
| - filename = self.canonic(filename) |
| - if filename not in self.breaks: |
| - return 'There are no breakpoints in %s' % filename |
| - for line in self.breaks[filename]: |
| - blist = Breakpoint.bplist[filename, line] |
| - for bp in blist: |
| - bp.deleteMe() |
| - del self.breaks[filename] |
| + def clear_all_file_breaks(self, fname): |
| + filename = canonic(fname) |
| + if (filename not in self.breakpoints or not |
| + self.breakpoints[filename].breakpts.keys()): |
| + return 'There are no breakpoints in %s' % fname |
| + for bp in self.breakpoints[filename].all_breakpoints(): |
| + bp.deleteMe() |
| def clear_all_breaks(self): |
| - if not self.breaks: |
| + if not self.has_breaks(): |
| return 'There are no breakpoints' |
| for bp in Breakpoint.bpbynumber: |
| if bp: |
| bp.deleteMe() |
| - self.breaks = {} |
| def get_bpbynumber(self, arg): |
| if not arg: |
| @@ -320,25 +789,31 @@ |
| return bp |
| def get_break(self, filename, lineno): |
| - filename = self.canonic(filename) |
| - return filename in self.breaks and \ |
| - lineno in self.breaks[filename] |
| + return len(self.get_breaks(filename, lineno)) != 0 |
| def get_breaks(self, filename, lineno): |
| - filename = self.canonic(filename) |
| - return filename in self.breaks and \ |
| - lineno in self.breaks[filename] and \ |
| - Breakpoint.bplist[filename, lineno] or [] |
| + filename = canonic(filename) |
| + if filename in self.breakpoints: |
| + return self.breakpoints[filename].get_breakpoints(lineno) |
| + return [] |
| def get_file_breaks(self, filename): |
| - filename = self.canonic(filename) |
| - if filename in self.breaks: |
| - return self.breaks[filename] |
| - else: |
| + filename = canonic(filename) |
| + if filename not in self.breakpoints: |
| return [] |
| + return [bp.line for bp in self.breakpoints[filename].all_breakpoints()] |
| def get_all_breaks(self): |
| - return self.breaks |
| + breaks = {} |
| + for filename in self.breakpoints: |
| + linebp_list = self.get_file_breaks(filename) |
| + if linebp_list: |
| + breaks[filename] = self.get_file_breaks(filename) |
| + return breaks |
| + |
| + def has_breaks(self): |
| + return any(self.breakpoints[f].breakpts.keys() |
| + for f in self.breakpoints) |
| # Derived classes and clients can call the following method |
| # to get a data structure representing a stack trace. |
| @@ -362,9 +837,9 @@ |
| return stack, i |
| def format_stack_entry(self, frame_lineno, lprefix=': '): |
| - import linecache, reprlib |
| + import reprlib |
| frame, lineno = frame_lineno |
| - filename = self.canonic(frame.f_code.co_filename) |
| + filename = canonic(frame.f_code.co_filename) |
| s = '%s(%r)' % (filename, lineno) |
| if frame.f_code.co_name: |
| s += frame.f_code.co_name |
| @@ -455,29 +930,18 @@ |
| Implements temporary breakpoints, ignore counts, disabling and |
| (re)-enabling, and conditionals. |
| - Breakpoints are indexed by number through bpbynumber and by |
| - the file,line tuple using bplist. The former points to a |
| - single instance of class Breakpoint. The latter points to a |
| - list of such instances since there may be more than one |
| - breakpoint per line. |
| + Breakpoints are indexed by number through bpbynumber. |
| """ |
| - # XXX Keeping state in the class is a mistake -- this means |
| - # you cannot have more than one active Bdb instance. |
| + next = 1 # Next bp to be assigned |
| + bpbynumber = [None] # Each entry is None or an instance of Bpt |
| - next = 1 # Next bp to be assigned |
| - bplist = {} # indexed by (file, lineno) tuple |
| - bpbynumber = [None] # Each entry is None or an instance of Bpt |
| - # index 0 is unused, except for marking an |
| - # effective break .... see effective() |
| - |
| - def __init__(self, file, line, temporary=False, cond=None, funcname=None): |
| - self.funcname = funcname |
| - # Needed if funcname is not None. |
| - self.func_first_executable_line = None |
| + def __init__(self, file, line, module, temporary=False, |
| + cond=None): |
| self.file = file # This better be in canonical form! |
| self.line = line |
| + self.module = module |
| self.temporary = temporary |
| self.cond = cond |
| self.enabled = True |
| @@ -485,20 +949,13 @@ |
| self.hits = 0 |
| self.number = Breakpoint.next |
| Breakpoint.next += 1 |
| - # Build the two lists |
| self.bpbynumber.append(self) |
| - if (file, line) in self.bplist: |
| - self.bplist[file, line].append(self) |
| - else: |
| - self.bplist[file, line] = [self] |
| + self.module.add_breakpoint(self) |
| def deleteMe(self): |
| - index = (self.file, self.line) |
| - self.bpbynumber[self.number] = None # No longer in list |
| - self.bplist[index].remove(self) |
| - if not self.bplist[index]: |
| - # No more bp for this f:l combo |
| - del self.bplist[index] |
| + if self.bpbynumber[self.number]: |
| + self.bpbynumber[self.number] = None # No longer in list |
| + self.module.delete_breakpoint(self) |
| def enable(self): |
| self.enabled = True |
| @@ -506,6 +963,27 @@ |
| def disable(self): |
| self.enabled = False |
| + def process_hit_event(self, frame): |
| + """Return (stop_state, delete_temporary) at a breakpoint hit event.""" |
| + if not self.enabled: |
| + return False, False |
| + # Count every hit when breakpoint is enabled. |
| + self.hits += 1 |
| + # A conditional breakpoint. |
| + if self.cond: |
| + try: |
| + if not eval(self.cond, frame.f_globals, frame.f_locals): |
| + return False, False |
| + except: |
| + # If the breakpoint condition evaluation fails, the most |
| + # conservative thing is to stop on the breakpoint. Don't |
| + # delete temporary, as another hint to the user. |
| + return True, False |
| + if self.ignore > 0: |
| + self.ignore -= 1 |
| + return False, False |
| + return True, True |
| + |
| def bpprint(self, out=None): |
| if out is None: |
| out = sys.stdout |
| @@ -537,81 +1015,6 @@ |
| def __str__(self): |
| return 'breakpoint %s at %s:%s' % (self.number, self.file, self.line) |
| -# -----------end of Breakpoint class---------- |
| - |
| -def checkfuncname(b, frame): |
| - """Check whether we should break here because of `b.funcname`.""" |
| - if not b.funcname: |
| - # Breakpoint was set via line number. |
| - if b.line != frame.f_lineno: |
| - # Breakpoint was set at a line with a def statement and the function |
| - # defined is called: don't break. |
| - return False |
| - return True |
| - |
| - # Breakpoint set via function name. |
| - |
| - if frame.f_code.co_name != b.funcname: |
| - # It's not a function call, but rather execution of def statement. |
| - return False |
| - |
| - # We are in the right frame. |
| - if not b.func_first_executable_line: |
| - # The function is entered for the 1st time. |
| - b.func_first_executable_line = frame.f_lineno |
| - |
| - if b.func_first_executable_line != frame.f_lineno: |
| - # But we are not at the first line number: don't break. |
| - return False |
| - return True |
| - |
| -# Determines if there is an effective (active) breakpoint at this |
| -# line of code. Returns breakpoint number or 0 if none |
| -def effective(file, line, frame): |
| - """Determine which breakpoint for this file:line is to be acted upon. |
| - |
| - Called only if we know there is a bpt at this |
| - location. Returns breakpoint that was triggered and a flag |
| - that indicates if it is ok to delete a temporary bp. |
| - |
| - """ |
| - possibles = Breakpoint.bplist[file, line] |
| - for b in possibles: |
| - if not b.enabled: |
| - continue |
| - if not checkfuncname(b, frame): |
| - continue |
| - # Count every hit when bp is enabled |
| - b.hits += 1 |
| - if not b.cond: |
| - # If unconditional, and ignoring go on to next, else break |
| - if b.ignore > 0: |
| - b.ignore -= 1 |
| - continue |
| - else: |
| - # breakpoint and marker that it's ok to delete if temporary |
| - return (b, True) |
| - else: |
| - # Conditional bp. |
| - # Ignore count applies only to those bpt hits where the |
| - # condition evaluates to true. |
| - try: |
| - val = eval(b.cond, frame.f_globals, frame.f_locals) |
| - if val: |
| - if b.ignore > 0: |
| - b.ignore -= 1 |
| - # continue |
| - else: |
| - return (b, True) |
| - # else: |
| - # continue |
| - except: |
| - # if eval fails, most conservative thing is to stop on |
| - # breakpoint regardless of ignore count. Don't delete |
| - # temporary, as another hint to user. |
| - return (b, False) |
| - return (None, None) |
| - |
| # -------------------- testing -------------------- |
| @@ -621,10 +1024,9 @@ |
| if not name: name = '???' |
| print('+++ call', name, args) |
| def user_line(self, frame): |
| - import linecache |
| name = frame.f_code.co_name |
| if not name: name = '???' |
| - fn = self.canonic(frame.f_code.co_filename) |
| + fn = canonic(frame.f_code.co_filename) |
| line = linecache.getline(fn, frame.f_lineno, frame.f_globals) |
| print('+++', fn, frame.f_lineno, name, ':', line.strip()) |
| def user_return(self, frame, retval): |