eric6/Graphics/ApplicationDiagramBuilder.py

Mon, 03 May 2021 19:58:28 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 03 May 2021 19:58:28 +0200
changeset 8287
30eb7bc13d63
parent 8286
62ae22eae123
child 8291
3d79b1e5bf3c
permissions
-rw-r--r--

Changed some source docu string to the new style.

# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog showing an imports diagram of the application.
"""

import os
import glob

from PyQt5.QtWidgets import QApplication, QInputDialog

from E5Gui import E5MessageBox
from E5Gui.E5ProgressDialog import E5ProgressDialog

from .UMLDiagramBuilder import UMLDiagramBuilder

import Utilities
import Preferences


class ApplicationDiagramBuilder(UMLDiagramBuilder):
    """
    Class implementing a builder for imports diagrams of the application.
    """
    def __init__(self, dialog, view, project, noModules=False):
        """
        Constructor
        
        @param dialog reference to the UML dialog
        @type UMLDialog
        @param view reference to the view object
        @type UMLGraphicsView
        @param project reference to the project object
        @type Project
        @param noModules flag indicating, that no module names should be
            shown
        @type bool
        """
        super().__init__(dialog, view, project)
        self.setObjectName("ApplicationDiagram")
        
        self.noModules = noModules
        
        self.umlView.setDiagramName(
            self.tr("Application Diagram {0}").format(
                self.project.getProjectName()))
        
    def __buildModulesDict(self):
        """
        Private method to build a dictionary of modules contained in the
        application.
        
        @return dictionary of modules contained in the application
        @rtype dict
        """
        import Utilities.ModuleParser
        extensions = (
            Preferences.getPython("Python3Extensions") +
            ['.rb']
        )
        moduleDict = {}
        mods = self.project.pdata["SOURCES"]
        modules = []
        for module in mods:
            modules.append(Utilities.normabsjoinpath(
                self.project.ppath, module))
        tot = len(modules)
        progress = E5ProgressDialog(
            self.tr("Parsing modules..."),
            None, 0, tot, self.tr("%v/%m Modules"), self.parent())
        progress.setWindowTitle(self.tr("Application Diagram"))
        try:
            progress.show()
            QApplication.processEvents()
            
            for prog, module in enumerate(modules):
                progress.setValue(prog)
                QApplication.processEvents()
                if module.endswith("__init__.py"):
                    continue
                try:
                    mod = Utilities.ModuleParser.readModule(
                        module, extensions=extensions, caching=False)
                except ImportError:
                    continue
                else:
                    name = mod.name
                    moduleDict[name] = mod
        finally:
            progress.setValue(tot)
            progress.deleteLater()
        return moduleDict
    
    def __findApplicationRoot(self):
        """
        Private method to find the application root path.
        
        @return application root path
        @rtype str
        """
        candidates = []
        path = self.project.getProjectPath()
        init = os.path.join(path, "__init__.py")
        if os.path.exists(init):
            # project is a package
            return path
        else:
            # check, if one of the top directories is a package
            for entry in os.listdir(path):
                if entry.startswith("."):
                    # ignore hidden files and directories
                    continue
                
                fullpath = os.path.join(path, entry)
                if os.path.isdir(fullpath):
                    init = os.path.join(fullpath, "__init__.py")
                    if os.path.exists(init):
                        candidates.append(fullpath)
            
            if len(candidates) == 1:
                return candidates[0]
            elif len(candidates) > 1:
                root, ok = QInputDialog.getItem(
                    None,
                    self.tr("Application Diagram"),
                    self.tr("Select the application directory:"),
                    sorted(candidates),
                    0, True)
                if ok:
                    return root
            else:
                E5MessageBox.warning(
                    None,
                    self.tr("Application Diagram"),
                    self.tr("""No application package could be detected."""
                            """ Aborting..."""))
        return None
        
    def buildDiagram(self):
        """
        Public method to build the packages shapes of the diagram.
        """
        rpath = self.__findApplicationRoot()
        if rpath is None:
            # no root path detected
            return
        
        root = os.path.splitdrive(rpath)[1].replace(os.sep, '.')[1:]
        
        packages = {}
        self.__shapes = {}
        
        modules = self.__buildModulesDict()
        
        # step 1: build a dictionary of packages
        for module in sorted(modules.keys()):
            if "." in module:
                packageName, moduleName = module.rsplit(".", 1)
            else:
                packageName, moduleName = "", module
            if packageName in packages:
                packages[packageName][0].append(moduleName)
            else:
                packages[packageName] = ([moduleName], [])
                
        # step 2: assign modules to dictionaries and update import relationship
        for module in sorted(modules.keys()):
            package = module.rsplit(".", 1)[0]
            impLst = []
            for moduleImport in modules[module].imports:
                if moduleImport in modules:
                    impLst.append(moduleImport)
                else:
                    if moduleImport.find('.') == -1:
                        n = "{0}.{1}".format(modules[module].package,
                                             moduleImport)
                        if n in modules:
                            impLst.append(n)
                        else:
                            n = "{0}.{1}".format(root, moduleImport)
                            if n in modules:
                                impLst.append(n)
                            elif n in packages:
                                n = "{0}.<<Dummy>>".format(n)
                                impLst.append(n)
                    else:
                        n = "{0}.{1}".format(root, moduleImport)
                        if n in modules:
                            impLst.append(n)
            for moduleImport in list(modules[module].from_imports.keys()):
                if moduleImport.startswith('.'):
                    dots = len(moduleImport) - len(moduleImport.lstrip('.'))
                    if dots == 1:
                        moduleImport = moduleImport[1:]
                    elif dots > 1:
                        packagePath = os.path.dirname(modules[module].file)
                        hasInit = True
                        ppath = packagePath
                        while hasInit:
                            ppath = os.path.dirname(ppath)
                            hasInit = len(glob.glob(os.path.join(
                                ppath, '__init__.*'))) > 0
                        shortPackage = (
                            packagePath.replace(ppath, '')
                            .replace(os.sep, '.')[1:]
                        )
                        packageList = shortPackage.split('.')[1:]
                        packageListLen = len(packageList)
                        moduleImport = '.'.join(
                            packageList[:packageListLen - dots + 1] +
                            [moduleImport[dots:]])
                
                if moduleImport in modules:
                    impLst.append(moduleImport)
                else:
                    if moduleImport.find('.') == -1:
                        n = "{0}.{1}".format(modules[module].package,
                                             moduleImport)
                        if n in modules:
                            impLst.append(n)
                        else:
                            n = "{0}.{1}".format(root, moduleImport)
                            if n in modules:
                                impLst.append(n)
                            elif n in packages:
                                n = "{0}.<<Dummy>>".format(n)
                                impLst.append(n)
                    else:
                        n = "{0}.{1}".format(root, moduleImport)
                        if n in modules:
                            impLst.append(n)
            for moduleImport in impLst:
                impPackage = moduleImport.rsplit(".", 1)[0]
                try:
                    if (
                        impPackage not in packages[package][1] and
                        impPackage != package
                    ):
                        packages[package][1].append(impPackage)
                except KeyError:
                    continue
        
        for package in sorted(packages.keys()):
            if package:
                relPackage = package.replace(root, '')
                if relPackage and relPackage[0] == '.':
                    relPackage = relPackage[1:]
                else:
                    relPackage = self.tr("<<Application>>")
            else:
                relPackage = self.tr("<<Others>>")
            shape = self.__addPackage(
                relPackage, packages[package][0], 0.0, 0.0)
            self.__shapes[package] = (shape, packages[package][1])
        
        # build a list of routes
        nodes = []
        routes = []
        for module in self.__shapes:
            nodes.append(module)
            for rel in self.__shapes[module][1]:
                route = (module, rel)
                if route not in routes:
                    routes.append(route)
        
        self.__arrangeNodes(nodes, routes[:])
        self.__createAssociations(routes)
        self.umlView.autoAdjustSceneSize(limit=True)
    
    def __addPackage(self, name, modules, x, y):
        """
        Private method to add a package to the diagram.
        
        @param name package name to be shown
        @type str
        @param modules list of module names contained in the package
        @type list of str
        @param x x-coordinate
        @type float
        @param y y-coordinate
        @type float
        @return reference to the package item
        @rtype PackageItem
        """
        from .PackageItem import PackageItem, PackageModel
        modules.sort()
        pm = PackageModel(name, modules)
        pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene,
                         colors=self.umlView.getDrawingColors())
        pw.setId(self.umlView.getItemId())
        return pw
    
    def __arrangeNodes(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
        @type list of str
        @param routes list of routes
        @type list of tuple of (str, str)
        @param whiteSpaceFactor factor to increase whitespace between
            items
        @type 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.__shapes[child][0].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.height() > currentHeight:
                    currentHeight = rect.height()
                currentWidth += rect.width()
                
            # update totals
            if currentWidth > width:
                width = currentWidth
            height += currentHeight
            
            # store generation info
            widths.append(currentWidth)
            heights.append(currentHeight)
        
        # add in some whitespace
        width *= whiteSpaceFactor
        height = height * whiteSpaceFactor - 20
        verticalWhiteSpace = 40.0
        
        sceneRect = self.umlView.sceneRect()
        width += 50.0
        height += 50.0
        swidth = sceneRect.width() if width < sceneRect.width() else width
        sheight = sceneRect.height() if height < sceneRect.height() else 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(reversed(widths), reversed(heights), reversed(generations))
        ):
            x = 10.0
            # whiteSpace is the space between any two elements
            whiteSpace = (
                (width - currentWidth - 20) /
                (len(generation) - 1.0 or 2.0)
            )
            for name in generation:
                shape = self.__shapes[name][0]
                shape.setPos(x, y)
                rect = shape.sceneBoundingRect()
                x = x + rect.width() + whiteSpace
            y = y + currentHeight + verticalWhiteSpace
    
    def __createAssociations(self, routes):
        """
        Private method to generate the associations between the module shapes.
        
        @param routes list of associations
        @type list of tuple of (str, str)
        """
        from .AssociationItem import AssociationItem, AssociationType
        for route in routes:
            assoc = AssociationItem(
                self.__shapes[route[0]][0],
                self.__shapes[route[1]][0],
                AssociationType.IMPORTS,
                colors=self.umlView.getDrawingColors())
            self.scene.addItem(assoc)
    
    def getPersistenceData(self):
        """
        Public method to get a string for data to be persisted.
        
        @return persisted data string
        @rtype str
        """
        return "project={0}, no_modules={1}".format(
            self.project.getProjectFile(), self.noModules)
    
    def parsePersistenceData(self, version, data):
        """
        Public method to parse persisted data.
        
        @param version version of the data
        @type str
        @param data persisted data to be parsed
        @type str
        @return flag indicating success
        @rtype bool
        """
        parts = data.split(", ")
        if (
            len(parts) != 2 or
            not parts[0].startswith("project=") or
            not parts[1].startswith("no_modules=")
        ):
            return False
        
        projectFile = parts[0].split("=", 1)[1].strip()
        if projectFile != self.project.getProjectFile():
            res = E5MessageBox.yesNo(
                None,
                self.tr("Load Diagram"),
                self.tr(
                    """<p>The diagram belongs to the project <b>{0}</b>."""
                    """ Shall this project be opened?</p>""").format(
                    projectFile))
            if res:
                self.project.openProject(projectFile)
            
        self.noModules = Utilities.toBool(parts[1].split("=", 1)[1].strip())
        
        self.initialize()
        
        return True

eric ide

mercurial