diff --git a/Lib/bdb.py b/Lib/bdb.py --- 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 == '': + 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): diff --git a/Lib/pdb.py b/Lib/pdb.py --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -258,7 +258,7 @@ def user_line(self, frame): """This function is called when we stop or break at this line.""" if self._wait_for_mainpyfile: - if (self.mainpyfile != self.canonic(frame.f_code.co_filename) + if (self.mainpyfile != bdb.canonic(frame.f_code.co_filename) or frame.f_lineno <= 0): return self._wait_for_mainpyfile = False @@ -266,27 +266,38 @@ self.interaction(frame, None) def bp_commands(self, frame): - """Call every command that was set for the current active breakpoint - (if there is one). + """Call every command that was set for the current active breakpoints. Returns True if the normal interaction function must be called, False otherwise.""" - # self.currentbp is set in bdb in Bdb.break_here if a breakpoint was hit - if getattr(self, "currentbp", False) and \ - self.currentbp in self.commands: - currentbp = self.currentbp - self.currentbp = 0 - lastcmd_back = self.lastcmd - self.setup(frame, None) - for line in self.commands[currentbp]: - self.onecmd(line) - self.lastcmd = lastcmd_back - if not self.commands_silent[currentbp]: - self.print_stack_entry(self.stack[self.curindex]) - if self.commands_doprompt[currentbp]: - self._cmdloop() - self.forget() - return + # Handle multiple breakpoints on the same line (issue 14789) + # self.effective_bp_list is set in bdb in Bdb.break_here if breakpoints + # were hit + if hasattr(self, 'effective_bp_list') and self.effective_bp_list: + silent = True + doprompt = False + atleast_one_cmd = False + for bp in self.effective_bp_list: + if bp in self.commands: + atleast_one_cmd = True + lastcmd_back = self.lastcmd + self.setup(frame, None) + for line in self.commands[bp]: + self.onecmd(line) + self.lastcmd = lastcmd_back + if not self.commands_silent[bp]: + silent = False + if self.commands_doprompt[bp]: + doprompt = True + + self.effective_bp_list = [] + if atleast_one_cmd: + if not silent: + self.print_stack_entry(self.stack[self.curindex]) + if doprompt: + self._cmdloop() + self.forget() + return return 1 def user_return(self, frame, return_value): @@ -602,81 +613,69 @@ sys.path; the .py suffix may be omitted. """ if not arg: - if self.breaks: # There's at least one + if self.has_breaks(): # There's at least one. self.message("Num Type Disp Enb Where") for bp in bdb.Breakpoint.bpbynumber: if bp: self.message(bp.bpformat()) return - # parse arguments; comma has lowest precedence - # and cannot occur in filename - filename = None - lineno = None + # Parse arguments, comma has lowest precedence and cannot occur in + # filename. cond = None comma = arg.find(',') if comma > 0: # parse stuff after comma: "condition" cond = arg[comma+1:].lstrip() arg = arg[:comma].rstrip() + # parse stuff before comma: [filename:]lineno | function - colon = arg.rfind(':') - funcname = None - if colon >= 0: - filename = arg[:colon].rstrip() - f = self.lookupmodule(filename) - if not f: - self.error('%r not found from sys.path' % filename) - return - else: - filename = f - arg = arg[colon+1:].lstrip() + i = arg.rfind(':') + if i >= 0: + filename = arg[:i].strip() + if not filename: + filename = self.curframe.f_code.co_filename try: - lineno = int(arg) + lineno = int(arg[i+1:]) except ValueError: - self.error('Bad lineno: %s' % arg) + self.error('Bad lineno: "{}".'.format(arg)) return else: - # no colon; can be lineno or function try: + # A line number lineno = int(arg) + filename = self.curframe.f_code.co_filename except ValueError: + # A function or method name try: - func = eval(arg, - self.curframe.f_globals, - self.curframe_locals) - except: - func = arg - try: - if hasattr(func, '__func__'): - func = func.__func__ - code = func.__code__ - #use co_name to identify the bkpt (function names - #could be aliased, but co_name is invariant) - funcname = code.co_name - lineno = code.co_firstlineno - filename = code.co_filename - except: - # last thing to try - (ok, filename, ln) = self.lineinfo(arg) - if not ok: - self.error('The specified object %r is not a function ' - 'or was not found along sys.path.' % arg) - return - funcname = ok # ok contains a function name - lineno = int(ln) - if not filename: - filename = self.defaultFile() - # Check for reasonable breakpoint - line = self.checkline(filename, lineno) - if line: - # now set the break point - err = self.set_break(filename, line, temporary, cond, funcname) - if err: - self.error(err, file=self.stdout) - else: - bp = self.get_breaks(filename, line)[-1] - self.message("Breakpoint %d at %s:%d" % - (bp.number, bp.file, bp.line)) + filename, lineno = bdb.funcname_breakpoint(arg.strip(), + frame=self.curframe) + except bdb.BdbError as e: + self.error(e.args[0]) + return + + filename = bdb.canonic(filename) + if filename.startswith('<') and filename.endswith('>'): + if filename == '' and self.mainpyfile: + filename = self.mainpyfile + # else + # doctest installs a patch at linecache.getlines to allow to be linecached and readable. + else: + root, ext = os.path.splitext(filename) + if ext == '': + filename = filename + '.py' + if not os.path.exists(filename): + self.error('Bad filename: "{}".'.format(arg)) + return + + # Now set the break point + error = self.set_break(filename, lineno, temporary, cond) + if error: + self.error(error) + else: + bp = self.get_breaks(filename, lineno)[-1] + self.message('Breakpoint {:d} at {}:{:d}'.format( + bp.number, bp.file, bp.line)) # To be overridden in derived debuggers def defaultFile(self): @@ -700,60 +699,6 @@ complete_tbreak = _complete_location - def lineinfo(self, identifier): - failed = (None, None, None) - # Input is identifier, may be in single quotes - idstring = identifier.split("'") - if len(idstring) == 1: - # not in single quotes - id = idstring[0].strip() - elif len(idstring) == 3: - # quoted - id = idstring[1].strip() - else: - return failed - if id == '': return failed - parts = id.split('.') - # Protection for derived debuggers - if parts[0] == 'self': - del parts[0] - if len(parts) == 0: - return failed - # Best first guess at file to look at - fname = self.defaultFile() - if len(parts) == 1: - item = parts[0] - else: - # More than one part. - # First is module, second is method/class - f = self.lookupmodule(parts[0]) - if f: - fname = f - item = parts[1] - answer = find_function(item, fname) - return answer or failed - - def checkline(self, filename, lineno): - """Check whether specified line seems to be executable. - - Return `lineno` if it is, 0 if not (e.g. a docstring, comment, blank - line or EOF). Warning: testing is not comprehensive. - """ - # this method should be callable before starting debugging, so default - # to "no globals" if there is no current frame - globs = self.curframe.f_globals if hasattr(self, 'curframe') else None - line = linecache.getline(filename, lineno, globs) - if not line: - self.message('End of file') - return 0 - line = line.strip() - # Don't allow setting breakpoint at a blank line - if (not line or (line[0] == '#') or - (line[:3] == '"""') or line[:3] == "'''"): - self.error('Blank or comment') - return 0 - return lineno - def do_enable(self, arg): """enable bpnumber [bpnumber ...] Enables the breakpoints given as a space separated list of @@ -1213,7 +1158,7 @@ first = self.lineno + 1 if last is None: last = first + 10 - filename = self.curframe.f_code.co_filename + filename = bdb.canonic(self.curframe.f_code.co_filename) breaklist = self.get_file_breaks(filename) try: lines = linecache.getlines(filename, self.curframe.f_globals) @@ -1481,30 +1426,6 @@ # other helper functions - def lookupmodule(self, filename): - """Helper function for break/clear parsing -- may be overridden. - - lookupmodule() translates (possibly incomplete) file or module name - into an absolute file name. - """ - if os.path.isabs(filename) and os.path.exists(filename): - return filename - f = os.path.join(sys.path[0], filename) - if os.path.exists(f) and self.canonic(f) == self.mainpyfile: - return f - root, ext = os.path.splitext(filename) - if ext == '': - filename = filename + '.py' - if os.path.isabs(filename): - return filename - for dirname in sys.path: - while os.path.islink(dirname): - dirname = os.readlink(dirname) - fullname = os.path.join(dirname, filename) - if os.path.exists(fullname): - return fullname - return None - def _runscript(self, filename): # The script has to run in __main__ namespace (or imports from # __main__ will break). @@ -1524,7 +1445,7 @@ # avoid stopping before we reach the main script (see user_line and # user_call for details). self._wait_for_mainpyfile = True - self.mainpyfile = self.canonic(filename) + self.mainpyfile = bdb.canonic(filename) self._user_requested_quit = False with open(filename, "rb") as fp: statement = "exec(compile(%r, %r, 'exec'))" % \ diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1,11 +1,17 @@ # A test suite for pdb; not very comprehensive at the moment. import imp +import bdb import pdb import sys import unittest import subprocess import textwrap +import time +try: + import threading +except ImportError: + threading = None from test import support # This little helper class is essential for testing pdb under doctest. @@ -275,6 +281,38 @@ """ +def test_issue_14792(): + """ + >>> def foo(): + ... x = 1 + ... x = 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True).set_trace() + ... foo() + + >>> with PdbTestInput([ + ... 'step', + ... 'step', + ... 'break foo', + ... 'continue', + ... ]): + ... test_function() + > (3)test_function() + -> foo() + (Pdb) step + --Call-- + > (1)foo() + -> def foo(): + (Pdb) step + > (2)foo() + -> x = 1 + (Pdb) break foo + Breakpoint 1 at :2 + (Pdb) continue + """ + + def do_nothing(): pass @@ -597,11 +635,36 @@ """ +def normalize(result, filename='', strip_bp_lnum=False): + """Normalize a test result.""" + lines = [] + for line in result.splitlines(): + while line.startswith('(Pdb) ') or line.startswith('(com) '): + line = line[6:] + words = line.split() + line = [] + # Replace tabs with spaces + for word in words: + if filename: + idx = word.find(filename) + # Remove the filename prefix + if idx > 0: + word = word[idx:] + if idx >=0 and strip_bp_lnum: + idx = word.find(':') + # Remove the ':' separator and breakpoint line number + if idx > 0: + word = word[:idx] + line.append(word) + line = ' '.join(line) + lines.append(line.strip()) + return '\n'.join(lines) + + class PdbTestCase(unittest.TestCase): - def run_pdb(self, script, commands): + def run_pdb(self, script, commands, filename): """Run 'script' lines with pdb and the pdb 'commands'.""" - filename = 'main.py' with open(filename, 'w') as f: f.write(textwrap.dedent(script)) self.addCleanup(support.unlink, filename) @@ -609,7 +672,7 @@ stdout = stderr = None with subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.STDOUT, + stderr=subprocess.PIPE, ) as proc: stdout, stderr = proc.communicate(str.encode(commands)) stdout = stdout and bytes.decode(stdout) @@ -662,11 +725,479 @@ with open('bar.py', 'w') as f: f.write(textwrap.dedent(bar)) self.addCleanup(support.unlink, 'bar.py') - stdout, stderr = self.run_pdb(script, commands) + stdout, stderr = self.run_pdb(script, commands, 'main.py') self.assertTrue( any('main.py(5)foo()->None' in l for l in stdout.splitlines()), 'Fail to step into the caller after a return') + def test_bdb_module_parser(self): + script = """ + '''Module documentation.''' + X = None + + def bar(a, b): + # comment + pass # comment + pass + # comment + + def main(x=None, + y=None): + '''Function documentation.''' + def nested_def(): + pass + + bar(x, + y) + # comment + pass + bar( + x, y # comment + ) + class NestedClass: + def n_foo(self): + pass + pass + + return bar( + x, y + ) + + class C: + '''Class documentation.''' + x = None + y = None + + class D: + def d_foo(self): + pass + class E: + def e_foo(self): + pass + + def c_bar(self): + pass + + c_bar2 = c_bar + c_bar3 = c_bar + + def foo(): + pass + + if __name__ == '__main__': + result = main( + 1, 2 + ) + print(result) + + """ + expected = """ + Functions: + [('bar', 5), + ('main', 11), + ('C.D.d_foo', 39), + ('C.D.E.e_foo', 42), + ('C.c_bar', 45), + ('foo', 51)] + Functions and classes firstlineno: + [(5, 5), + (11, 11), + (14, 11), + (24, 11), + (25, 11), + (33, 0), + (38, 0), + (39, 39), + (41, 0), + (42, 42), + (45, 45), + (51, 51)] + Last line: 59 + Module lines: + [(3, 3, 1, 0), + (33, 36, 1, 0), + (38, 38, 1, 0), + (41, 41, 1, 0), + (48, 49, 1, 0), + (54, 54, 1, 0), + (55, 57, 2, 0), + (58, 58, 1, 0)] + Functions lines: + [(5, 5, 0, 5), (7, 8, 1, 5)] + [(11, 12, 0, 11), + (14, 15, 1, 11), + (17, 18, 2, 11), + (20, 20, 1, 11), + (21, 23, 2, 11), + (24, 27, 1, 11), + (29, 31, 2, 11)] + [(39, 39, 0, 39), (40, 40, 1, 39)] + [(42, 42, 0, 42), (43, 43, 1, 42)] + [(45, 45, 0, 45), (46, 46, 1, 45)] + [(51, 51, 0, 51), (52, 52, 1, 51)] + """ + filename = 'main.py' + with open(filename, 'w') as f: + f.write(textwrap.dedent(script)) + self.addCleanup(support.unlink, filename) + result = str(bdb.ModuleSource(filename)) + expected = textwrap.dedent(expected) + self.assertTrue(result in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to parse correctly a module.'.format(expected, result)) + + def test_breakpoints_set_on_non_statement(self): + script = """ + def foo(): + + # comment + x = 1 + + x = len( + 'foo' + ) + + foo() + """ + commands = """ + break 3 + break 4 + break 8 + continue + break + continue + quit + """ + expected = """ + > main.py(2)() + -> def foo(): + Breakpoint 1 at main.py:3 + Breakpoint 2 at main.py:4 + Breakpoint 3 at main.py:8 + > main.py(5)foo() + -> x = 1 + Num Type Disp Enb Where + 1 breakpoint keep yes at main.py:3 + breakpoint already hit 1 time + 2 breakpoint keep yes at main.py:4 + breakpoint already hit 1 time + 3 breakpoint keep yes at main.py:8 + > main.py(7)foo() + -> x = len( + """ + filename = 'main.py' + stdout, stderr = self.run_pdb(script, commands, filename) + stdout = normalize(stdout, filename) + expected = normalize(expected) + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to stop at breakpoint set at empty line, comment line or' + ' multi-line statement.'.format(expected, stdout)) + + def test_issue14789(self): + script = """ + def bar(a): + x = 1 + + bar(10) + bar(20) + """ + commands = """ + break bar + commands 1 + print a + end + ignore 1 1 + break bar + commands 2 + print a + 1 + end + ignore 2 1 + continue + break + quit + """ + expected = """ + > main.py(2)() + -> def bar(a): + Breakpoint 1 at main.py:3 + Will ignore next 1 crossing of breakpoint 1. + Breakpoint 2 at main.py:3 + Will ignore next 1 crossing of breakpoint 2. + 20 + 21 + > main.py(3)bar() + -> x = 1 + Num Type Disp Enb Where + 1 breakpoint keep yes at main.py:3 + breakpoint already hit 2 times + 2 breakpoint keep yes at main.py:3 + breakpoint already hit 2 times + """ + filename = 'main.py' + stdout, stderr = self.run_pdb(script, commands, filename) + stdout = normalize(stdout, filename) + expected = normalize(expected) + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to handle two breakpoints set on the same line.'.format( + expected, stdout)) + + def test_issue14795(self): + script = """ + class C: + def foo(self): + pass + """ + commands = """ + break C.foo + break bar.bar + quit + """ + bar = """ + def bar(): + pass + """ + expected = """ + > main.py(2)() + -> class C: + Breakpoint 1 at main.py:4 + Breakpoint 2 at bar.py:3 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + filename = 'main.py' + stdout, stderr = self.run_pdb(script, commands, filename) + stdout = normalize(normalize(stdout, filename), 'bar.py') + expected = normalize(expected) + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to set a breakpoint in a method, or in a function whose' + ' module is not yet imported.'.format(expected, stdout)) + + def test_issue_14808(self): + script = """ + def foo(): + pass + + def bar(): + pass + + foo() + bar() + """ + commands = """ + break 2 + break bar + continue + continue + break + quit + """ + expected = """ + > main.py(2)() + -> def foo(): + Breakpoint 1 at main.py:2 + Breakpoint 2 at main.py:6 + > main.py(3)foo() + -> pass + > main.py(6)bar() + -> pass + Num Type Disp Enb Where + 1 breakpoint keep yes at main.py:2 + breakpoint already hit 1 time + 2 breakpoint keep yes at main.py:6 + breakpoint already hit 1 time + """ + filename = 'main.py' + stdout, stderr = self.run_pdb(script, commands, filename) + stdout = normalize(stdout, filename) + expected = normalize(expected) + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to stop at breakpoint set at function definition line' + ' or at function name.'.format(expected, stdout)) + + @unittest.skipIf(not threading, 'the source file is changed in a thread') + def test_issue_14912(self): + script = """ + import bar + + def foo(): + bar.bar() + + foo() + """ + commands = """ + break bar.bar + continue + list + import time; time.sleep(2) + restart + continue + list + quit + """ + bar = """ + def bar(): + x = 1 + x = 2 + """ + bar_2 = """ + def bar(): + # comment + # comment + x = 1 + x = 2 + """ + expected = """ + > main.py(2)() + -> import bar + Breakpoint 1 at bar.py:3 + > bar.py(3)bar() + -> x = 1 + 1 + 2 def bar(): + 3 B-> x = 1 + 4 x = 2 + [EOF] + Restarting main.py with arguments: + main.py + > main.py(2)() + -> import bar + > bar.py(5)bar() + -> x = 1 + 1 + 2 def bar(): + 3 B # comment + 4 # comment + 5 -> x = 1 + 6 x = 2 + [EOF] + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + filename = 'main.py' + with open(filename, 'w') as f: + f.write(textwrap.dedent(script)) + self.addCleanup(support.unlink, filename) + cmd = [sys.executable, '-m', 'pdb', filename] + stdout = stderr = None + with subprocess.Popen(cmd, stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as proc: + def update_source(): + # Wait for the Pdb subprocess to be fully started. + time.sleep(1) + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar_2)) + threading.Thread(target=update_source).start() + stdout, stderr = proc.communicate(str.encode(commands)) + stdout = stdout and bytes.decode(stdout) + stderr = stderr and bytes.decode(stderr) + stdout = normalize(normalize(stdout, filename), 'bar.py') + expected = normalize(expected, 'bar.py') + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to restart a new session after the source of an imported' + ' module has been modified.'.format(expected, stdout)) + + def test_set_breakpoint_by_function_name(self): + script = """ + class C: + c_foo = 1 + + class D: + def d_foo(self): + pass + + def foo(): + pass + + not_a_function = 1 + foo() + """ + commands = """ + break C + break C.c_foo + break D.d_foo + break foo + break not_a_function + break len + break logging.handlers.SocketHandler.close + continue + break C + break C.c_foo + break D.d_foo + break foo + break not_a_function + quit + """ + expected = """ + > main.py(2)() + -> class C: + *** Bad name: "C". + *** Bad name: "C.c_foo". + Breakpoint 1 at main.py:7 + Breakpoint 2 at main.py:10 + *** Bad name: "not_a_function". + *** Cannot set a breakpoint at "len" + Breakpoint 3 at handlers.py: + > main.py(10)foo() + -> pass + *** Bad function name: "C". + *** Cannot set a breakpoint at "C.c_foo" + Breakpoint 4 at main.py:7 + Breakpoint 5 at main.py:10 + *** Cannot set a breakpoint at "not_a_function" + """ + filename = 'main.py' + stdout, stderr = self.run_pdb(script, commands, filename) + stdout = normalize(normalize(stdout, 'handlers.py', strip_bp_lnum=True), filename) + expected = normalize(expected, 'handlers.py', strip_bp_lnum=True) + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to handle a breakpoint set by function name.'.format(expected, stdout)) + + def test_breakpoint_set_by_line_and_funcname(self): + script = """ + def foo(): + pass + + foo() + """ + commands = """ + break foo + break 2 + continue + break + quit + """ + expected = """ + > main.py(2)() + -> def foo(): + Breakpoint 1 at main.py:3 + Breakpoint 2 at main.py:2 + > main.py(3)foo() + -> pass + Num Type Disp Enb Where + 1 breakpoint keep yes at main.py:3 + breakpoint already hit 1 time + 2 breakpoint keep yes at main.py:2 + breakpoint already hit 1 time + """ + filename = 'main.py' + stdout, stderr = self.run_pdb(script, commands, filename) + stdout = normalize(stdout, filename) + expected = normalize(expected) + self.assertTrue(stdout in expected, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Failed hits for breakpoints set by line number and by function' + ' name.'.format(expected, stdout)) + def tearDown(self): support.unlink(support.TESTFN)