class ProfileGraphViewer(QWidget): """Profiling results as a graph""" sigEscapePressed = pyqtSignal() def __init__(self, scriptName, params, reportTime, dataFile, stats, parent=None): QWidget.__init__(self, parent) self.__dataFile = dataFile self.__script = scriptName self.__reportTime = reportTime self.__params = params self.__stats = stats project = GlobalData().project if project.isLoaded(): self.__projectPrefix = os.path.dirname(project.fileName) else: self.__projectPrefix = os.path.dirname(scriptName) if not self.__projectPrefix.endswith(os.path.sep): self.__projectPrefix += os.path.sep self.__createLayout() self.__getDiagramLayout() self.__viewer.setScene(self.__scene) def setFocus(self): """Sets the focus properly""" self.__viewer.setFocus() def __isOutsideItem(self, fileName): """Detects if the record should be shown as an outside one""" return not fileName.startswith(self.__projectPrefix) def __createLayout(self): """Creates the widget layout""" totalCalls = self.__stats.total_calls # The calls were not induced via recursion totalPrimitiveCalls = self.__stats.prim_calls totalTime = self.__stats.total_tt txt = "<b>Script:</b> " + self.__script + " " + \ self.__params['arguments'] + "<br/>" \ "<b>Run at:</b> " + self.__reportTime + "<br/>" + \ str(totalCalls) + " function calls (" + \ str(totalPrimitiveCalls) + " primitive calls) in " + \ FLOAT_FORMAT % totalTime + " CPU seconds" summary = HeaderFitLabel(self) summary.setText(txt) summary.setToolTip(txt) summary.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) summary.setMinimumWidth(10) self.__scene = QGraphicsScene() self.__viewer = DiagramWidget() self.__viewer.sigEscapePressed.connect(self.__onESC) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) vLayout.addWidget(summary) vLayout.addWidget(self.__viewer) self.setLayout(vLayout) @staticmethod def __getDotFont(parts): """Provides a QFont object if a font spec is found""" for part in parts: if 'fontname=' in part: fontName = part.replace('fontname=', '') fontName = fontName.replace('[', '') fontName = fontName.replace(']', '') fontName = fontName.replace(',', '') return QFont(fontName) return None def __postprocessFullDotSpec(self, dotSpec): """Removes the arrow size, extracts tooltips, extracts font info""" nodeFont = None edgeFont = None tooltips = {} processed = [] for line in dotSpec.splitlines(): parts = line.split() lineModified = False if parts: if parts[0] == 'node': # need to extract the fontname nodeFont = self.__getDotFont(parts) elif parts[0] == 'edge': # need to extract the fontname edgeFont = self.__getDotFont(parts) elif parts[0].isdigit(): if parts[1] == '->': # certain edge spec: replace arrowsize and font size for index, part in enumerate(parts): if part.startswith('[arrowsize='): modified = parts[:] modified[index] = '[arrowsize="0.0",' processed.append(' '.join(modified)) elif part.startswith('fontsize='): size = float(part.split('"')[1]) if edgeFont: edgeFont.setPointSize(size) lineModified = True elif parts[1].startswith('['): # certain node spec: pick the tooltip and font size lineno = None for part in parts: if part.startswith('tooltip='): nodePath = part.split('"')[1] pathLine = nodePath + ':' + str(lineno) tooltips[int(parts[0])] = pathLine elif part.startswith('fontsize='): size = float(part.split('"')[1]) if nodeFont: nodeFont.setPointSize(size) elif part.startswith('label='): try: lineno = int(part.split(':')[1]) except: pass if not lineModified: processed.append(line) return '\n'.join(processed), tooltips, nodeFont, edgeFont def __rungprof2dot(self): """Runs gprof2dot which produces a full dot spec""" nodeLimit = Settings().getProfilerSettings().nodeLimit edgeLimit = Settings().getProfilerSettings().edgeLimit with io.StringIO() as buf: gprofParser = gprof2dot.PstatsParser(self.__dataFile) profileData = gprofParser.parse() profileData.prune(nodeLimit / 100.0, edgeLimit / 100.0, False, False) dot = gprof2dot.DotWriter(buf) dot.strip = False dot.wrap = False dot.graph(profileData, gprof2dot.TEMPERATURE_COLORMAP) output = buf.getvalue() return self.__postprocessFullDotSpec(output) def __getDiagramLayout(self): """Runs external tools to get the diagram layout""" fullDotSpec, tooltips, nodeFont, edgeFont = self.__rungprof2dot() dotProc = Popen(["dot", "-Tplain"], stdin=PIPE, stdout=PIPE, bufsize=1) graphDescr = dotProc.communicate( fullDotSpec.encode('utf-8'))[0].decode('utf-8') graph = getGraphFromPlainDotData(graphDescr) graph.normalize(self.physicalDpiX(), self.physicalDpiY()) self.__scene.clear() self.__scene.setSceneRect(0, 0, graph.width, graph.height) for edge in graph.edges: self.__scene.addItem(FuncConnection(edge)) if edge.label != "": self.__scene.addItem(FuncConnectionLabel(edge, edgeFont)) for node in graph.nodes: fileName = "" lineNumber = 0 try: nodeNameAsInt = int(node.name) if nodeNameAsInt in tooltips: parts = tooltips[nodeNameAsInt].rsplit(':', 1) fileName = parts[0] if parts[1].isdigit(): lineNumber = int(parts[1]) except: pass self.__scene.addItem( Function(node, fileName, lineNumber, self.__isOutsideItem(fileName), nodeFont)) def __onESC(self): """Triggered when ESC is clicked""" self.sigEscapePressed.emit() def onCopy(self): """Copies the diagram to the exchange buffer""" self.__viewer.onCopy() def onSaveAs(self, fileName): """Saves the diagram to a file""" self.__viewer.onSaveAs(fileName) def zoomIn(self): """Triggered on the 'zoom in' button""" self.__viewer.zoomIn() def zoomOut(self): """Triggered on the 'zoom out' button""" self.__viewer.zoomOut() def resetZoom(self): """Triggered on the 'zoom reset' button""" self.__viewer.resetZoom()
class ImportsDiagramProgress(QDialog): """Progress of the diagram generator""" def __init__(self, what, options, path="", buf="", parent=None): QDialog.__init__(self, parent) self.__cancelRequest = False self.__inProgress = False self.__what = what self.__options = options self.__path = path # could be a dir or a file self.__buf = buf # content in case of a modified file # Working process data self.__participantFiles = [] # Collected list of files self.__projectImportDirs = [] self.__projectImportsCache = {} # utils.settings -> /full/path/to.py self.__dirsToImportsCache = {} # /dir/path -> { my.mod: path.py, ... } self.dataModel = ImportDiagramModel() self.scene = QGraphicsScene() # Avoid pylint complains self.progressBar = None self.infoLabel = None self.__createLayout() self.setWindowTitle('Imports/dependencies diagram generator') QTimer.singleShot(0, self.__process) def keyPressEvent(self, event): """Processes the ESC key specifically""" if event.key() == Qt.Key_Escape: self.__onClose() else: QDialog.keyPressEvent(self, event) def __createLayout(self): """Creates the dialog layout""" self.resize(450, 20) self.setSizeGripEnabled(True) verticalLayout = QVBoxLayout(self) # Info label self.infoLabel = QLabel(self) verticalLayout.addWidget(self.infoLabel) # Progress bar self.progressBar = QProgressBar(self) self.progressBar.setValue(0) self.progressBar.setOrientation(Qt.Horizontal) verticalLayout.addWidget(self.progressBar) # Buttons buttonBox = QDialogButtonBox(self) buttonBox.setOrientation(Qt.Horizontal) buttonBox.setStandardButtons(QDialogButtonBox.Close) verticalLayout.addWidget(buttonBox) buttonBox.rejected.connect(self.__onClose) def __onClose(self): """triggered when the close button is clicked""" self.__cancelRequest = True if not self.__inProgress: self.close() def __buildParticipants(self): """Builds a list of participating files and dirs""" if self.__what in [ ImportsDiagramDialog.SingleBuffer, ImportsDiagramDialog.SingleFile ]: # File exists but could be modified self.__path = os.path.realpath(self.__path) self.__participantFiles.append(self.__path) return if self.__what == ImportsDiagramDialog.ProjectFiles: self.__scanProjectDirs() return # This is a recursive directory self.__path = os.path.realpath(self.__path) self.__scanDirForPythonFiles(self.__path + os.path.sep) def __scanDirForPythonFiles(self, path): """Scans the directory for the python files recursively""" for item in os.listdir(path): if item in [".svn", ".cvs", '.git', '.hg']: continue if os.path.isdir(path + item): self.__scanDirForPythonFiles(path + item + os.path.sep) continue if isPythonFile(path + item): self.__participantFiles.append(os.path.realpath(path + item)) def __scanProjectDirs(self): """Populates participant lists from the project files""" for fName in GlobalData().project.filesList: if isPythonFile(fName): self.__participantFiles.append(fName) def __addBoxInfo(self, box, info): """Adds information to the given box if so configured""" if info.docstring is not None: box.docstring = info.docstring.text if self.__options.includeClasses: for klass in info.classes: box.classes.append(klass) if self.__options.includeFuncs: for func in info.functions: box.funcs.append(func) if self.__options.includeGlobs: for glob in info.globals: box.globs.append(glob) if self.__options.includeConnText: for imp in info.imports: box.imports.append(imp) def __addDocstringBox(self, info, fName, modBoxName): """Adds a docstring box if needed""" if self.__options.includeDocs: if info.docstring is not None: docBox = DgmDocstring() docBox.docstring = info.docstring docBox.refFile = fName # Add the box and its connection docBoxName = self.dataModel.addDocstringBox(docBox) conn = DgmConnection() conn.kind = DgmConnection.ModuleDoc conn.source = modBoxName conn.target = docBoxName self.dataModel.addConnection(conn) # Add rank for better layout rank = DgmRank() rank.firstObj = modBoxName rank.secondObj = docBoxName self.dataModel.addRank(rank) def __getSytemWideImportDocstring(self, path): """Provides the system wide module docstring""" if isPythonFile(path): try: info = GlobalData().briefModinfoCache.get(path) if info.docstring is not None: return info.docstring.text except: pass return '' @staticmethod def __getModuleTitle(fName): """Extracts a module name out of the file name""" baseTitle = os.path.basename(fName).split('.')[0] if baseTitle != "__init__": return baseTitle # __init__ is not very descriptive. Add a top level dir. dirName = os.path.dirname(fName) topDir = os.path.basename(dirName) return topDir + "(" + baseTitle + ")" @staticmethod def __isLocalOrProject(fName, resolvedPath): """True if the module is a project one or is in the nested dirs""" if resolvedPath is None: return False if not os.path.isabs(resolvedPath): return False if GlobalData().project.isProjectFile(resolvedPath): return True resolvedDir = os.path.dirname(resolvedPath) baseDir = os.path.dirname(fName) return resolvedDir.startswith(baseDir) def __addSingleFileToDataModel(self, info, fName): """Adds a single file to the data model""" if fName.endswith('__init__.py'): if not info.classes and not info.functions and \ not info.globals and not info.imports: # Skip dummy init files return modBox = DgmModule() modBox.refFile = fName modBox.kind = DgmModule.ModuleOfInterest modBox.title = self.__getModuleTitle(fName) self.__addBoxInfo(modBox, info) modBoxName = self.dataModel.addModule(modBox) self.__addDocstringBox(info, fName, modBoxName) # Analyze what was imported resolvedImports, errors = resolveImports(fName, info.imports) if errors: message = 'Errors while analyzing ' + fName + ':' for err in errors: message += '\n ' + err logging.warning(message) for item in resolvedImports: importName = item[0] # from name resolvedPath = item[1] # 'built-in', None or absolute path importedNames = item[2] # list of strings impBox = DgmModule() impBox.title = importName if self.__isLocalOrProject(fName, resolvedPath): impBox.kind = DgmModule.OtherProjectModule impBox.refFile = resolvedPath if isPythonFile(resolvedPath): otherInfo = GlobalData().briefModinfoCache.get( resolvedPath) self.__addBoxInfo(impBox, otherInfo) else: if resolvedPath is None: # e.g. 'import sys' will have None for the path impBox.kind = DgmModule.UnknownModule elif os.path.isabs(resolvedPath): impBox.kind = DgmModule.SystemWideModule impBox.refFile = resolvedPath impBox.docstring = \ self.__getSytemWideImportDocstring(resolvedPath) else: # e.g. 'import time' will have 'built-in' in the path impBox.kind = DgmModule.BuiltInModule impBoxName = self.dataModel.addModule(impBox) impConn = DgmConnection() impConn.kind = DgmConnection.ModuleDependency impConn.source = modBoxName impConn.target = impBoxName if self.__options.includeConnText: for impWhat in importedNames: if impWhat: impConn.labels.append(impWhat) self.dataModel.addConnection(impConn) def __process(self): """Accumulation process""" # Intermediate working data self.__participantFiles = [] self.__projectImportDirs = [] self.__projectImportsCache = {} self.dataModel.clear() self.__inProgress = True try: self.infoLabel.setText('Building the list of files to analyze...') QApplication.processEvents() # Build the list of participating python files self.__buildParticipants() self.__projectImportDirs = \ GlobalData().project.getImportDirsAsAbsolutePaths() QApplication.processEvents() if self.__cancelRequest: QApplication.restoreOverrideCursor() self.close() return self.progressBar.setRange(0, len(self.__participantFiles)) index = 1 # Now, parse the files and build the diagram data model if self.__what == ImportsDiagramDialog.SingleBuffer: info = getBriefModuleInfoFromMemory(str(self.__buf)) self.__addSingleFileToDataModel(info, self.__path) else: infoSrc = GlobalData().briefModinfoCache for fName in self.__participantFiles: self.progressBar.setValue(index) self.infoLabel.setText('Analyzing ' + fName + "...") QApplication.processEvents() if self.__cancelRequest: QApplication.restoreOverrideCursor() self.dataModel.clear() self.close() return info = infoSrc.get(fName) self.__addSingleFileToDataModel(info, fName) index += 1 # The import caches and other working data are not needed anymore self.__participantFiles = None self.__projectImportDirs = None self.__projectImportsCache = None # Generating the graphviz layout self.infoLabel.setText('Generating layout using graphviz...') QApplication.processEvents() graph = getGraphFromDescriptionData(self.dataModel.toGraphviz()) graph.normalize(self.physicalDpiX(), self.physicalDpiY()) QApplication.processEvents() if self.__cancelRequest: QApplication.restoreOverrideCursor() self.dataModel.clear() self.close() return # Generate graphics scene self.infoLabel.setText('Generating graphics scene...') QApplication.processEvents() self.__buildGraphicsScene(graph) # Clear the data model self.dataModel = None except Exception as exc: QApplication.restoreOverrideCursor() logging.error(str(exc)) self.__inProgress = False self.__onClose() return QApplication.restoreOverrideCursor() self.infoLabel.setText('Done') QApplication.processEvents() self.__inProgress = False self.accept() def __buildGraphicsScene(self, graph): """Builds the QT graphics scene""" self.scene.clear() self.scene.setSceneRect(0, 0, graph.width, graph.height) for edge in graph.edges: # self.scene.addItem( GraphicsEdge( edge, self ) ) dataModelObj = self.dataModel.findConnection(edge.tail, edge.head) if dataModelObj is None: raise Exception("Cannot find the following connection: " + edge.tail + " -> " + edge.head) if dataModelObj.kind == DgmConnection.ModuleDoc: modObj = self.dataModel.findModule(dataModelObj.source) if modObj is None: raise Exception("Cannot find module object: " + dataModelObj.source) self.scene.addItem(ImportsDgmDocConn(edge, modObj)) continue if dataModelObj.kind == DgmConnection.ModuleDependency: # Find the source module object first modObj = self.dataModel.findModule(dataModelObj.source) if modObj is None: raise Exception("Cannot find module object: " + dataModelObj.source) self.scene.addItem( ImportsDgmDependConn(edge, modObj, dataModelObj)) if edge.label != "": self.scene.addItem(ImportsDgmEdgeLabel(edge, modObj)) continue raise Exception("Unexpected type of connection: " + str(dataModelObj.kind)) for node in graph.nodes: dataModelObj = self.dataModel.findModule(node.name) if dataModelObj is None: dataModelObj = self.dataModel.findDocstring(node.name) if dataModelObj is None: raise Exception("Cannot find object " + node.name) if isinstance(dataModelObj, DgmDocstring): self.scene.addItem( ImportsDgmDocNote(node, dataModelObj.refFile, dataModelObj.docstring)) continue # OK, this is a module rectangle. Switch by type of the module. if dataModelObj.kind == DgmModule.ModuleOfInterest: self.scene.addItem( ImportsDgmModuleOfInterest(node, dataModelObj.refFile, dataModelObj, self.physicalDpiX())) elif dataModelObj.kind == DgmModule.OtherProjectModule: self.scene.addItem( ImportsDgmOtherPrjModule(node, dataModelObj.refFile, dataModelObj, self.physicalDpiX())) elif dataModelObj.kind == DgmModule.SystemWideModule: self.scene.addItem( ImportsDgmSystemWideModule(node, dataModelObj.refFile, dataModelObj.docstring)) elif dataModelObj.kind == DgmModule.BuiltInModule: self.scene.addItem(ImportsDgmBuiltInModule(node)) elif dataModelObj.kind == DgmModule.UnknownModule: self.scene.addItem(ImportsDgmUnknownModule(node)) else: raise Exception("Unexpected type of module: " + str(dataModelObj.kind)) tooltip = dataModelObj.getTooltip() if tooltip: pixmap = getPixmap('diagramdoc.png') docItem = QGraphicsPixmapItem(pixmap) docItem.setToolTip(tooltip) posX = node.posX + node.width / 2.0 - pixmap.width() / 2.0 posY = node.posY - node.height / 2.0 - pixmap.height() / 2.0 docItem.setPos(posX, posY) self.scene.addItem(docItem)
class CustomColorsDialog(QDialog): """Custom colors dialog implementation""" def __init__(self, bgcolor, fgcolor, bordercolor, parent=None): """colors are instances of QColor""" QDialog.__init__(self, parent) self.setWindowTitle('Custom colors') self.__createLayout() self.__bgColorButton.setColor(bgcolor) self.__bgColorButton.sigColorChanged.connect(self.__onColorChanged) self.__fgColorButton.setColor(fgcolor) self.__fgColorButton.sigColorChanged.connect(self.__onColorChanged) self.__borderColorButton.setColor(bordercolor) self.__borderColorButton.sigColorChanged.connect(self.__onColorChanged) QTimer.singleShot(1, self.__onColorChanged) def __createLayout(self): """Creates the dialog layout""" self.setMinimumWidth(300) self.setMinimumHeight(250) self.resize(400, 300) self.setSizeGripEnabled(True) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(5, 5, 5, 5) gridLayout = QGridLayout() bgLabel = QLabel('Select background color:', self) gridLayout.addWidget(bgLabel, 0, 0, 1, 1) self.__bgColorButton = ColorButton('', self) gridLayout.addWidget(self.__bgColorButton, 0, 1, 1, 1) fgLabel = QLabel('Select foreground color:', self) gridLayout.addWidget(fgLabel, 1, 0, 1, 1) self.__fgColorButton = ColorButton('', self) gridLayout.addWidget(self.__fgColorButton, 1, 1, 1, 1) borderLabel = QLabel('Select border color (*):', self) gridLayout.addWidget(borderLabel, 2, 0, 1, 1) self.__borderColorButton = ColorButton('', self) gridLayout.addWidget(self.__borderColorButton, 2, 1, 1, 1) verticalLayout.addLayout(gridLayout) verticalLayout.addWidget( QLabel('(*): docstrings use it only when shown as badges')) # Sample area self.__scene = QGraphicsScene() self.__view = QGraphicsView() self.__view.setScene(self.__scene) verticalLayout.addWidget(self.__view) # Buttons at the bottom buttonBox = QDialogButtonBox(self) buttonBox.setOrientation(Qt.Horizontal) buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) verticalLayout.addWidget(buttonBox) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) def __onColorChanged(self): """The user changed the color so redraw the sample""" viewWidth = self.__view.width() viewHeight = self.__view.height() self.__scene.clear() # without '-4' scrollbar will appear self.__scene.setSceneRect(0, 0, viewWidth - 4, viewHeight - 4) block = SampleBlock(getCflowSettings(self), self.backgroundColor(), self.foregroundColor(), self.borderColor(), viewWidth, viewHeight) self.__scene.addItem(block) self.__scene.update() def backgroundColor(self): """Provides the background color""" return self.__bgColorButton.color() def foregroundColor(self): """Provides the foreground color""" return self.__fgColorButton.color() def borderColor(self): """Provides the border color""" return self.__borderColorButton.color()