예제 #1
0
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()
예제 #2
0
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)
예제 #3
0
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()