eric6/Graphics/PackageDiagramBuilder.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing a UML like class diagram of a package.
8 """
9
10 from __future__ import unicode_literals
11
12 import glob
13 import os.path
14 try: # Py3
15 from itertools import zip_longest
16 except ImportError:
17 from itertools import izip_longest as zip_longest # __IGNORE_WARNING__
18
19 from PyQt5.QtWidgets import QApplication, QGraphicsTextItem
20
21 from E5Gui.E5ProgressDialog import E5ProgressDialog
22
23 from .UMLDiagramBuilder import UMLDiagramBuilder
24
25 import Utilities
26 import Preferences
27
28
29 class PackageDiagramBuilder(UMLDiagramBuilder):
30 """
31 Class implementing a builder for UML like class diagrams of a package.
32 """
33 def __init__(self, dialog, view, project, package, noAttrs=False):
34 """
35 Constructor
36
37 @param dialog reference to the UML dialog (UMLDialog)
38 @param view reference to the view object (UMLGraphicsView)
39 @param project reference to the project object (Project)
40 @param package name of a python package to be shown (string)
41 @keyparam noAttrs flag indicating, that no attributes should be shown
42 (boolean)
43 """
44 super(PackageDiagramBuilder, self).__init__(dialog, view, project)
45 self.setObjectName("PackageDiagram")
46
47 self.package = Utilities.normabspath(package)
48 self.noAttrs = noAttrs
49
50 def initialize(self):
51 """
52 Public method to initialize the object.
53 """
54 pname = self.project.getProjectName()
55 if pname:
56 name = self.tr("Package Diagram {0}: {1}").format(
57 pname, self.project.getRelativePath(self.package))
58 else:
59 name = self.tr("Package Diagram: {0}").format(self.package)
60 self.umlView.setDiagramName(name)
61
62 def __getCurrentShape(self, name):
63 """
64 Private method to get the named shape.
65
66 @param name name of the shape (string)
67 @return shape (QCanvasItem)
68 """
69 return self.allClasses.get(name)
70
71 def __buildModulesDict(self):
72 """
73 Private method to build a dictionary of modules contained in the
74 package.
75
76 @return dictionary of modules contained in the package.
77 """
78 import Utilities.ModuleParser
79
80 supportedExt = \
81 ['*{0}'.format(ext) for ext in
82 Preferences.getPython("PythonExtensions")] + \
83 ['*{0}'.format(ext) for ext in
84 Preferences.getPython("Python3Extensions")] + \
85 ['*.rb']
86 extensions = Preferences.getPython("PythonExtensions") + \
87 Preferences.getPython("Python3Extensions") + ['.rb']
88
89 moduleDict = {}
90 modules = []
91 for ext in supportedExt:
92 modules.extend(glob.glob(
93 Utilities.normjoinpath(self.package, ext)))
94 tot = len(modules)
95 progress = E5ProgressDialog(
96 self.tr("Parsing modules..."),
97 None, 0, tot, self.tr("%v/%m Modules"), self.parent())
98 progress.setWindowTitle(self.tr("Package Diagram"))
99 try:
100 prog = 0
101 progress.show()
102 QApplication.processEvents()
103
104 for module in modules:
105 progress.setValue(prog)
106 QApplication.processEvents()
107 prog += 1
108 try:
109 mod = Utilities.ModuleParser.readModule(
110 module, extensions=extensions, caching=False)
111 except ImportError:
112 continue
113 else:
114 name = mod.name
115 if name.startswith(self.package):
116 name = name[len(self.package) + 1:]
117 moduleDict[name] = mod
118 finally:
119 progress.setValue(tot)
120 progress.deleteLater()
121 return moduleDict
122
123 def __buildSubpackagesDict(self):
124 """
125 Private method to build a dictionary of sub-packages contained in this
126 package.
127
128 @return dictionary of sub-packages contained in this package
129 """
130 import Utilities.ModuleParser
131
132 supportedExt = \
133 ['*{0}'.format(ext) for ext in
134 Preferences.getPython("PythonExtensions")] + \
135 ['*{0}'.format(ext) for ext in
136 Preferences.getPython("Python3Extensions")] + \
137 ['*.rb']
138 extensions = Preferences.getPython("PythonExtensions") + \
139 Preferences.getPython("Python3Extensions") + ['.rb']
140
141 subpackagesDict = {}
142 subpackagesList = []
143
144 for subpackage in os.listdir(self.package):
145 subpackagePath = os.path.join(self.package, subpackage)
146 if os.path.isdir(subpackagePath) and \
147 subpackage != "__pycache__" and \
148 len(glob.glob(os.path.join(subpackagePath, "__init__.*"))) != 0:
149 subpackagesList.append(subpackagePath)
150
151 tot = 0
152 for ext in supportedExt:
153 for subpackage in subpackagesList:
154 tot += len(glob.glob(Utilities.normjoinpath(subpackage, ext)))
155 progress = E5ProgressDialog(
156 self.tr("Parsing modules..."),
157 None, 0, tot, self.tr("%v/%m Modules"), self.parent())
158 progress.setWindowTitle(self.tr("Package Diagram"))
159 try:
160 prog = 0
161 progress.show()
162 QApplication.processEvents()
163
164 for subpackage in subpackagesList:
165 packageName = os.path.basename(subpackage)
166 subpackagesDict[packageName] = []
167 modules = []
168 for ext in supportedExt:
169 modules.extend(glob.glob(
170 Utilities.normjoinpath(subpackage, ext)))
171 for module in modules:
172 progress.setValue(prog)
173 QApplication.processEvents()
174 prog += 1
175 try:
176 mod = Utilities.ModuleParser.readModule(
177 module, extensions=extensions, caching=False)
178 except ImportError:
179 continue
180 else:
181 name = mod.name
182 if "." in name:
183 name = name.rsplit(".", 1)[1]
184 subpackagesDict[packageName].append(name)
185 subpackagesDict[packageName].sort()
186 # move __init__ to the front
187 if "__init__" in subpackagesDict[packageName]:
188 subpackagesDict[packageName].remove("__init__")
189 subpackagesDict[packageName].insert(0, "__init__")
190 finally:
191 progress.setValue(tot)
192 progress.deleteLater()
193 return subpackagesDict
194
195 def buildDiagram(self):
196 """
197 Public method to build the class shapes of the package diagram.
198
199 The algorithm is borrowed from Boa Constructor.
200 """
201 self.allClasses = {}
202
203 initlist = glob.glob(os.path.join(self.package, '__init__.*'))
204 if len(initlist) == 0:
205 ct = QGraphicsTextItem(None)
206 self.scene.addItem(ct)
207 ct.setHtml(
208 self.tr("The directory <b>'{0}'</b> is not a package.")
209 .format(self.package))
210 return
211
212 modules = self.__buildModulesDict()
213 if not modules:
214 ct = QGraphicsTextItem(None)
215 self.scene.addItem(ct)
216 ct.setHtml(
217 self.tr(
218 "The package <b>'{0}'</b> does not contain any modules.")
219 .format(self.package))
220 return
221
222 # step 1: build all classes found in the modules
223 classesFound = False
224
225 for modName in list(modules.keys()):
226 module = modules[modName]
227 for cls in list(module.classes.keys()):
228 classesFound = True
229 self.__addLocalClass(cls, module.classes[cls], 0, 0)
230 if not classesFound:
231 ct = QGraphicsTextItem(None)
232 self.scene.addItem(ct)
233 ct.setHtml(
234 self.tr(
235 "The package <b>'{0}'</b> does not contain any classes.")
236 .format(self.package))
237 return
238
239 # step 2: build the class hierarchies
240 routes = []
241 nodes = []
242
243 for modName in list(modules.keys()):
244 module = modules[modName]
245 todo = [module.createHierarchy()]
246 while todo:
247 hierarchy = todo[0]
248 for className in list(hierarchy.keys()):
249 cw = self.__getCurrentShape(className)
250 if not cw and className.find('.') >= 0:
251 cw = self.__getCurrentShape(className.split('.')[-1])
252 if cw:
253 self.allClasses[className] = cw
254 if cw and cw.noAttrs != self.noAttrs:
255 cw = None
256 if cw and not (cw.external and
257 (className in module.classes or
258 className in module.modules)
259 ):
260 if className not in nodes:
261 nodes.append(className)
262 else:
263 if className in module.classes:
264 # this is a local class (defined in this module)
265 self.__addLocalClass(
266 className, module.classes[className],
267 0, 0)
268 elif className in module.modules:
269 # this is a local module (defined in this module)
270 self.__addLocalClass(
271 className, module.modules[className],
272 0, 0, True)
273 else:
274 self.__addExternalClass(className, 0, 0)
275 nodes.append(className)
276
277 if hierarchy.get(className):
278 todo.append(hierarchy.get(className))
279 children = list(hierarchy.get(className).keys())
280 for child in children:
281 if (className, child) not in routes:
282 routes.append((className, child))
283
284 del todo[0]
285
286 # step 3: build the subpackages
287 subpackages = self.__buildSubpackagesDict()
288 for subpackage in sorted(subpackages.keys()):
289 self.__addPackage(subpackage, subpackages[subpackage], 0, 0)
290 nodes.append(subpackage)
291
292 self.__arrangeClasses(nodes, routes[:])
293 self.__createAssociations(routes)
294 self.umlView.autoAdjustSceneSize(limit=True)
295
296 def __arrangeClasses(self, nodes, routes, whiteSpaceFactor=1.2):
297 """
298 Private method to arrange the shapes on the canvas.
299
300 The algorithm is borrowed from Boa Constructor.
301
302 @param nodes list of nodes to arrange
303 @param routes list of routes
304 @param whiteSpaceFactor factor to increase whitespace between
305 items (float)
306 """
307 from . import GraphicsUtilities
308 generations = GraphicsUtilities.sort(nodes, routes)
309
310 # calculate width and height of all elements
311 sizes = []
312 for generation in generations:
313 sizes.append([])
314 for child in generation:
315 sizes[-1].append(
316 self.__getCurrentShape(child).sceneBoundingRect())
317
318 # calculate total width and total height
319 width = 0
320 height = 0
321 widths = []
322 heights = []
323 for generation in sizes:
324 currentWidth = 0
325 currentHeight = 0
326
327 for rect in generation:
328 if rect.bottom() > currentHeight:
329 currentHeight = rect.bottom()
330 currentWidth = currentWidth + rect.right()
331
332 # update totals
333 if currentWidth > width:
334 width = currentWidth
335 height = height + currentHeight
336
337 # store generation info
338 widths.append(currentWidth)
339 heights.append(currentHeight)
340
341 # add in some whitespace
342 width = width * whiteSpaceFactor
343 height = height * whiteSpaceFactor - 20
344 verticalWhiteSpace = 40.0
345
346 sceneRect = self.umlView.sceneRect()
347 width += 50.0
348 height += 50.0
349 swidth = width < sceneRect.width() and sceneRect.width() or width
350 sheight = height < sceneRect.height() and sceneRect.height() or height
351 self.umlView.setSceneSize(swidth, sheight)
352
353 # distribute each generation across the width and the
354 # generations across height
355 y = 10.0
356 for currentWidth, currentHeight, generation in \
357 zip_longest(widths, heights, generations):
358 x = 10.0
359 # whiteSpace is the space between any two elements
360 whiteSpace = (width - currentWidth - 20) / \
361 (len(generation) - 1.0 or 2.0)
362 for className in generation:
363 cw = self.__getCurrentShape(className)
364 cw.setPos(x, y)
365 rect = cw.sceneBoundingRect()
366 x = x + rect.width() + whiteSpace
367 y = y + currentHeight + verticalWhiteSpace
368
369 def __addLocalClass(self, className, _class, x, y, isRbModule=False):
370 """
371 Private method to add a class defined in the module.
372
373 @param className name of the class to be as a dictionary key (string)
374 @param _class class to be shown (ModuleParser.Class)
375 @param x x-coordinate (float)
376 @param y y-coordinate (float)
377 @param isRbModule flag indicating a Ruby module (boolean)
378 """
379 from .ClassItem import ClassItem, ClassModel
380 meths = sorted(_class.methods.keys())
381 attrs = sorted(_class.attributes.keys())
382 name = _class.name
383 if isRbModule:
384 name = "{0} (Module)".format(name)
385 cl = ClassModel(name, meths[:], attrs[:])
386 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene)
387 cw.setId(self.umlView.getItemId())
388 self.allClasses[className] = cw
389
390 def __addExternalClass(self, _class, x, y):
391 """
392 Private method to add a class defined outside the module.
393
394 If the canvas is too small to take the shape, it
395 is enlarged.
396
397 @param _class class to be shown (string)
398 @param x x-coordinate (float)
399 @param y y-coordinate (float)
400 """
401 from .ClassItem import ClassItem, ClassModel
402 cl = ClassModel(_class)
403 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene)
404 cw.setId(self.umlView.getItemId())
405 self.allClasses[_class] = cw
406
407 def __addPackage(self, name, modules, x, y):
408 """
409 Private method to add a package to the diagram.
410
411 @param name package name to be shown (string)
412 @param modules list of module names contained in the package
413 (list of strings)
414 @param x x-coordinate (float)
415 @param y y-coordinate (float)
416 """
417 from .PackageItem import PackageItem, PackageModel
418 pm = PackageModel(name, modules)
419 pw = PackageItem(pm, x, y, scene=self.scene)
420 pw.setId(self.umlView.getItemId())
421 self.allClasses[name] = pw
422
423 def __createAssociations(self, routes):
424 """
425 Private method to generate the associations between the class shapes.
426
427 @param routes list of relationsships
428 """
429 from .AssociationItem import AssociationItem, Generalisation
430 for route in routes:
431 if len(route) > 1:
432 assoc = AssociationItem(
433 self.__getCurrentShape(route[1]),
434 self.__getCurrentShape(route[0]),
435 Generalisation,
436 topToBottom=True)
437 self.scene.addItem(assoc)
438
439 def getPersistenceData(self):
440 """
441 Public method to get a string for data to be persisted.
442
443 @return persisted data string (string)
444 """
445 return "package={0}, no_attributes={1}".format(
446 self.package, self.noAttrs)
447
448 def parsePersistenceData(self, version, data):
449 """
450 Public method to parse persisted data.
451
452 @param version version of the data (string)
453 @param data persisted data to be parsed (string)
454 @return flag indicating success (boolean)
455 """
456 parts = data.split(", ")
457 if len(parts) != 2 or \
458 not parts[0].startswith("package=") or \
459 not parts[1].startswith("no_attributes="):
460 return False
461
462 self.package = parts[0].split("=", 1)[1].strip()
463 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip())
464
465 self.initialize()
466
467 return True

eric ide

mercurial