eric7/Graphics/ImportsDiagramBuilder.py

Sat, 22 May 2021 18:51:46 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 22 May 2021 18:51:46 +0200
branch
eric7
changeset 8356
68ec9c3d4de5
parent 8348
f4775ae8f441
child 8358
144a6b854f70
permissions
-rw-r--r--

Renamed the modules and classes of the E5Gui package to have the prefix 'Eric' instead of 'E5'.

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

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

"""
Module implementing a dialog showing an imports diagram of a package.
"""

import glob
import os

from PyQt6.QtWidgets import QApplication, QGraphicsTextItem

from E5Gui.EricProgressDialog import EricProgressDialog

from .UMLDiagramBuilder import UMLDiagramBuilder

import Utilities
import Preferences


class ImportsDiagramBuilder(UMLDiagramBuilder):
    """
    Class implementing a builder for imports diagrams of a package.
    
    Note: Only package internal imports are shown in order to maintain
    some readability.
    """
    def __init__(self, dialog, view, project, package,
                 showExternalImports=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 package name of a python package to show the import
            relationships
        @type str
        @param showExternalImports flag indicating to show exports from
            outside the package
        @type bool
        """
        super().__init__(dialog, view, project)
        self.setObjectName("ImportsDiagram")
        
        self.showExternalImports = showExternalImports
        self.packagePath = os.path.abspath(package)
        
        self.__relPackagePath = (
            self.project.getRelativePath(self.packagePath)
            if self.project.isProjectSource(self.packagePath) else
            ""
        )
    
    def initialize(self):
        """
        Public method to initialize the object.
        """
        self.package = os.path.splitdrive(self.packagePath)[1].replace(
            os.sep, '.')[1:]
        hasInit = True
        ppath = self.packagePath
        while hasInit:
            ppath = os.path.dirname(ppath)
            hasInit = len(glob.glob(os.path.join(ppath, '__init__.*'))) > 0
        self.shortPackage = self.packagePath.replace(ppath, '').replace(
            os.sep, '.')[1:]
        
        pname = self.project.getProjectName()
        name = (
            self.tr("Imports Diagramm {0}: {1}").format(
                pname, self.project.getRelativePath(self.packagePath))
            if pname else
            self.tr("Imports Diagramm: {0}").format(self.packagePath)
        )
        self.umlView.setDiagramName(name)
    
    def __buildModulesDict(self):
        """
        Private method to build a dictionary of modules contained in the
        package.
        
        @return dictionary of modules contained in the package
        @rtype dict
        """
        import Utilities.ModuleParser
        extensions = (
            Preferences.getPython("Python3Extensions")
        )
        moduleDict = {}
        modules = []
        for ext in (
            Preferences.getPython("Python3Extensions")
        ):
            modules.extend(glob.glob(Utilities.normjoinpath(
                self.packagePath, '*{0}'.format(ext))))
        
        tot = len(modules)
        progress = EricProgressDialog(
            self.tr("Parsing modules..."),
            None, 0, tot, self.tr("%v/%m Modules"), self.parent())
        progress.setWindowTitle(self.tr("Imports Diagramm"))
        try:
            progress.show()
            QApplication.processEvents()
            for prog, module in enumerate(modules):
                progress.setValue(prog)
                QApplication.processEvents()
                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 buildDiagram(self):
        """
        Public method to build the modules shapes of the diagram.
        """
        initlist = glob.glob(os.path.join(self.packagePath, '__init__.*'))
        if len(initlist) == 0:
            ct = QGraphicsTextItem(None)
            ct.setHtml(self.buildErrorMessage(
                self.tr("The directory <b>'{0}'</b> is not a Python"
                        " package.").format(self.package)
            ))
            self.scene.addItem(ct)
            return
        
        self.__shapes = {}
        
        modules = self.__buildModulesDict()
        externalMods = []
        packageList = self.shortPackage.split('.')
        packageListLen = len(packageList)
        for module in sorted(modules.keys()):
            impLst = []
            for importName in modules[module].imports:
                n = (
                    importName[len(self.package) + 1:]
                    if importName.startswith(self.package) else
                    importName
                )
                if importName in modules:
                    impLst.append(n)
                elif self.showExternalImports:
                    impLst.append(n)
                    if n not in externalMods:
                        externalMods.append(n)
            for importName in list(modules[module].from_imports.keys()):
                if importName.startswith('.'):
                    dots = len(importName) - len(importName.lstrip('.'))
                    if dots == 1:
                        n = importName[1:]
                        importName = n
                    else:
                        if self.showExternalImports:
                            n = '.'.join(
                                packageList[:packageListLen - dots + 1] +
                                [importName[dots:]])
                        else:
                            n = importName
                elif importName.startswith(self.package):
                    n = importName[len(self.package) + 1:]
                else:
                    n = importName
                if importName in modules:
                    impLst.append(n)
                elif self.showExternalImports:
                    impLst.append(n)
                    if n not in externalMods:
                        externalMods.append(n)
            
            classNames = []
            for class_ in list(modules[module].classes.keys()):
                className = modules[module].classes[class_].name
                if className not in classNames:
                    classNames.append(className)
            shape = self.__addModule(module, classNames, 0.0, 0.0)
            self.__shapes[module] = (shape, impLst)
        
        for module in externalMods:
            shape = self.__addModule(module, [], 0.0, 0.0)
            self.__shapes[module] = (shape, [])
        
        # 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 __addModule(self, name, classes, x, y):
        """
        Private method to add a module to the diagram.
        
        @param name module name to be shown
        @type str
        @param classes list of class names contained in the module
        @type list of str
        @param x x-coordinate
        @type float
        @param y y-coordinate
        @type float
        @return reference to the imports item
        @rtype ModuleItem
        """
        from .ModuleItem import ModuleItem, ModuleModel
        classes.sort()
        impM = ModuleModel(name, classes)
        impW = ModuleItem(impM, x, y, scene=self.scene,
                          colors=self.umlView.getDrawingColors())
        impW.setId(self.umlView.getItemId())
        return impW
    
    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 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("package=") or
            not parts[1].startswith("show_external=")
        ):
            return False
        
        self.packagePath = parts[0].split("=", 1)[1].strip()
        self.showExternalImports = Utilities.toBool(
            parts[1].split("=", 1)[1].strip())
        
        self.initialize()
        
        return True
    
    def toDict(self):
        """
        Public method to collect data to be persisted.
        
        @return dictionary containing data to be persisted
        @rtype dict
        """
        data = {
            "project_name": self.project.getProjectName(),
            "show_external": self.showExternalImports,
        }
        
        data["package"] = (
            Utilities.fromNativeSeparators(self.__relPackagePath)
            if self.__relPackagePath else
            Utilities.fromNativeSeparators(self.packagePath)
        )
        
        return data
    
    def fromDict(self, version, data):
        """
        Public method to populate the class with data persisted by 'toDict()'.
        
        @param version version of the data
        @type str
        @param data dictionary containing the persisted data
        @type dict
        @return tuple containing a flag indicating success and an info
            message in case the diagram belongs to a different project
        @rtype tuple of (bool, str)
        """
        try:
            self.showExternalImports = data["show_external"]
            
            packagePath = Utilities.toNativeSeparators(data["package"])
            if os.path.isabs(packagePath):
                self.packagePath = packagePath
                self.__relPackagePath = ""
            else:
                # relative package paths indicate a project package
                if data["project_name"] != self.project.getProjectName():
                    msg = self.tr(
                        "<p>The diagram belongs to project <b>{0}</b>."
                        " Please open it and try again.</p>"
                    ).format(data["project_name"])
                    return False, msg
                
                self.__relPackagePath = packagePath
                self.package = self.project.getAbsolutePath(packagePath)
        except KeyError:
            return False, ""
        
        self.initialize()
        
        return True, ""

eric ide

mercurial