diff --git a/Lib/bdb.py b/Lib/bdb.py index 5a80fa8ba53..eb2fa4fb3e9 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -7,6 +7,7 @@ from inspect import CO_GENERATOR __all__ = ["BdbQuit", "Bdb", "Breakpoint"] + class BdbQuit(Exception): """Exception to give up completely.""" @@ -17,6 +18,12 @@ class Bdb: This class takes care of details of the trace facility; a derived class should implement user interaction. The standard debugger class (pdb.Pdb) is an example. + + The optional skip argument must be an iterable of glob-style + module name patterns. The debugger will not step into frames + that originate in a module that matches one of these patterns. + Whether a frame is considered to originate in a certain module + is determined by the __name__ in the frame globals. """ def __init__(self, skip=None): @@ -26,6 +33,13 @@ class Bdb: self.frame_returning = None def canonic(self, filename): + """Return canonical form of filename. + + For real filenames, the canonical form is a case-normalized (on + case insenstive filesystems) absolute path. 'Filenames' with + angle brackets, such as "", generated in interactive + mode, are returned unchanged. + """ if filename == "<" + filename[1:-1] + ">": return filename canonic = self.fncache.get(filename) @@ -36,12 +50,36 @@ class Bdb: return canonic def reset(self): + """Set values of attributes as ready to start debugging.""" import linecache linecache.checkcache() self.botframe = None self._set_stopinfo(None, None) def trace_dispatch(self, frame, event, arg): + """Dispatch a trace function for debugged frames based on the event. + + This function is installed as the trace function for debugged + frames. Its return value is the new trace function, which is + usually itself. The default implementation decides how to + dispatch a frame, depending on the type of event (passed in as a + string) that is about to be executed. + + The event can be one of the following: + line: A new line of code is going to be executed. + call: A function is about to be called or another code block + is entered. + return: A function or other code block is about to return. + exception: An exception has occurred. + c_call: A C function is about to be called. + c_return: A C functon has returned. + c_exception: A C function has raised an exception. + + For the Python events, specialized functions (see the dispatch_*() + methods) are called. For the C events, no action is taken. + + The arg parameter depends on the previous event. + """ if self.quitting: return # None if event == 'line': @@ -62,12 +100,24 @@ class Bdb: return self.trace_dispatch def dispatch_line(self, frame): + """Invoke user function and return trace function for line event. + + If the debugger stops on the current line, invoke + self.user_line(). Raise BdbQuit if self.quitting is set. + Return self.trace_dispatch to continue tracing in this scope. + """ if self.stop_here(frame) or self.break_here(frame): self.user_line(frame) if self.quitting: raise BdbQuit return self.trace_dispatch def dispatch_call(self, frame, arg): + """Invoke user function and return trace function for call event. + + If the debugger stops on this function call, invoke + self.user_call(). Raise BbdQuit if self.quitting is set. + Return self.trace_dispatch to continue tracing in this scope. + """ # XXX 'arg' is no longer used if self.botframe is None: # First call of dispatch since reset() @@ -84,6 +134,12 @@ class Bdb: return self.trace_dispatch def dispatch_return(self, frame, arg): + """Invoke user function and return trace function for return event. + + If the debugger stops on this function return, invoke + self.user_return(). Raise BdbQuit if self.quitting is set. + Return self.trace_dispatch to continue tracing in this scope. + """ if self.stop_here(frame) or frame == self.returnframe: # Ignore return events in generator except when stepping. if self.stopframe and frame.f_code.co_flags & CO_GENERATOR: @@ -100,6 +156,12 @@ class Bdb: return self.trace_dispatch def dispatch_exception(self, frame, arg): + """Invoke user function and return trace function for exception event. + + If the debugger stops on this exception, invoke + self.user_exception(). Raise BdbQuit if self.quitting is set. + Return self.trace_dispatch to continue tracing in this scope. + """ if self.stop_here(frame): # When stepping with next/until/return in a generator frame, skip # the internal StopIteration exception (with no traceback) @@ -125,12 +187,14 @@ class Bdb: # definition of stopping and breakpoints. def is_skipped_module(self, module_name): + "Return True if module_name matches any skip pattern." for pattern in self.skip: if fnmatch.fnmatch(module_name, pattern): return True return False def stop_here(self, frame): + "Return True if frame is below the starting frame in the stack." # (CT) stopframe may now also be None, see dispatch_call. # (CT) the former test for None is therefore removed from here. if self.skip and \ @@ -145,6 +209,11 @@ class Bdb: return False def break_here(self, frame): + """Return True if there is an effective breakpoint for this line. + + Check for line or function breakpoint and if in effect. + Delete temporary breakpoints if effective() says to. + """ filename = self.canonic(frame.f_code.co_filename) if filename not in self.breaks: return False @@ -167,33 +236,43 @@ class Bdb: return False def do_clear(self, arg): + """Remove temporary breakpoint. + + Must implement in derived classes or get NotImplementedError. + """ raise NotImplementedError("subclass of bdb must implement do_clear()") def break_anywhere(self, frame): + """Return True if there is any breakpoint for frame's filename. + """ return self.canonic(frame.f_code.co_filename) in self.breaks # Derived classes should override the user_* methods # to gain control. def user_call(self, frame, argument_list): - """This method is called when there is the remote possibility - that we ever need to stop in this function.""" + """Called if we might stop in a function.""" pass def user_line(self, frame): - """This method is called when we stop or break at this line.""" + """Called when when we stop or break at a line.""" pass def user_return(self, frame, return_value): - """This method is called when a return trap is set here.""" + """Called when a return trap is set here.""" pass def user_exception(self, frame, exc_info): - """This method is called if an exception occurs, - but only if we are to stop at or just below this level.""" + """Called when we stop on an exception.""" pass def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): + """Set the attributes for stopping. + + If stoplineno is greater than or equal to 0, then stop at line + greater than or equal to the stopline. If stoplineno is -1, then + don't stop at all. + """ self.stopframe = stopframe self.returnframe = returnframe self.quitting = False @@ -205,8 +284,8 @@ class Bdb: # to affect the stepping state. def set_until(self, frame, lineno=None): - """Stop when the line with the line no greater than the current one is - reached or when returning from current frame""" + """Stop when the line with the lineno greater than the current one is + reached or when returning from current frame.""" # the name "until" is borrowed from gdb if lineno is None: lineno = frame.f_lineno + 1 @@ -236,7 +315,7 @@ class Bdb: self._set_stopinfo(frame.f_back, frame) def set_trace(self, frame=None): - """Start debugging from `frame`. + """Start debugging from frame. If frame is not specified, debugging starts from caller's frame. """ @@ -251,6 +330,10 @@ class Bdb: sys.settrace(self.trace_dispatch) def set_continue(self): + """Stop only at breakpoints or when finished. + + If there are no breakpoints, set the system trace function to None. + """ # Don't stop except at breakpoints or when finished self._set_stopinfo(self.botframe, None, -1) if not self.breaks: @@ -262,6 +345,10 @@ class Bdb: frame = frame.f_back def set_quit(self): + """Set quitting attribute to True. + + Raises BdbQuit exception in the next call to a dispatch_*() method. + """ self.stopframe = self.botframe self.returnframe = None self.quitting = True @@ -269,13 +356,18 @@ class Bdb: # 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. + # error message if 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): + """Set a new breakpoint for filename:lineno. + + If lineno doesn't exist for the filename, return an error message. + The filename should be in canonical form. + """ filename = self.canonic(filename) import linecache # Import as late as possible line = linecache.getline(filename, lineno) @@ -285,14 +377,26 @@ class Bdb: if lineno not in list: list.append(lineno) bp = Breakpoint(filename, lineno, temporary, cond, funcname) + return None def _prune_breaks(self, filename, lineno): + """Prune breakpoints for filname:lineno. + + A list of breakpoints is maintained in the Bdb instance and in + the Breakpoint class. If a breakpoint in the Bdb instance no + longer exists in the Breakpoint class, then it's removed from the + Bdb instance. + """ if (filename, lineno) not in Breakpoint.bplist: self.breaks[filename].remove(lineno) if not self.breaks[filename]: del self.breaks[filename] def clear_break(self, filename, lineno): + """Delete breakpoints for filename:lineno. + + If no breakpoints were set, return an error message. + """ filename = self.canonic(filename) if filename not in self.breaks: return 'There are no breakpoints in %s' % filename @@ -303,16 +407,26 @@ class Bdb: for bp in Breakpoint.bplist[filename, lineno][:]: bp.deleteMe() self._prune_breaks(filename, lineno) + return None def clear_bpbynumber(self, arg): + """Delete a breakpoint by its index in Breakpoint.bpbynumber. + + If arg is invalid, return an error message. + """ try: bp = self.get_bpbynumber(arg) except ValueError as err: return str(err) bp.deleteMe() self._prune_breaks(bp.file, bp.line) + return None def clear_all_file_breaks(self, filename): + """Delete all breakpoints in filename. + + If none were set, return an error message. + """ filename = self.canonic(filename) if filename not in self.breaks: return 'There are no breakpoints in %s' % filename @@ -321,16 +435,27 @@ class Bdb: for bp in blist: bp.deleteMe() del self.breaks[filename] + return None def clear_all_breaks(self): + """Delete all existing breakpoints. + + If none were set, return an error message. + """ if not self.breaks: return 'There are no breakpoints' for bp in Breakpoint.bpbynumber: if bp: bp.deleteMe() self.breaks = {} + return None def get_bpbynumber(self, arg): + """Return a breakpoint by its index in Breakpoint.bybpnumber. + + For invalid arg values or if the breakpoint doesn't exist, + raise a ValueError. + """ if not arg: raise ValueError('Breakpoint number expected') try: @@ -346,17 +471,26 @@ class Bdb: return bp def get_break(self, filename, lineno): + """Return True if there is a breakpoint for filename:lineno.""" filename = self.canonic(filename) return filename in self.breaks and \ lineno in self.breaks[filename] def get_breaks(self, filename, lineno): + """Return all breakpoints for filename:lineno. + + If no breakpoints are set, return an empty list. + """ filename = self.canonic(filename) return filename in self.breaks and \ lineno in self.breaks[filename] and \ Breakpoint.bplist[filename, lineno] or [] def get_file_breaks(self, filename): + """Return all lines with breakpoints for filename. + + If no breakpoints are set, return an empty list. + """ filename = self.canonic(filename) if filename in self.breaks: return self.breaks[filename] @@ -364,12 +498,18 @@ class Bdb: return [] def get_all_breaks(self): + """Return all breakpoints that are set.""" return self.breaks # Derived classes and clients can call the following method # to get a data structure representing a stack trace. def get_stack(self, f, t): + """Return a list of (frame, lineno) in a stack trace and a size. + + List starts with original calling frame, if there is one. + Size may be number of frames above or below f. + """ stack = [] if t and t.tb_frame is f: t = t.tb_next @@ -388,6 +528,14 @@ class Bdb: return stack, i def format_stack_entry(self, frame_lineno, lprefix=': '): + """Return a string with information about a stack entry. + + The stack entry frame_lineno is a (frame, lineno) tuple. The + return string contains the canonical filename, the function name + or '', the input arguments, the return value, and the + line of code (if it exists). + + """ import linecache, reprlib frame, lineno = frame_lineno filename = self.canonic(frame.f_code.co_filename) @@ -418,6 +566,10 @@ class Bdb: # Both can be given as a string, or a code object. def run(self, cmd, globals=None, locals=None): + """Debug a statement executed via the exec() function. + + globals defaults to __main__.dict; locals defaults to globals. + """ if globals is None: import __main__ globals = __main__.__dict__ @@ -436,6 +588,10 @@ class Bdb: sys.settrace(None) def runeval(self, expr, globals=None, locals=None): + """Debug an expression executed via the eval() function. + + globals defaults to __main__.dict; locals defaults to globals. + """ if globals is None: import __main__ globals = __main__.__dict__ @@ -452,12 +608,17 @@ class Bdb: sys.settrace(None) def runctx(self, cmd, globals, locals): + """For backwards-compatibility. Defers to run().""" # B/W compatibility self.run(cmd, globals, locals) # This method is more useful to debug a single function call. def runcall(self, func, *args, **kwds): + """Debug a single function call. + + Return the result of the function call. + """ self.reset() sys.settrace(self.trace_dispatch) res = None @@ -472,6 +633,7 @@ class Bdb: def set_trace(): + """Start debugging with a Bdb instance from the caller's frame.""" Bdb().set_trace() @@ -482,11 +644,15 @@ class Breakpoint: (re)-enabling, and conditionals. Breakpoints are indexed by number through bpbynumber and by - the file,line tuple using bplist. The former points to a + 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. + When creating a breakpoint, its associated filename should be + in canonical form. If funcname is defined, a breakpoint hit will be + counted when the first line of that function is executed. A + conditional breakpoint always counts a hit. """ # XXX Keeping state in the class is a mistake -- this means @@ -519,6 +685,12 @@ class Breakpoint: self.bplist[file, line] = [self] def deleteMe(self): + """Delete the breakpoint from the list associated to a file:line. + + If it is the last breakpoint in that position, it also deletes + the entry for the file:line. + """ + index = (self.file, self.line) self.bpbynumber[self.number] = None # No longer in list self.bplist[index].remove(self) @@ -527,17 +699,31 @@ class Breakpoint: del self.bplist[index] def enable(self): + """Mark the breakpoint as enabled.""" self.enabled = True def disable(self): + """Mark the breakpoint as disabled.""" self.enabled = False def bpprint(self, out=None): + """Print the output of bpformat(). + + The optional out argument directs where the output is sent + and defaults to standard output. + """ if out is None: out = sys.stdout print(self.bpformat(), file=out) def bpformat(self): + """Return a string with information about the breakpoint. + + The information includes the breakpoint number, temporary + status, file:line position, break condition, number of times to + ignore, and number of times hit. + + """ if self.temporary: disp = 'del ' else: @@ -561,12 +747,20 @@ class Breakpoint: return ret def __str__(self): + "Return a condensed description of the breakpoint." 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`.""" + """Return True if break should happen here. + + Whether a break should happen depends on the way that b (the breakpoint) + was set. If it was set via line number, check if b.line is the same as + the one in the frame. If it was set via function name, check if this is + the right function and if it is on the first executable line. + """ if not b.funcname: # Breakpoint was set via line number. if b.line != frame.f_lineno: @@ -576,7 +770,6 @@ def checkfuncname(b, frame): 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 @@ -586,20 +779,21 @@ def checkfuncname(b, frame): # 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: + 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. - + Called only if we know there is a breakpoint at this location. Return + the breakpoint that was triggered and a boolean that indicates if it is + ok to delete a temporary breakpoint. Return (None, None) if there is no + matching breakpoint. """ possibles = Breakpoint.bplist[file, line] for b in possibles: