eric6/Graphics/PackageDiagramBuilder.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Graphics/PackageDiagramBuilder.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,467 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog showing a UML like class diagram of a package.
+"""
+
+from __future__ import unicode_literals
+
+import glob
+import os.path
+try:  # Py3
+    from itertools import zip_longest
+except ImportError:
+    from itertools import izip_longest as zip_longest    # __IGNORE_WARNING__
+
+from PyQt5.QtWidgets import QApplication, QGraphicsTextItem
+
+from E5Gui.E5ProgressDialog import E5ProgressDialog
+
+from .UMLDiagramBuilder import UMLDiagramBuilder
+
+import Utilities
+import Preferences
+
+
+class PackageDiagramBuilder(UMLDiagramBuilder):
+    """
+    Class implementing a builder for UML like class diagrams of a package.
+    """
+    def __init__(self, dialog, view, project, package, noAttrs=False):
+        """
+        Constructor
+        
+        @param dialog reference to the UML dialog (UMLDialog)
+        @param view reference to the view object (UMLGraphicsView)
+        @param project reference to the project object (Project)
+        @param package name of a python package to be shown (string)
+        @keyparam noAttrs flag indicating, that no attributes should be shown
+            (boolean)
+        """
+        super(PackageDiagramBuilder, self).__init__(dialog, view, project)
+        self.setObjectName("PackageDiagram")
+        
+        self.package = Utilities.normabspath(package)
+        self.noAttrs = noAttrs
+    
+    def initialize(self):
+        """
+        Public method to initialize the object.
+        """
+        pname = self.project.getProjectName()
+        if pname:
+            name = self.tr("Package Diagram {0}: {1}").format(
+                pname, self.project.getRelativePath(self.package))
+        else:
+            name = self.tr("Package Diagram: {0}").format(self.package)
+        self.umlView.setDiagramName(name)
+    
+    def __getCurrentShape(self, name):
+        """
+        Private method to get the named shape.
+        
+        @param name name of the shape (string)
+        @return shape (QCanvasItem)
+        """
+        return self.allClasses.get(name)
+    
+    def __buildModulesDict(self):
+        """
+        Private method to build a dictionary of modules contained in the
+        package.
+        
+        @return dictionary of modules contained in the package.
+        """
+        import Utilities.ModuleParser
+        
+        supportedExt = \
+            ['*{0}'.format(ext) for ext in
+             Preferences.getPython("PythonExtensions")] + \
+            ['*{0}'.format(ext) for ext in
+             Preferences.getPython("Python3Extensions")] + \
+            ['*.rb']
+        extensions = Preferences.getPython("PythonExtensions") + \
+            Preferences.getPython("Python3Extensions") + ['.rb']
+        
+        moduleDict = {}
+        modules = []
+        for ext in supportedExt:
+            modules.extend(glob.glob(
+                Utilities.normjoinpath(self.package, ext)))
+        tot = len(modules)
+        progress = E5ProgressDialog(
+            self.tr("Parsing modules..."),
+            None, 0, tot, self.tr("%v/%m Modules"), self.parent())
+        progress.setWindowTitle(self.tr("Package Diagram"))
+        try:
+            prog = 0
+            progress.show()
+            QApplication.processEvents()
+            
+            for module in modules:
+                progress.setValue(prog)
+                QApplication.processEvents()
+                prog += 1
+                try:
+                    mod = Utilities.ModuleParser.readModule(
+                        module, extensions=extensions, caching=False)
+                except ImportError:
+                    continue
+                else:
+                    name = mod.name
+                    if name.startswith(self.package):
+                        name = name[len(self.package) + 1:]
+                    moduleDict[name] = mod
+        finally:
+            progress.setValue(tot)
+            progress.deleteLater()
+        return moduleDict
+    
+    def __buildSubpackagesDict(self):
+        """
+        Private method to build a dictionary of sub-packages contained in this
+        package.
+        
+        @return dictionary of sub-packages contained in this package
+        """
+        import Utilities.ModuleParser
+        
+        supportedExt = \
+            ['*{0}'.format(ext) for ext in
+             Preferences.getPython("PythonExtensions")] + \
+            ['*{0}'.format(ext) for ext in
+             Preferences.getPython("Python3Extensions")] + \
+            ['*.rb']
+        extensions = Preferences.getPython("PythonExtensions") + \
+            Preferences.getPython("Python3Extensions") + ['.rb']
+        
+        subpackagesDict = {}
+        subpackagesList = []
+        
+        for subpackage in os.listdir(self.package):
+            subpackagePath = os.path.join(self.package, subpackage)
+            if os.path.isdir(subpackagePath) and \
+               subpackage != "__pycache__" and \
+               len(glob.glob(os.path.join(subpackagePath, "__init__.*"))) != 0:
+                subpackagesList.append(subpackagePath)
+        
+        tot = 0
+        for ext in supportedExt:
+            for subpackage in subpackagesList:
+                tot += len(glob.glob(Utilities.normjoinpath(subpackage, ext)))
+        progress = E5ProgressDialog(
+            self.tr("Parsing modules..."),
+            None, 0, tot, self.tr("%v/%m Modules"), self.parent())
+        progress.setWindowTitle(self.tr("Package Diagram"))
+        try:
+            prog = 0
+            progress.show()
+            QApplication.processEvents()
+            
+            for subpackage in subpackagesList:
+                packageName = os.path.basename(subpackage)
+                subpackagesDict[packageName] = []
+                modules = []
+                for ext in supportedExt:
+                    modules.extend(glob.glob(
+                        Utilities.normjoinpath(subpackage, ext)))
+                for module in modules:
+                    progress.setValue(prog)
+                    QApplication.processEvents()
+                    prog += 1
+                    try:
+                        mod = Utilities.ModuleParser.readModule(
+                            module, extensions=extensions, caching=False)
+                    except ImportError:
+                        continue
+                    else:
+                        name = mod.name
+                        if "." in name:
+                            name = name.rsplit(".", 1)[1]
+                        subpackagesDict[packageName].append(name)
+                subpackagesDict[packageName].sort()
+                # move __init__ to the front
+                if "__init__" in subpackagesDict[packageName]:
+                    subpackagesDict[packageName].remove("__init__")
+                    subpackagesDict[packageName].insert(0, "__init__")
+        finally:
+            progress.setValue(tot)
+            progress.deleteLater()
+        return subpackagesDict
+    
+    def buildDiagram(self):
+        """
+        Public method to build the class shapes of the package diagram.
+        
+        The algorithm is borrowed from Boa Constructor.
+        """
+        self.allClasses = {}
+        
+        initlist = glob.glob(os.path.join(self.package, '__init__.*'))
+        if len(initlist) == 0:
+            ct = QGraphicsTextItem(None)
+            self.scene.addItem(ct)
+            ct.setHtml(
+                self.tr("The directory <b>'{0}'</b> is not a package.")
+                    .format(self.package))
+            return
+        
+        modules = self.__buildModulesDict()
+        if not modules:
+            ct = QGraphicsTextItem(None)
+            self.scene.addItem(ct)
+            ct.setHtml(
+                self.tr(
+                    "The package <b>'{0}'</b> does not contain any modules.")
+                .format(self.package))
+            return
+            
+        # step 1: build all classes found in the modules
+        classesFound = False
+        
+        for modName in list(modules.keys()):
+            module = modules[modName]
+            for cls in list(module.classes.keys()):
+                classesFound = True
+                self.__addLocalClass(cls, module.classes[cls], 0, 0)
+        if not classesFound:
+            ct = QGraphicsTextItem(None)
+            self.scene.addItem(ct)
+            ct.setHtml(
+                self.tr(
+                    "The package <b>'{0}'</b> does not contain any classes.")
+                .format(self.package))
+            return
+        
+        # step 2: build the class hierarchies
+        routes = []
+        nodes = []
+        
+        for modName in list(modules.keys()):
+            module = modules[modName]
+            todo = [module.createHierarchy()]
+            while todo:
+                hierarchy = todo[0]
+                for className in list(hierarchy.keys()):
+                    cw = self.__getCurrentShape(className)
+                    if not cw and className.find('.') >= 0:
+                        cw = self.__getCurrentShape(className.split('.')[-1])
+                        if cw:
+                            self.allClasses[className] = cw
+                    if cw and cw.noAttrs != self.noAttrs:
+                        cw = None
+                    if cw and not (cw.external and
+                                   (className in module.classes or
+                                    className in module.modules)
+                                   ):
+                        if className not in nodes:
+                            nodes.append(className)
+                    else:
+                        if className in module.classes:
+                            # this is a local class (defined in this module)
+                            self.__addLocalClass(
+                                className, module.classes[className],
+                                0, 0)
+                        elif className in module.modules:
+                            # this is a local module (defined in this module)
+                            self.__addLocalClass(
+                                className, module.modules[className],
+                                0, 0, True)
+                        else:
+                            self.__addExternalClass(className, 0, 0)
+                        nodes.append(className)
+                    
+                    if hierarchy.get(className):
+                        todo.append(hierarchy.get(className))
+                        children = list(hierarchy.get(className).keys())
+                        for child in children:
+                            if (className, child) not in routes:
+                                routes.append((className, child))
+                
+                del todo[0]
+        
+        # step 3: build the subpackages
+        subpackages = self.__buildSubpackagesDict()
+        for subpackage in sorted(subpackages.keys()):
+            self.__addPackage(subpackage, subpackages[subpackage], 0, 0)
+            nodes.append(subpackage)
+        
+        self.__arrangeClasses(nodes, routes[:])
+        self.__createAssociations(routes)
+        self.umlView.autoAdjustSceneSize(limit=True)
+    
+    def __arrangeClasses(self, nodes, routes, whiteSpaceFactor=1.2):
+        """
+        Private method to arrange the shapes on the canvas.
+        
+        The algorithm is borrowed from Boa Constructor.
+        
+        @param nodes list of nodes to arrange
+        @param routes list of routes
+        @param whiteSpaceFactor factor to increase whitespace between
+            items (float)
+        """
+        from . import GraphicsUtilities
+        generations = GraphicsUtilities.sort(nodes, routes)
+        
+        # calculate width and height of all elements
+        sizes = []
+        for generation in generations:
+            sizes.append([])
+            for child in generation:
+                sizes[-1].append(
+                    self.__getCurrentShape(child).sceneBoundingRect())
+                
+        # calculate total width and total height
+        width = 0
+        height = 0
+        widths = []
+        heights = []
+        for generation in sizes:
+            currentWidth = 0
+            currentHeight = 0
+            
+            for rect in generation:
+                if rect.bottom() > currentHeight:
+                    currentHeight = rect.bottom()
+                currentWidth = currentWidth + rect.right()
+                
+            # update totals
+            if currentWidth > width:
+                width = currentWidth
+            height = height + currentHeight
+            
+            # store generation info
+            widths.append(currentWidth)
+            heights.append(currentHeight)
+            
+        # add in some whitespace
+        width = width * whiteSpaceFactor
+        height = height * whiteSpaceFactor - 20
+        verticalWhiteSpace = 40.0
+        
+        sceneRect = self.umlView.sceneRect()
+        width += 50.0
+        height += 50.0
+        swidth = width < sceneRect.width() and sceneRect.width() or width
+        sheight = height < sceneRect.height() and sceneRect.height() or height
+        self.umlView.setSceneSize(swidth, sheight)
+        
+        # distribute each generation across the width and the
+        # generations across height
+        y = 10.0
+        for currentWidth, currentHeight, generation in \
+                zip_longest(widths, heights, generations):
+            x = 10.0
+            # whiteSpace is the space between any two elements
+            whiteSpace = (width - currentWidth - 20) / \
+                (len(generation) - 1.0 or 2.0)
+            for className in generation:
+                cw = self.__getCurrentShape(className)
+                cw.setPos(x, y)
+                rect = cw.sceneBoundingRect()
+                x = x + rect.width() + whiteSpace
+            y = y + currentHeight + verticalWhiteSpace
+    
+    def __addLocalClass(self, className, _class, x, y, isRbModule=False):
+        """
+        Private method to add a class defined in the module.
+        
+        @param className name of the class to be as a dictionary key (string)
+        @param _class class to be shown (ModuleParser.Class)
+        @param x x-coordinate (float)
+        @param y y-coordinate (float)
+        @param isRbModule flag indicating a Ruby module (boolean)
+        """
+        from .ClassItem import ClassItem, ClassModel
+        meths = sorted(_class.methods.keys())
+        attrs = sorted(_class.attributes.keys())
+        name = _class.name
+        if isRbModule:
+            name = "{0} (Module)".format(name)
+        cl = ClassModel(name, meths[:], attrs[:])
+        cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene)
+        cw.setId(self.umlView.getItemId())
+        self.allClasses[className] = cw
+    
+    def __addExternalClass(self, _class, x, y):
+        """
+        Private method to add a class defined outside the module.
+        
+        If the canvas is too small to take the shape, it
+        is enlarged.
+        
+        @param _class class to be shown (string)
+        @param x x-coordinate (float)
+        @param y y-coordinate (float)
+        """
+        from .ClassItem import ClassItem, ClassModel
+        cl = ClassModel(_class)
+        cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene)
+        cw.setId(self.umlView.getItemId())
+        self.allClasses[_class] = cw
+    
+    def __addPackage(self, name, modules, x, y):
+        """
+        Private method to add a package to the diagram.
+        
+        @param name package name to be shown (string)
+        @param modules list of module names contained in the package
+            (list of strings)
+        @param x x-coordinate (float)
+        @param y y-coordinate (float)
+        """
+        from .PackageItem import PackageItem, PackageModel
+        pm = PackageModel(name, modules)
+        pw = PackageItem(pm, x, y, scene=self.scene)
+        pw.setId(self.umlView.getItemId())
+        self.allClasses[name] = pw
+    
+    def __createAssociations(self, routes):
+        """
+        Private method to generate the associations between the class shapes.
+        
+        @param routes list of relationsships
+        """
+        from .AssociationItem import AssociationItem, Generalisation
+        for route in routes:
+            if len(route) > 1:
+                assoc = AssociationItem(
+                    self.__getCurrentShape(route[1]),
+                    self.__getCurrentShape(route[0]),
+                    Generalisation,
+                    topToBottom=True)
+                self.scene.addItem(assoc)
+    
+    def getPersistenceData(self):
+        """
+        Public method to get a string for data to be persisted.
+        
+        @return persisted data string (string)
+        """
+        return "package={0}, no_attributes={1}".format(
+            self.package, self.noAttrs)
+    
+    def parsePersistenceData(self, version, data):
+        """
+        Public method to parse persisted data.
+        
+        @param version version of the data (string)
+        @param data persisted data to be parsed (string)
+        @return flag indicating success (boolean)
+        """
+        parts = data.split(", ")
+        if len(parts) != 2 or \
+           not parts[0].startswith("package=") or \
+           not parts[1].startswith("no_attributes="):
+            return False
+        
+        self.package = parts[0].split("=", 1)[1].strip()
+        self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip())
+        
+        self.initialize()
+        
+        return True

eric ide

mercurial