--- a/eric6/Graphics/PackageDiagramBuilder.py Sat May 01 14:27:38 2021 +0200 +++ b/eric6/Graphics/PackageDiagramBuilder.py Thu Jun 03 11:39:23 2021 +0200 @@ -4,7 +4,7 @@ # """ -Module implementing a dialog showing a UML like class diagram of a package. +Module implementing a dialog showing an UML like class diagram of a package. """ import glob @@ -29,18 +29,28 @@ """ 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) + @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 be shown + @type str @param noAttrs flag indicating, that no attributes should be shown - (boolean) + @type bool """ super().__init__(dialog, view, project) self.setObjectName("PackageDiagram") self.package = os.path.abspath(package) self.noAttrs = noAttrs + + self.__relPackage = ( + self.project.getRelativePath(self.package) + if self.project.isProjectSource(self.package) else + "" + ) def initialize(self): """ @@ -59,8 +69,10 @@ """ Private method to get the named shape. - @param name name of the shape (string) - @return shape (QCanvasItem) + @param name name of the shape + @type str + @return shape + @rtype QCanvasItem """ return self.allClasses.get(name) @@ -69,7 +81,8 @@ Private method to build a dictionary of modules contained in the package. - @return dictionary of modules contained in the package. + @return dictionary of modules contained in the package + @rtype dict """ import Utilities.ModuleParser @@ -121,6 +134,7 @@ package. @return dictionary of sub-packages contained in this package + @rtype dict """ import Utilities.ModuleParser @@ -208,13 +222,15 @@ return modules = self.__buildModulesDict() - if not modules: + subpackages = self.__buildSubpackagesDict() + + if not modules and not subpackages: ct = QGraphicsTextItem(None) self.scene.addItem(ct) - ct.setHtml( - self.tr( - "The package <b>'{0}'</b> does not contain any modules.") - .format(self.package)) + ct.setHtml(self.buildErrorMessage( + self.tr("The package <b>'{0}'</b> does not contain any modules" + " or subpackages.").format(self.package) + )) return # step 1: build all classes found in the modules @@ -225,13 +241,13 @@ for cls in list(module.classes.keys()): classesFound = True self.__addLocalClass(cls, module.classes[cls], 0, 0) - if not classesFound: + if not classesFound and not subpackages: ct = QGraphicsTextItem(None) self.scene.addItem(ct) - ct.setHtml( - self.tr( - "The package <b>'{0}'</b> does not contain any classes.") - .format(self.package)) + ct.setHtml(self.buildErrorMessage( + self.tr("The package <b>'{0}'</b> does not contain any" + " classes or subpackages.").format(self.package) + )) return # step 2: build the class hierarchies @@ -243,7 +259,7 @@ todo = [module.createHierarchy()] while todo: hierarchy = todo[0] - for className in list(hierarchy.keys()): + for className in hierarchy: cw = self.__getCurrentShape(className) if not cw and className.find('.') >= 0: cw = self.__getCurrentShape(className.split('.')[-1]) @@ -282,7 +298,6 @@ 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) @@ -298,9 +313,12 @@ 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 (float) + items + @type float """ from . import GraphicsUtilities generations = GraphicsUtilities.sort(nodes, routes) @@ -335,7 +353,7 @@ # store generation info widths.append(currentWidth) heights.append(currentHeight) - + # add in some whitespace width *= whiteSpaceFactor height = height * whiteSpaceFactor - 20 @@ -344,8 +362,8 @@ 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 + 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 @@ -371,19 +389,27 @@ """ 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) + @param className name of the class to be as a dictionary key + @type str + @param _class class to be shown + @type ModuleParser.Class + @param x x-coordinate + @type float + @param y y-coordinate + @type float + @param isRbModule flag indicating a Ruby module + @type bool """ 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[:]) + cl = ClassModel( + name, + sorted(_class.methods.keys())[:], + sorted(_class.attributes.keys())[:], + sorted(_class.globals.keys())[:] + ) cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene, colors=self.umlView.getDrawingColors()) cw.setId(self.umlView.getItemId()) @@ -396,9 +422,12 @@ 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) + @param _class class to be shown + @type ModuleParser.Class + @param x x-coordinate + @type float + @param y y-coordinate + @type float """ from .ClassItem import ClassItem, ClassModel cl = ClassModel(_class) @@ -411,11 +440,14 @@ """ Private method to add a package to the diagram. - @param name package name to be shown (string) + @param name package name to be shown + @type str @param modules list of module names contained in the package - (list of strings) - @param x x-coordinate (float) - @param y y-coordinate (float) + @type list of str + @param x x-coordinate + @type float + @param y y-coordinate + @type float """ from .PackageItem import PackageItem, PackageModel pm = PackageModel(name, modules) @@ -429,6 +461,7 @@ Private method to generate the associations between the class shapes. @param routes list of relationsships + @type list of tuple of (str, str) """ from .AssociationItem import AssociationItem, AssociationType for route in routes: @@ -445,7 +478,8 @@ """ Public method to get a string for data to be persisted. - @return persisted data string (string) + @return persisted data string + @rtype str """ return "package={0}, no_attributes={1}".format( self.package, self.noAttrs) @@ -454,9 +488,12 @@ """ 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) + @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 ( @@ -472,3 +509,60 @@ 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(), + "no_attributes": self.noAttrs, + } + + data["package"] = ( + Utilities.fromNativeSeparators(self.__relPackage) + if self.__relPackage else + Utilities.fromNativeSeparators(self.package) + ) + + 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.noAttrs = data["no_attributes"] + + package = Utilities.toNativeSeparators(data["package"]) + if os.path.isabs(package): + self.package = package + self.__relPackage = "" + 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.__relPackage = package + self.package = self.project.getAbsolutePath(package) + except KeyError: + return False, "" + + self.initialize() + + return True, ""