--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/DebugClients/Python/BreakpointWatch.py Mon Sep 19 22:47:52 2016 +0200 @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the breakpoint and watch class. +""" + +import os + + +class Breakpoint: + """ + Breakpoint class. + + Implements temporary breakpoints, ignore counts, disabling and + (re)-enabling, and conditionals. + + Breakpoints are indexed by the file,line tuple using breaks. It + points to a single Breakpoint instance. This is rather different to + the original bdb, since there may be more than one breakpoint per line. + + To test for a specific line in a file there is another dict breakInFile, + which is indexed only by filename and holds all line numbers where + breakpoints are. + """ + breaks = {} # indexed by (filename, lineno) tuple: Breakpoint + breakInFile = {} # indexed by filename: [lineno] + breakInFrameCache = {} + + def __init__(self, filename, lineno, temporary=False, cond=None): + """ + Constructor + + @param filename file name where a breakpoint is set + @type str + @param lineno line number of the breakpoint + @type int + @keyparam temporary flag to indicate a temporary breakpoint + @type bool + @keyparam cond Python expression which dynamically enables this bp + @type str + """ + filename = os.path.abspath(filename) + self.file = filename + self.line = lineno + self.temporary = temporary + self.cond = cond + self.enabled = True + self.ignore = 0 + self.hits = 0 + self.breaks[filename, lineno] = self + lines = self.breakInFile.setdefault(filename, []) + if lineno not in lines: + lines.append(lineno) + self.breakInFrameCache.clear() + + def deleteMe(self): + """ + Public method to clear this breakpoint. + """ + try: + del self.breaks[(self.file, self.line)] + self.breakInFile[self.file].remove(self.line) + if not self.breakInFile[self.file]: + del self.breakInFile[self.file] + except KeyError: + pass + + def enable(self): + """ + Public method to enable this breakpoint. + """ + self.enabled = True + + def disable(self): + """ + Public method to disable this breakpoint. + """ + self.enabled = False + + @staticmethod + def clear_break(filename, lineno): + """ + Public method reimplemented from bdb.py to clear a breakpoint. + + @param filename file name of the bp to retrieve + @type str + @param lineno line number of the bp to retrieve + @type int + """ + bp = Breakpoint.breaks.get((filename, lineno)) + if bp: + bp.deleteMe() + Breakpoint.breakInFrameCache.clear() + + @staticmethod + def clear_all_breaks(): + """ + Public method to clear all breakpoints. + """ + for bp in Breakpoint.breaks.copy(): + bp.deleteMe() + Breakpoint.breakInFrameCache.clear() + + @staticmethod + def get_break(filename, lineno): + """ + Public method to get the breakpoint of a particular line. + + Because eric6 supports only one breakpoint per line, this + method will return only one breakpoint. + + @param filename file name of the bp to retrieve + @type str + @param lineno line number of the bp to retrieve + @type int + @return Breakpoint or None, if there is no bp + @rtype Breakpoint object or None + """ + return Breakpoint.breaks.get((filename, lineno)) + + @staticmethod + def effectiveBreak(filename, lineno, frame): + """ + Public method to determine which breakpoint for this filename:lineno + 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. + + @param filename file name of the bp to retrieve + @type str + @param lineno line number of the bp to retrieve + @type int + @param frame the current execution frame + @type frame object + @return tuple of Breakpoint and a flag to indicate, that a + temporary breakpoint may be deleted + @rtype tuple of Breakpoint, bool + """ + b = Breakpoint.breaks[filename, lineno] + if not b.enabled: + return (None, False) + + # 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 + return (None, False) + else: + # breakpoint and marker that'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 Exception: + # 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, False) + + +class Watch: + """ + Watch class. + + Implements temporary watches, ignore counts, disabling and + (re)-enabling, and conditionals. + """ + watches = [] + + def __init__(self, cond, compiledCond, flag, temporary=False): + """ + Constructor + + @param cond condition as string with flag + @type str + @param compiledCond precompiled condition + @type code object + @param flag indicates type of watch (created or changed) + @type str + @keyparam temporary flag for temporary watches + @type bool + """ + # Should not occur + if not cond: + return + + self.cond = cond + self.compiledCond = compiledCond + self.temporary = temporary + + self.enabled = True + self.ignore = 0 + + self.created = False + self.changed = False + if flag == '??created??': + self.created = True + elif flag == '??changed??': + self.changed = True + + self.values = {} + self.watches.append(self) + + def deleteMe(self): + """ + Public method to clear this watch expression. + """ + try: + del self.watches[self] + except ValueError: + pass + + def enable(self): + """ + Public method to enable this watch. + """ + self.enabled = True + + def disable(self): + """ + Public method to disable this watch. + """ + self.enabled = False + + @staticmethod + def clear_watch(cond): + """ + Public method to clear a watch expression. + + @param cond expression of the watch expression to be cleared + @type str + """ + try: + Watch.watches.remove(Watch.get_watch(cond)) + except ValueError: + pass + + @staticmethod + def clear_all_watches(): + """ + Public method to clear all watch expressions. + """ + del Watch.watches[:] + + @staticmethod + def get_watch(cond): + """ + Public method to get a watch expression. + + @param cond expression of the watch expression to be cleared + @type str + @return reference to the watch point + @rtype Watch or None + """ + for b in Watch.watches: + if b.cond == cond: + return b + + @staticmethod + def effectiveWatch(frame): + """ + Public method to determine, if a watch expression is effective. + + @param frame the current execution frame + @type frame object + @return tuple of watch expression and a flag to indicate, that a + temporary watch expression may be deleted + @rtype tuple of Watch, int + """ + for b in Watch.watches: + if not b.enabled: + continue + try: + val = eval(b.compiledCond, frame.f_globals, frame.f_locals) + if b.created: + if frame in b.values: + continue + else: + b.values[frame] = [1, val, b.ignore] + return (b, True) + + elif b.changed: + try: + if b.values[frame][1] != val: + b.values[frame][1] = val + else: + continue + except KeyError: + b.values[frame] = [1, val, b.ignore] + + if b.values[frame][2] > 0: + b.values[frame][2] -= 1 + continue + else: + return (b, True) + + elif val: + if b.ignore > 0: + b.ignore -= 1 + continue + else: + return (b, True) + except Exception: + continue + return (None, False) + + +# +# eflag: noqa = M702