class LogWidget(QtGui.QWidget): sigDisplayEntry = QtCore.Signal(object) ## for thread-safetyness sigAddEntry = QtCore.Signal(object) ## for thread-safetyness sigScrollToAnchor = QtCore.Signal(object) # for internal use. def __init__(self, parent, manager): QtGui.QWidget.__init__(self, parent) self.ui = LogWidgetTemplate.Ui_Form() self.manager = manager self.ui.setupUi(self) # self.ui.input.hide() self.ui.filterTree.topLevelItem(1).setExpanded(True) self.entries = [] ## stores all log entries in memory self.cache = {} ## for storing html strings of entries that have already been processed self.displayedEntries = [] # self.currentEntries = None ## recordArray that stores currently displayed entries -- so that if filters get more restrictive we can just refilter this list instead of filtering everything self.typeFilters = [] self.importanceFilter = 0 self.dirFilter = False self.entryArrayBuffer = np.zeros( 1000, dtype=[ ### a record array for quick filtering of entries ("index", "int32"), ("importance", "int32"), ("msgType", "|S10"), ("directory", "|S100"), ("entryId", "int32"), ], ) self.entryArray = self.entryArrayBuffer[:0] self.filtersChanged() self.sigDisplayEntry.connect(self.displayEntry, QtCore.Qt.QueuedConnection) self.sigAddEntry.connect(self.addEntry, QtCore.Qt.QueuedConnection) self.ui.exportHtmlBtn.clicked.connect(self.exportHtml) self.ui.filterTree.itemChanged.connect(self.setCheckStates) self.ui.importanceSlider.valueChanged.connect(self.filtersChanged) # self.ui.logView.linkClicked.connect(self.linkClicked) self.ui.output.anchorClicked.connect(self.linkClicked) self.sigScrollToAnchor.connect(self.scrollToAnchor, QtCore.Qt.QueuedConnection) # page = self.ui.logView.page() # page.setLinkDelegationPolicy(page.DelegateAllLinks) def loadFile(self, f): """Load the file, f. f must be able to be read by configfile.py""" log = configfile.readConfigFile(f) self.entries = [] self.entryArrayBuffer = np.zeros( len(log), dtype=[ ("index", "int32"), ("importance", "int32"), ("msgType", "|S10"), ("directory", "|S100"), ("entryId", "int32"), ], ) self.entryArray = self.entryArrayBuffer[:] i = 0 for k, v in log.iteritems(): v["id"] = k[9:] ## record unique ID to facilitate HTML generation (javascript needs this ID) self.entries.append(v) self.entryArray[i] = np.array( [ ( i, v.get("importance", 5), v.get("msgType", "status"), v.get("currentDir", ""), v.get("entryId", v["id"]), ) ], dtype=[ ("index", "int32"), ("importance", "int32"), ("msgType", "|S10"), ("directory", "|S100"), ("entryId", "int32"), ], ) i += 1 self.filterEntries() ## puts all entries through current filters and displays the ones that pass def addEntry(self, entry): ## All incoming messages begin here ## for thread-safetyness: isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() if not isGuiThread: self.sigAddEntry.emit(entry) return self.entries.append(entry) i = len(self.entryArray) entryDir = entry.get("currentDir", None) if entryDir is None: entryDir = "" arr = np.array( [(i, entry["importance"], entry["msgType"], entryDir, entry["id"])], dtype=[ ("index", "int32"), ("importance", "int32"), ("msgType", "|S10"), ("directory", "|S100"), ("entryId", "int32"), ], ) ## make more room if needed if len(self.entryArrayBuffer) == len(self.entryArray): newArray = np.empty(len(self.entryArrayBuffer) + 1000, self.entryArrayBuffer.dtype) newArray[: len(self.entryArray)] = self.entryArray self.entryArrayBuffer = newArray self.entryArray = self.entryArrayBuffer[: len(self.entryArray) + 1] # self.entryArray[i] = [(i, entry['importance'], entry['msgType'], entry['currentDir'])] self.entryArray[i] = arr self.checkDisplay(entry) ## displays the entry if it passes the current filters # np.append(self.entryArray, np.array(i, [[i, entry['importance'], entry['msgType'], entry['currentDir']]]), dtype = [('index', int), ('importance', int), ('msgType', str), ('directory', str)]) def setCheckStates(self, item, column): if item == self.ui.filterTree.topLevelItem(1): if item.checkState(0): for i in range(item.childCount()): item.child(i).setCheckState(0, QtCore.Qt.Checked) elif item.parent() == self.ui.filterTree.topLevelItem(1): if not item.checkState(0): self.ui.filterTree.topLevelItem(1).setCheckState(0, QtCore.Qt.Unchecked) self.filtersChanged() def filtersChanged(self): ### Update self.typeFilters, self.importanceFilter, and self.dirFilter to reflect changes. tree = self.ui.filterTree self.typeFilters = [] for i in range(tree.topLevelItem(1).childCount()): child = tree.topLevelItem(1).child(i) if tree.topLevelItem(1).checkState(0) or child.checkState(0): text = child.text(0) self.typeFilters.append(unicode(text)) self.importanceFilter = self.ui.importanceSlider.value() self.updateDirFilter() # self.dirFilter = self.manager.getDirOfSelectedFile().name() # else: # self.dirFilter = False self.filterEntries() def updateDirFilter(self, dh=None): if self.ui.filterTree.topLevelItem(0).checkState(0): if dh == None: self.dirFilter = self.manager.getDirOfSelectedFile().name() else: self.dirFilter = dh.name() else: self.dirFilter = False def filterEntries(self): """Runs each entry in self.entries through the filters and displays if it makes it through.""" ### make self.entries a record array, then filtering will be much faster (to OR true/false arrays, + them) typeMask = self.entryArray["msgType"] == "" for t in self.typeFilters: typeMask += self.entryArray["msgType"] == t mask = (self.entryArray["importance"] > self.importanceFilter) * typeMask if self.dirFilter != False: d = np.ascontiguousarray(self.entryArray["directory"]) j = len(self.dirFilter) i = len(d) d = d.view(np.byte).reshape(i, 100)[:, :j] d = d.reshape(i * j).view("|S%d" % j) mask *= d == self.dirFilter self.ui.output.clear() global Stylesheet self.ui.output.document().setDefaultStyleSheet(Stylesheet) # global pageTemplate # self.ui.logView.setHtml(pageTemplate) indices = list(self.entryArray[mask]["index"]) inds = indices # if self.dirFilter != False: # j = len(self.dirFilter) # for i, n in inds: # if not self.entries[n]['currentDir'][:j] == self.dirFilter: # indices.pop(i) self.displayEntry([self.entries[i] for i in indices]) def checkDisplay(self, entry): ### checks whether entry passes the current filters and displays it if it does. if entry["msgType"] not in self.typeFilters: return elif entry["importance"] < self.importanceFilter: return elif self.dirFilter is not False: if entry["currentDir"][: len(self.dirFilter)] != self.dirFilter: return else: self.displayEntry([entry]) def displayEntry(self, entries): ## entries should be a list of log entries ## for thread-safetyness: isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() if not isGuiThread: self.sigDisplayEntry.emit(entries) return for entry in entries: if not self.cache.has_key(id(entry)): self.cache[id(entry)] = self.generateEntryHtml(entry) ## determine message color: # if entry['msgType'] == 'status': # color = 'green' # elif entry['msgType'] == 'user': # color = 'blue' # elif entry['msgType'] == 'error': # color = 'red' # elif entry['msgType'] == 'warning': # color = '#DD4400' ## orange # else: # color = 'black' # if entry.has_key('exception') or entry.has_key('docs') or entry.has_key('reasons'): ##self.displayComplexMessage(entry, color) # self.displayComplexMessage(entry) # else: # self.displayText(entry['message'], entry, color, timeStamp=entry['timestamp']) # for x in self.cache[id(entry)]: # self.ui.output.appendHtml(x) html = self.cache[id(entry)] # frame = self.ui.logView.page().currentFrame() # isMax = frame.scrollBarValue(QtCore.Qt.Vertical) == frame.scrollBarMaximum(QtCore.Qt.Vertical) sb = self.ui.output.verticalScrollBar() isMax = sb.value() == sb.maximum() # frame.findFirstElement('body').appendInside(html) self.ui.output.append(html) self.displayedEntries.append(entry) if isMax: ## can't scroll to end until the web frame has processed the html change # frame.setScrollBarValue(QtCore.Qt.Vertical, frame.scrollBarMaximum(QtCore.Qt.Vertical)) ## Calling processEvents anywhere inside an error handler is forbidden ## because this can lead to Qt complaining about paint() recursion. # QtGui.QApplication.processEvents() # self.ui.output.scrollToAnchor(str(entry['id'])) self.sigScrollToAnchor.emit(str(entry["id"])) ## queued connection # self.ui.logView.update() def scrollToAnchor(self, anchor): self.ui.output.scrollToAnchor(anchor) def generateEntryHtml(self, entry): msg = self.cleanText(entry["message"]) reasons = "" docs = "" exc = "" if entry.has_key("reasons"): reasons = self.formatReasonStrForHTML(entry["reasons"]) if entry.has_key("docs"): docs = self.formatDocsStrForHTML(entry["docs"]) if entry.get("exception", None) is not None: exc = self.formatExceptionForHTML(entry, entryId=entry["id"]) extra = reasons + docs + exc if extra != "": # extra = "<div class='logExtra'>" + extra + "</div>" extra = "<table class='logExtra'><tr><td>" + extra + "</td></tr></table>" # return """ # <div class='entry'> # <div class='%s'> # <span class='timestamp'>%s</span> # <span class='message'>%s</span> #%s # </div> # </div> # """ % (entry['msgType'], entry['timestamp'], msg, extra) return """ <a name="%s"/><table class='entry'><tr><td> <table class='%s'><tr><td> <span class='timestamp'>%s</span> <span class='message'>%s</span> %s </td></tr></table> </td></tr></table> """ % ( str(entry["id"]), entry["msgType"], entry["timestamp"], msg, extra, ) # if entry.has_key('exception') or entry.has_key('docs') or entry.has_key('reasons'): ##self.displayComplexMessage(entry, color) # return self.generateComplex(entry) # else: # return self.generateSimple(entry['message'], entry, color, timeStamp=entry['timestamp']) ##self.displayText(entry['message'], entry, color, timeStamp=entry['timestamp']) @staticmethod def cleanText(text): text = re.sub(r"&", "&", text) text = re.sub(r">", ">", text) text = re.sub(r"<", "<", text) text = re.sub(r"\n", "<br/>\n", text) return text # def displayText(self, msg, entry, colorStr='black', timeStamp=None, clean=True): # if clean: # msg = self.cleanText(msg) # if msg[-1:] == '\n': # msg = msg[:-1] # msg = '<br />'.join(msg.split('\n')) # if timeStamp is not None: # strn = '<b style="color:black"> %s </b> <span style="color:%s"> %s </span>' % (timeStamp, colorStr, msg) # else: # strn = '<span style="color:%s"> %s </span>' % (colorStr, msg) ##self.ui.output.appendHtml(strn) # self.cache[id(entry)].append(strn) # def displayComplexMessage(self, entry, color='black'): # self.displayText(entry['message'], entry, color, timeStamp = entry['timestamp'], clean=True) # if entry.has_key('reasons'): # reasons = self.formatReasonStrForHTML(entry['reasons']) # self.displayText(reasons, entry, 'black', clean=False) # if entry.has_key('docs'): # docs = self.formatDocsStrForHTML(entry['docs']) # self.displayText(docs, entry, 'black', clean=False) # if entry.get('exception', None) is not None: # self.displayException(entry['exception'], entry, 'black', tracebacks=entry.get('traceback', None)) def formatExceptionForHTML(self, entry, exception=None, count=1, entryId=None): ### Here, exception is a dict that holds the message, reasons, docs, traceback and oldExceptions (which are also dicts, with the same entries) ## the count and tracebacks keywords are for calling recursively if exception is None: exception = entry["exception"] # if tracebacks is None: # tracebacks = [] indent = 10 text = self.cleanText(exception["message"]) text = re.sub(r"^HelpfulException: ", "", text) # if exception.has_key('oldExc'): # self.displayText(" "*indent + str(count)+'. ' + text, entry, color, clean=False) # else: # self.displayText(" "*indent + str(count)+'. Original error: ' + text, entry, color, clean=False) messages = [text] # print "\n", messages, "\n" if exception.has_key("reasons"): reasons = self.formatReasonsStrForHTML(exception["reasons"]) text += reasons # self.displayText(reasons, entry, color, clean=False) if exception.has_key("docs"): docs = self.formatDocsStrForHTML(exception["docs"]) # self.displayText(docs, entry, color, clean=False) text += docs traceback = [self.formatTracebackForHTML(exception["traceback"], count)] text = [text] if exception.has_key("oldExc"): exc, tb, msgs = self.formatExceptionForHTML(entry, exception["oldExc"], count=count + 1) text.extend(exc) messages.extend(msgs) traceback.extend(tb) # else: # if len(tracebacks)==count+1: # n=0 # else: # n=1 # for i, tb in enumerate(tracebacks): # self.displayTraceback(tb, entry, number=i+n) if count == 1: exc = '<div class="exception"><ol>' + "\n".join(["<li>%s</li>" % ex for ex in text]) + "</ol></div>" tbStr = "\n".join( [ "<li><b>%s</b><br/><span class='traceback'>%s</span></li>" % (messages[i], tb) for i, tb in enumerate(traceback) ] ) # traceback = "<div class=\"traceback\" id=\"%s\"><ol>"%str(entryId) + tbStr + "</ol></div>" entry["tracebackHtml"] = tbStr # return exc + '<a href="#" onclick="showDiv(\'%s\')">Show traceback</a>'%str(entryId) + traceback return exc + '<a href="exc:%s">Show traceback %s</a>' % (str(entryId), str(entryId)) else: return text, traceback, messages def formatTracebackForHTML(self, tb, number): try: tb = [line for line in tb if not line.startswith("Traceback (most recent call last)")] except: print "\n" + str(tb) + "\n" raise return re.sub(" ", " ", ("").join(map(self.cleanText, tb)))[:-1] # tb = [self.cleanText(strip(x)) for x in tb] # lines = [] # prefix = '' # for l in ''.join(tb).split('\n'): # if l == '': # continue # if l[:9] == "Traceback": # prefix = ' ' + str(number) + '. ' # continue # spaceCount = 0 # while l[spaceCount] == ' ': # spaceCount += 1 # if prefix is not '': # spaceCount -= 1 # lines.append(" "*(spaceCount*4) + prefix + l) # prefix = '' # return '<div class="traceback">' + '<br />'.join(lines) + '</div>' # self.displayText('<br />'.join(lines), entry, color, clean=False) def formatReasonsStrForHTML(self, reasons): # indent = 6 reasonStr = "<table class='reasons'><tr><td>Possible reasons include:\n<ul>\n" for r in reasons: r = self.cleanText(r) reasonStr += "<li>" + r + "</li>\n" # reasonStr += " "*22 + chr(97+i) + ". " + r + "<br>" reasonStr += "</ul></td></tr></table>\n" return reasonStr def formatDocsStrForHTML(self, docs): # indent = 6 docStr = "<div class='docRefs'>Relevant documentation:\n<ul>\n" for d in docs: d = self.cleanText(d) docStr += '<li><a href="doc:%s">%s</a></li>\n' % (d, d) docStr += "</ul></div>\n" return docStr def exportHtml(self, fileName=False): # self.makeError1() if fileName is False: self.fileDialog = FileDialog(self, "Save HTML as...", self.manager.getCurrentDir().name()) # self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.exportHtml) return if fileName[-5:] != ".html": fileName += ".html" # doc = self.ui.output.document().toHtml('utf-8') # for e in self.displayedEntries: # if e.has_key('tracebackHtml'): # doc = re.sub(r'<a href="exc:%s">(<[^>]+>)*Show traceback %s(<[^>]+>)*</a>'%(str(e['id']), str(e['id'])), e['tracebackHtml'], doc) global pageTemplate doc = pageTemplate for e in self.displayedEntries: doc += self.cache[id(e)] for e in self.displayedEntries: if e.has_key("tracebackHtml"): doc = re.sub( r'<a href="exc:%s">(<[^>]+>)*Show traceback %s(<[^>]+>)*</a>' % (str(e["id"]), str(e["id"])), e["tracebackHtml"], doc, ) # doc = self.ui.logView.page().currentFrame().toHtml() f = open(fileName, "w") f.write(doc.encode("utf-8")) f.close() def makeError1(self): ### just for testing error logging try: self.makeError2() # print x except: t, exc, tb = sys.exc_info() # logExc(message="This button doesn't work", reasons='reason a, reason b', docs='documentation') # if isinstance(exc, HelpfulException): # exc.prependErr("Button doesn't work", (t,exc,tb), reasons = ["It's supposed to raise an error for testing purposes", "You're doing it wrong."]) # raise # else: printExc("This is the message sent to printExc.") # raise HelpfulException(message='This button does not work.', exc=(t, exc, tb), reasons=["It's supposed to raise an error for testing purposes", "You're doing it wrong."]) def makeError2(self): ### just for testing error logging try: print y except: t, exc, tb = sys.exc_info() raise HelpfulException( message="msg from makeError", exc=(t, exc, tb), reasons=["reason one", "reason 2"], docs=["what, you expect documentation?"], ) def linkClicked(self, url): url = url.toString() if url[:4] == "doc:": self.manager.showDocumentation(url[4:]) elif url[:4] == "exc:": cursor = self.ui.output.document().find("Show traceback %s" % url[4:]) try: tb = self.entries[int(url[4:]) - 1]["tracebackHtml"] except IndexError: try: tb = self.entries[self.entryArray[self.entryArray["entryId"] == (int(url[4:]))]["index"]][ "tracebackHtml" ] except: print "requested index %d, but only %d entries exist." % (int(url[4:]) - 1, len(self.entries)) raise cursor.insertHtml(tb) def clear(self): # self.ui.logView.setHtml("") self.ui.output.clear() self.displayedEntryies = []
class FileAnalysisView(QtGui.QWidget): sigDbChanged = QtCore.Signal() def __init__(self, parent, mod): QtGui.QWidget.__init__(self, parent) self.ui = Ui_Form() self.ui.setupUi(self) self.man = acq4.Manager.getManager() self.mod = mod self.dbFile = None self.db = None self.mods = [] self.currentModel = None self.populateModuleList() self.populateModelList() stateFile = os.path.join('modules', self.mod.name + '_db_file_list') files = self.mod.manager.readConfigFile(stateFile).get('db_file_list', []) self.ui.databaseCombo.addItem('') for f in files: self.ui.databaseCombo.addItem(f) self.ui.openDbBtn.clicked.connect(self.openDbClicked) self.ui.createDbBtn.clicked.connect(self.createDbClicked) self.ui.loadModuleBtn.clicked.connect(self.loadModule) self.ui.refreshDbBtn.clicked.connect(self.refreshDb) self.ui.dataModelCombo.currentIndexChanged.connect(self.loadModel) self.ui.analysisModuleList.currentItemChanged.connect(self.showModuleDescription) self.ui.analysisModuleList.itemDoubleClicked.connect(self.loadModule) self.ui.databaseCombo.currentIndexChanged.connect(self.dbComboChanged) def openDbClicked(self): bd = self.man.getBaseDir() if bd is None: bd = "" else: bd = bd.name() self.fileDialog = FileDialog(self, "Select Database File", bd, "SQLite Database (*.sqlite *.sql);;All Files (*.*)") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.openDb) def openDb(self, fileName): #fn = str(QtGui.QFileDialog.getOpenFileName(self, "Select Database File", self.man.getBaseDir().name(), "SQLite Database (*.sqlite)")) fileName = str(fileName) if fileName == '': return #if not fileName[-7:] == '.sqlite' and '.' not in fileName: # fileName =+ '.sqlite' self.ui.databaseCombo.blockSignals(True) try: ## put fileName at the top of the list, write to disk files = [self.ui.databaseCombo.itemText(i) for i in range(self.ui.databaseCombo.count())] files.remove('') if fileName in files: files.remove(fileName) files = [fileName] + files self.ui.databaseCombo.clear() self.ui.databaseCombo.addItem('') for f in files: self.ui.databaseCombo.addItem(f) stateFile = os.path.join('modules', self.mod.name + '_db_file_list') self.mod.manager.writeConfigFile({'db_file_list': files}, stateFile) self.ui.databaseCombo.setCurrentIndex(1) finally: self.ui.databaseCombo.blockSignals(False) self.dbFile = fileName self.db = database.AnalysisDatabase(self.dbFile, dataModel=self.currentModel) self.sigDbChanged.emit() def dbComboChanged(self): fn = self.ui.databaseCombo.currentText() if fn == '': return if not os.path.exists(fn): raise Exception("Database file does not exist: %s" % fn) self.openDb(fn) def quit(self): if self.db is not None: self.db.close() def createDbClicked(self): bd = self.man.getBaseDir() if bd is None: raise Exception("Must select a base directory before creating database.") self.fileDialog = FileDialog(self, "Create Database File", bd.name(), "SQLite Database (*.sqlite *.sql);;All Files (*.*)") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.setOption(QtGui.QFileDialog.DontConfirmOverwrite) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.createDb) def createDb(self, fileName): #fn = str(QtGui.QFileDialog.getSaveFileName(self, "Create Database File", self.man.getBaseDir().name(), "SQLite Database (*.sqlite)", None, QtGui.QFileDialog.DontConfirmOverwrite)) fileName = str(fileName) if fileName is '': return self.dbFile = fileName self.db = database.AnalysisDatabase(self.dbFile, dataModel=self.currentModel, baseDir=self.man.getBaseDir()) self.ui.databaseCombo.blockSignals(True) try: self.ui.databaseCombo.addItem(fileName) self.ui.databaseCombo.setCurrentIndex(self.ui.databaseCombo.count()-1) finally: self.ui.databaseCombo.blockSignals(False) self.sigDbChanged.emit() def refreshDb(self): if self.db is None: return self.db._readTableList() def addFileClicked(self): cf = self.mod.selectedFile() self.db.addDir(cf) def populateModuleList(self): for m in analysis.listModules(): self.ui.analysisModuleList.addItem(m) def loadModule(self): mod = self.ui.analysisModuleList.currentItem() if mod is None: return modName = str(mod.text()) #if self.ui.analysisCombo.currentIndex() == 0: #return #modName = str(self.ui.analysisCombo.currentText()) #self.ui.analysisCombo.setCurrentIndex(0) mod = AnalysisHost.AnalysisHost(dataManager=self.mod, dataModel=self.currentModel, module=modName) self.mods.append(mod) self.man.modules[modName] = mod def populateModelList(self): self.ui.dataModelCombo.clear() self.ui.dataModelCombo.addItem('Load...') mods = models.listModels() for m in mods: self.ui.dataModelCombo.addItem(m) if len(mods) == 1: self.ui.dataModelCombo.setCurrentIndex(1) self.loadModel() def loadModel(self): if self.ui.dataModelCombo.currentIndex() == 0: return modName = str(self.ui.dataModelCombo.currentText()) self.currentModel = models.loadModel(modName) acq4.Manager.getManager().dataModel = self.currentModel ## make model globally available if self.db is not None: self.db.setDataModel(self.currentModel) def currentDatabase(self): return self.db def currentDataModel(self): return self.currentModel def showModuleDescription(self): mod = self.ui.analysisModuleList.currentItem() if mod is None: return modName = str(mod.text()) cls = analysis.getModuleClass(modName) doc = cls.__doc__ self.ui.modDescriptionText.setPlainText(doc)
class LogWidget(Qt.QWidget): sigDisplayEntry = Qt.Signal(object) ## for thread-safetyness sigAddEntry = Qt.Signal(object) ## for thread-safetyness sigScrollToAnchor = Qt.Signal(object) # for internal use. def __init__(self, parent, manager): Qt.QWidget.__init__(self, parent) self.ui = LogWidgetTemplate.Ui_Form() self.manager = manager self.ui.setupUi(self) #self.ui.input.hide() self.ui.filterTree.topLevelItem(1).setExpanded(True) self.entries = [] ## stores all log entries in memory self.cache = { } ## for storing html strings of entries that have already been processed self.displayedEntries = [] #self.currentEntries = None ## recordArray that stores currently displayed entries -- so that if filters get more restrictive we can just refilter this list instead of filtering everything self.typeFilters = [] self.importanceFilter = 0 self.dirFilter = False self.entryArrayBuffer = np.zeros( 1000, dtype=[ ### a record array for quick filtering of entries ('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32') ]) self.entryArray = self.entryArrayBuffer[:0] self.filtersChanged() self.sigDisplayEntry.connect(self.displayEntry, Qt.Qt.QueuedConnection) self.sigAddEntry.connect(self.addEntry, Qt.Qt.QueuedConnection) self.ui.exportHtmlBtn.clicked.connect(self.exportHtml) self.ui.filterTree.itemChanged.connect(self.setCheckStates) self.ui.importanceSlider.valueChanged.connect(self.filtersChanged) #self.ui.logView.linkClicked.connect(self.linkClicked) self.ui.output.anchorClicked.connect(self.linkClicked) self.sigScrollToAnchor.connect(self.scrollToAnchor, Qt.Qt.QueuedConnection) #page = self.ui.logView.page() #page.setLinkDelegationPolicy(page.DelegateAllLinks) def loadFile(self, f): """Load the file, f. f must be able to be read by configfile.py""" log = configfile.readConfigFile(f) self.entries = [] self.entryArrayBuffer = np.zeros(len(log), dtype=[('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32')]) self.entryArray = self.entryArrayBuffer[:] i = 0 for k, v in log.items(): v['id'] = k[ 9:] ## record unique ID to facilitate HTML generation (javascript needs this ID) self.entries.append(v) self.entryArray[i] = np.array( [(i, v.get('importance', 5), v.get('msgType', 'status'), v.get('currentDir', ''), v.get('entryId', v['id']))], dtype=[('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32')]) i += 1 self.filterEntries( ) ## puts all entries through current filters and displays the ones that pass def addEntry(self, entry): ## All incoming messages begin here ## for thread-safetyness: isGuiThread = Qt.QThread.currentThread( ) == Qt.QCoreApplication.instance().thread() if not isGuiThread: self.sigAddEntry.emit(entry) return self.entries.append(entry) i = len(self.entryArray) entryDir = entry.get('currentDir', None) if entryDir is None: entryDir = '' arr = np.array([ (i, entry['importance'], entry['msgType'], entryDir, entry['id']) ], dtype=[('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32')]) ## make more room if needed if len(self.entryArrayBuffer) == len(self.entryArray): newArray = np.empty( len(self.entryArrayBuffer) + 1000, self.entryArrayBuffer.dtype) newArray[:len(self.entryArray)] = self.entryArray self.entryArrayBuffer = newArray self.entryArray = self.entryArrayBuffer[:len(self.entryArray) + 1] #self.entryArray[i] = [(i, entry['importance'], entry['msgType'], entry['currentDir'])] self.entryArray[i] = arr self.checkDisplay( entry) ## displays the entry if it passes the current filters #np.append(self.entryArray, np.array(i, [[i, entry['importance'], entry['msgType'], entry['currentDir']]]), dtype = [('index', int), ('importance', int), ('msgType', str), ('directory', str)]) def setCheckStates(self, item, column): if item == self.ui.filterTree.topLevelItem(1): if item.checkState(0): for i in range(item.childCount()): item.child(i).setCheckState(0, Qt.Qt.Checked) elif item.parent() == self.ui.filterTree.topLevelItem(1): if not item.checkState(0): self.ui.filterTree.topLevelItem(1).setCheckState( 0, Qt.Qt.Unchecked) self.filtersChanged() def filtersChanged(self): ### Update self.typeFilters, self.importanceFilter, and self.dirFilter to reflect changes. tree = self.ui.filterTree self.typeFilters = [] for i in range(tree.topLevelItem(1).childCount()): child = tree.topLevelItem(1).child(i) if tree.topLevelItem(1).checkState(0) or child.checkState(0): text = child.text(0) self.typeFilters.append(six.text_type(text)) self.importanceFilter = self.ui.importanceSlider.value() self.updateDirFilter() #self.dirFilter = self.manager.getDirOfSelectedFile().name() #else: #self.dirFilter = False self.filterEntries() def updateDirFilter(self, dh=None): if self.ui.filterTree.topLevelItem(0).checkState(0): if dh == None: self.dirFilter = self.manager.getDirOfSelectedFile().name() else: self.dirFilter = dh.name() else: self.dirFilter = False def filterEntries(self): """Runs each entry in self.entries through the filters and displays if it makes it through.""" ### make self.entries a record array, then filtering will be much faster (to OR true/false arrays, + them) typeMask = self.entryArray['msgType'] == '' for t in self.typeFilters: typeMask += self.entryArray['msgType'] == t mask = (self.entryArray['importance'] > self.importanceFilter) * typeMask if self.dirFilter != False: d = np.ascontiguousarray(self.entryArray['directory']) j = len(self.dirFilter) i = len(d) d = d.view(np.byte).reshape(i, 100)[:, :j] d = d.reshape(i * j).view('|S%d' % j) mask *= (d == self.dirFilter) self.ui.output.clear() global Stylesheet self.ui.output.document().setDefaultStyleSheet(Stylesheet) #global pageTemplate #self.ui.logView.setHtml(pageTemplate) indices = list(self.entryArray[mask]['index']) inds = indices #if self.dirFilter != False: #j = len(self.dirFilter) #for i, n in inds: #if not self.entries[n]['currentDir'][:j] == self.dirFilter: #indices.pop(i) self.displayEntry([self.entries[i] for i in indices]) def checkDisplay(self, entry): ### checks whether entry passes the current filters and displays it if it does. if entry['msgType'] not in self.typeFilters: return elif entry['importance'] < self.importanceFilter: return elif self.dirFilter is not False: if entry['currentDir'][:len(self.dirFilter)] != self.dirFilter: return else: self.displayEntry([entry]) def displayEntry(self, entries): ## entries should be a list of log entries ## for thread-safetyness: isGuiThread = Qt.QThread.currentThread( ) == Qt.QCoreApplication.instance().thread() if not isGuiThread: self.sigDisplayEntry.emit(entries) return for entry in entries: if id(entry) not in self.cache: self.cache[id(entry)] = self.generateEntryHtml(entry) ## determine message color: #if entry['msgType'] == 'status': #color = 'green' #elif entry['msgType'] == 'user': #color = 'blue' #elif entry['msgType'] == 'error': #color = 'red' #elif entry['msgType'] == 'warning': #color = '#DD4400' ## orange #else: #color = 'black' #if entry.has_key('exception') or entry.has_key('docs') or entry.has_key('reasons'): ##self.displayComplexMessage(entry, color) #self.displayComplexMessage(entry) #else: #self.displayText(entry['message'], entry, color, timeStamp=entry['timestamp']) #for x in self.cache[id(entry)]: #self.ui.output.appendHtml(x) html = self.cache[id(entry)] #frame = self.ui.logView.page().currentFrame() #isMax = frame.scrollBarValue(Qt.Qt.Vertical) == frame.scrollBarMaximum(Qt.Qt.Vertical) sb = self.ui.output.verticalScrollBar() isMax = sb.value() == sb.maximum() #frame.findFirstElement('body').appendInside(html) self.ui.output.append(html) self.displayedEntries.append(entry) if isMax: ## can't scroll to end until the web frame has processed the html change #frame.setScrollBarValue(Qt.Qt.Vertical, frame.scrollBarMaximum(Qt.Qt.Vertical)) ## Calling processEvents anywhere inside an error handler is forbidden ## because this can lead to Qt complaining about paint() recursion. #Qt.QApplication.processEvents() #self.ui.output.scrollToAnchor(str(entry['id'])) self.sigScrollToAnchor.emit(str( entry['id'])) ## queued connection #self.ui.logView.update() def scrollToAnchor(self, anchor): self.ui.output.scrollToAnchor(anchor) def generateEntryHtml(self, entry): msg = self.cleanText(entry['message']) reasons = "" docs = "" exc = "" if 'reasons' in entry: reasons = self.formatReasonStrForHTML(entry['reasons']) if 'docs' in entry: docs = self.formatDocsStrForHTML(entry['docs']) if entry.get('exception', None) is not None: exc = self.formatExceptionForHTML(entry, entryId=entry['id']) extra = reasons + docs + exc if extra != "": #extra = "<div class='logExtra'>" + extra + "</div>" extra = "<table class='logExtra'><tr><td>" + extra + "</td></tr></table>" #return """ #<div class='entry'> #<div class='%s'> #<span class='timestamp'>%s</span> #<span class='message'>%s</span> #%s #</div> #</div> #""" % (entry['msgType'], entry['timestamp'], msg, extra) return """ <a name="%s"/><table class='entry'><tr><td> <table class='%s'><tr><td> <span class='timestamp'>%s</span> <span class='message'>%s</span> %s </td></tr></table> </td></tr></table> """ % (str( entry['id']), entry['msgType'], entry['timestamp'], msg, extra) #if entry.has_key('exception') or entry.has_key('docs') or entry.has_key('reasons'): ##self.displayComplexMessage(entry, color) #return self.generateComplex(entry) #else: #return self.generateSimple(entry['message'], entry, color, timeStamp=entry['timestamp']) ##self.displayText(entry['message'], entry, color, timeStamp=entry['timestamp']) @staticmethod def cleanText(text): text = re.sub(r'&', '&', text) text = re.sub(r'>', '>', text) text = re.sub(r'<', '<', text) text = re.sub(r'\n', '<br/>\n', text) return text #def displayText(self, msg, entry, colorStr='black', timeStamp=None, clean=True): #if clean: #msg = self.cleanText(msg) #if msg[-1:] == '\n': #msg = msg[:-1] #msg = '<br />'.join(msg.split('\n')) #if timeStamp is not None: #strn = '<b style="color:black"> %s </b> <span style="color:%s"> %s </span>' % (timeStamp, colorStr, msg) #else: #strn = '<span style="color:%s"> %s </span>' % (colorStr, msg) ##self.ui.output.appendHtml(strn) #self.cache[id(entry)].append(strn) #def displayComplexMessage(self, entry, color='black'): #self.displayText(entry['message'], entry, color, timeStamp = entry['timestamp'], clean=True) #if entry.has_key('reasons'): #reasons = self.formatReasonStrForHTML(entry['reasons']) #self.displayText(reasons, entry, 'black', clean=False) #if entry.has_key('docs'): #docs = self.formatDocsStrForHTML(entry['docs']) #self.displayText(docs, entry, 'black', clean=False) #if entry.get('exception', None) is not None: #self.displayException(entry['exception'], entry, 'black', tracebacks=entry.get('traceback', None)) def formatExceptionForHTML(self, entry, exception=None, count=1, entryId=None): ### Here, exception is a dict that holds the message, reasons, docs, traceback and oldExceptions (which are also dicts, with the same entries) ## the count and tracebacks keywords are for calling recursively if exception is None: exception = entry['exception'] #if tracebacks is None: #tracebacks = [] indent = 10 text = self.cleanText(exception['message']) text = re.sub(r'^HelpfulException: ', '', text) #if exception.has_key('oldExc'): #self.displayText(" "*indent + str(count)+'. ' + text, entry, color, clean=False) #else: #self.displayText(" "*indent + str(count)+'. Original error: ' + text, entry, color, clean=False) messages = [text] #print "\n", messages, "\n" if 'reasons' in exception: reasons = self.formatReasonsStrForHTML(exception['reasons']) text += reasons #self.displayText(reasons, entry, color, clean=False) if 'docs' in exception: docs = self.formatDocsStrForHTML(exception['docs']) #self.displayText(docs, entry, color, clean=False) text += docs traceback = [ self.formatTracebackForHTML(exception['traceback'], count) ] text = [text] if 'oldExc' in exception: exc, tb, msgs = self.formatExceptionForHTML(entry, exception['oldExc'], count=count + 1) text.extend(exc) messages.extend(msgs) traceback.extend(tb) #else: #if len(tracebacks)==count+1: #n=0 #else: #n=1 #for i, tb in enumerate(tracebacks): #self.displayTraceback(tb, entry, number=i+n) if count == 1: exc = "<div class=\"exception\"><ol>" + "\n".join( ["<li>%s</li>" % ex for ex in text]) + "</ol></div>" tbStr = "\n".join([ "<li><b>%s</b><br/><span class='traceback'>%s</span></li>" % (messages[i], tb) for i, tb in enumerate(traceback) ]) #traceback = "<div class=\"traceback\" id=\"%s\"><ol>"%str(entryId) + tbStr + "</ol></div>" entry['tracebackHtml'] = tbStr #return exc + '<a href="#" onclick="showDiv(\'%s\')">Show traceback</a>'%str(entryId) + traceback return exc + '<a href="exc:%s">Show traceback %s</a>' % ( str(entryId), str(entryId)) else: return text, traceback, messages def formatTracebackForHTML(self, tb, number): try: tb = [ line for line in tb if not line.startswith("Traceback (most recent call last)") ] except: print("\n" + str(tb) + "\n") raise return re.sub(" ", " ", ("").join(map(self.cleanText, tb)))[:-1] #tb = [self.cleanText(strip(x)) for x in tb] #lines = [] #prefix = '' #for l in ''.join(tb).split('\n'): #if l == '': #continue #if l[:9] == "Traceback": #prefix = ' ' + str(number) + '. ' #continue #spaceCount = 0 #while l[spaceCount] == ' ': #spaceCount += 1 #if prefix is not '': #spaceCount -= 1 #lines.append(" "*(spaceCount*4) + prefix + l) #prefix = '' #return '<div class="traceback">' + '<br />'.join(lines) + '</div>' #self.displayText('<br />'.join(lines), entry, color, clean=False) def formatReasonsStrForHTML(self, reasons): #indent = 6 reasonStr = "<table class='reasons'><tr><td>Possible reasons include:\n<ul>\n" for r in reasons: r = self.cleanText(r) reasonStr += "<li>" + r + "</li>\n" #reasonStr += " "*22 + chr(97+i) + ". " + r + "<br>" reasonStr += "</ul></td></tr></table>\n" return reasonStr def formatDocsStrForHTML(self, docs): #indent = 6 docStr = "<div class='docRefs'>Relevant documentation:\n<ul>\n" for d in docs: d = self.cleanText(d) docStr += "<li><a href=\"doc:%s\">%s</a></li>\n" % (d, d) docStr += "</ul></div>\n" return docStr def exportHtml(self, fileName=False): #self.makeError1() if fileName is False: self.fileDialog = FileDialog(self, "Save HTML as...", self.manager.getCurrentDir().name()) #self.fileDialog.setFileMode(Qt.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(Qt.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.exportHtml) return if fileName[-5:] != '.html': fileName += '.html' #doc = self.ui.output.document().toHtml('utf-8') #for e in self.displayedEntries: #if e.has_key('tracebackHtml'): #doc = re.sub(r'<a href="exc:%s">(<[^>]+>)*Show traceback %s(<[^>]+>)*</a>'%(str(e['id']), str(e['id'])), e['tracebackHtml'], doc) global pageTemplate doc = pageTemplate for e in self.displayedEntries: doc += self.cache[id(e)] for e in self.displayedEntries: if 'tracebackHtml' in e: doc = re.sub( r'<a href="exc:%s">(<[^>]+>)*Show traceback %s(<[^>]+>)*</a>' % (str(e['id']), str(e['id'])), e['tracebackHtml'], doc) #doc = self.ui.logView.page().currentFrame().toHtml() f = open(fileName, 'w') f.write(doc.encode('utf-8')) f.close() def makeError1(self): ### just for testing error logging try: self.makeError2() #print x except: t, exc, tb = sys.exc_info() #logExc(message="This button doesn't work", reasons='reason a, reason b', docs='documentation') #if isinstance(exc, HelpfulException): #exc.prependErr("Button doesn't work", (t,exc,tb), reasons = ["It's supposed to raise an error for testing purposes", "You're doing it wrong."]) #raise #else: printExc("This is the message sent to printExc.") #raise HelpfulException(message='This button does not work.', exc=(t, exc, tb), reasons=["It's supposed to raise an error for testing purposes", "You're doing it wrong."]) def makeError2(self): ### just for testing error logging try: print(y) except: t, exc, tb = sys.exc_info() raise HelpfulException(message='msg from makeError', exc=(t, exc, tb), reasons=["reason one", "reason 2"], docs=['what, you expect documentation?']) def linkClicked(self, url): url = url.toString() if url[:4] == 'doc:': self.manager.showDocumentation(url[4:]) elif url[:4] == 'exc:': cursor = self.ui.output.document().find('Show traceback %s' % url[4:]) try: tb = self.entries[int(url[4:]) - 1]['tracebackHtml'] except IndexError: try: tb = self.entries[self.entryArray[ self.entryArray['entryId'] == ( int(url[4:]))]['index']]['tracebackHtml'] except: print("requested index %d, but only %d entries exist." % (int(url[4:]) - 1, len(self.entries))) raise cursor.insertHtml(tb) def clear(self): #self.ui.logView.setHtml("") self.ui.output.clear() self.displayedEntryies = []