eric6/Graphics/PackageDiagramBuilder.py

branch
maintenance
changeset 8400
b3eefd7e58d1
parent 8273
698ae46f40a4
parent 8295
3f5e8b0a338e
equal deleted inserted replaced
8274:197414ba11cc 8400:b3eefd7e58d1
2 2
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 # 4 #
5 5
6 """ 6 """
7 Module implementing a dialog showing a UML like class diagram of a package. 7 Module implementing a dialog showing an UML like class diagram of a package.
8 """ 8 """
9 9
10 import glob 10 import glob
11 import os.path 11 import os.path
12 from itertools import zip_longest 12 from itertools import zip_longest
27 """ 27 """
28 def __init__(self, dialog, view, project, package, noAttrs=False): 28 def __init__(self, dialog, view, project, package, noAttrs=False):
29 """ 29 """
30 Constructor 30 Constructor
31 31
32 @param dialog reference to the UML dialog (UMLDialog) 32 @param dialog reference to the UML dialog
33 @param view reference to the view object (UMLGraphicsView) 33 @type UMLDialog
34 @param project reference to the project object (Project) 34 @param view reference to the view object
35 @param package name of a python package to be shown (string) 35 @type UMLGraphicsView
36 @param project reference to the project object
37 @type Project
38 @param package name of a python package to be shown
39 @type str
36 @param noAttrs flag indicating, that no attributes should be shown 40 @param noAttrs flag indicating, that no attributes should be shown
37 (boolean) 41 @type bool
38 """ 42 """
39 super().__init__(dialog, view, project) 43 super().__init__(dialog, view, project)
40 self.setObjectName("PackageDiagram") 44 self.setObjectName("PackageDiagram")
41 45
42 self.package = os.path.abspath(package) 46 self.package = os.path.abspath(package)
43 self.noAttrs = noAttrs 47 self.noAttrs = noAttrs
48
49 self.__relPackage = (
50 self.project.getRelativePath(self.package)
51 if self.project.isProjectSource(self.package) else
52 ""
53 )
44 54
45 def initialize(self): 55 def initialize(self):
46 """ 56 """
47 Public method to initialize the object. 57 Public method to initialize the object.
48 """ 58 """
57 67
58 def __getCurrentShape(self, name): 68 def __getCurrentShape(self, name):
59 """ 69 """
60 Private method to get the named shape. 70 Private method to get the named shape.
61 71
62 @param name name of the shape (string) 72 @param name name of the shape
63 @return shape (QCanvasItem) 73 @type str
74 @return shape
75 @rtype QCanvasItem
64 """ 76 """
65 return self.allClasses.get(name) 77 return self.allClasses.get(name)
66 78
67 def __buildModulesDict(self): 79 def __buildModulesDict(self):
68 """ 80 """
69 Private method to build a dictionary of modules contained in the 81 Private method to build a dictionary of modules contained in the
70 package. 82 package.
71 83
72 @return dictionary of modules contained in the package. 84 @return dictionary of modules contained in the package
85 @rtype dict
73 """ 86 """
74 import Utilities.ModuleParser 87 import Utilities.ModuleParser
75 88
76 supportedExt = ( 89 supportedExt = (
77 ['*{0}'.format(ext) for ext in 90 ['*{0}'.format(ext) for ext in
119 """ 132 """
120 Private method to build a dictionary of sub-packages contained in this 133 Private method to build a dictionary of sub-packages contained in this
121 package. 134 package.
122 135
123 @return dictionary of sub-packages contained in this package 136 @return dictionary of sub-packages contained in this package
137 @rtype dict
124 """ 138 """
125 import Utilities.ModuleParser 139 import Utilities.ModuleParser
126 140
127 supportedExt = ( 141 supportedExt = (
128 ['*{0}'.format(ext) for ext in 142 ['*{0}'.format(ext) for ext in
206 self.tr("The directory <b>'{0}'</b> is not a package.") 220 self.tr("The directory <b>'{0}'</b> is not a package.")
207 .format(self.package)) 221 .format(self.package))
208 return 222 return
209 223
210 modules = self.__buildModulesDict() 224 modules = self.__buildModulesDict()
211 if not modules: 225 subpackages = self.__buildSubpackagesDict()
226
227 if not modules and not subpackages:
212 ct = QGraphicsTextItem(None) 228 ct = QGraphicsTextItem(None)
213 self.scene.addItem(ct) 229 self.scene.addItem(ct)
214 ct.setHtml( 230 ct.setHtml(self.buildErrorMessage(
215 self.tr( 231 self.tr("The package <b>'{0}'</b> does not contain any modules"
216 "The package <b>'{0}'</b> does not contain any modules.") 232 " or subpackages.").format(self.package)
217 .format(self.package)) 233 ))
218 return 234 return
219 235
220 # step 1: build all classes found in the modules 236 # step 1: build all classes found in the modules
221 classesFound = False 237 classesFound = False
222 238
223 for modName in list(modules.keys()): 239 for modName in list(modules.keys()):
224 module = modules[modName] 240 module = modules[modName]
225 for cls in list(module.classes.keys()): 241 for cls in list(module.classes.keys()):
226 classesFound = True 242 classesFound = True
227 self.__addLocalClass(cls, module.classes[cls], 0, 0) 243 self.__addLocalClass(cls, module.classes[cls], 0, 0)
228 if not classesFound: 244 if not classesFound and not subpackages:
229 ct = QGraphicsTextItem(None) 245 ct = QGraphicsTextItem(None)
230 self.scene.addItem(ct) 246 self.scene.addItem(ct)
231 ct.setHtml( 247 ct.setHtml(self.buildErrorMessage(
232 self.tr( 248 self.tr("The package <b>'{0}'</b> does not contain any"
233 "The package <b>'{0}'</b> does not contain any classes.") 249 " classes or subpackages.").format(self.package)
234 .format(self.package)) 250 ))
235 return 251 return
236 252
237 # step 2: build the class hierarchies 253 # step 2: build the class hierarchies
238 routes = [] 254 routes = []
239 nodes = [] 255 nodes = []
241 for modName in list(modules.keys()): 257 for modName in list(modules.keys()):
242 module = modules[modName] 258 module = modules[modName]
243 todo = [module.createHierarchy()] 259 todo = [module.createHierarchy()]
244 while todo: 260 while todo:
245 hierarchy = todo[0] 261 hierarchy = todo[0]
246 for className in list(hierarchy.keys()): 262 for className in hierarchy:
247 cw = self.__getCurrentShape(className) 263 cw = self.__getCurrentShape(className)
248 if not cw and className.find('.') >= 0: 264 if not cw and className.find('.') >= 0:
249 cw = self.__getCurrentShape(className.split('.')[-1]) 265 cw = self.__getCurrentShape(className.split('.')[-1])
250 if cw: 266 if cw:
251 self.allClasses[className] = cw 267 self.allClasses[className] = cw
280 routes.append((className, child)) 296 routes.append((className, child))
281 297
282 del todo[0] 298 del todo[0]
283 299
284 # step 3: build the subpackages 300 # step 3: build the subpackages
285 subpackages = self.__buildSubpackagesDict()
286 for subpackage in sorted(subpackages.keys()): 301 for subpackage in sorted(subpackages.keys()):
287 self.__addPackage(subpackage, subpackages[subpackage], 0, 0) 302 self.__addPackage(subpackage, subpackages[subpackage], 0, 0)
288 nodes.append(subpackage) 303 nodes.append(subpackage)
289 304
290 self.__arrangeClasses(nodes, routes[:]) 305 self.__arrangeClasses(nodes, routes[:])
296 Private method to arrange the shapes on the canvas. 311 Private method to arrange the shapes on the canvas.
297 312
298 The algorithm is borrowed from Boa Constructor. 313 The algorithm is borrowed from Boa Constructor.
299 314
300 @param nodes list of nodes to arrange 315 @param nodes list of nodes to arrange
316 @type list of str
301 @param routes list of routes 317 @param routes list of routes
318 @type list of tuple of (str, str)
302 @param whiteSpaceFactor factor to increase whitespace between 319 @param whiteSpaceFactor factor to increase whitespace between
303 items (float) 320 items
321 @type float
304 """ 322 """
305 from . import GraphicsUtilities 323 from . import GraphicsUtilities
306 generations = GraphicsUtilities.sort(nodes, routes) 324 generations = GraphicsUtilities.sort(nodes, routes)
307 325
308 # calculate width and height of all elements 326 # calculate width and height of all elements
333 height += currentHeight 351 height += currentHeight
334 352
335 # store generation info 353 # store generation info
336 widths.append(currentWidth) 354 widths.append(currentWidth)
337 heights.append(currentHeight) 355 heights.append(currentHeight)
338 356
339 # add in some whitespace 357 # add in some whitespace
340 width *= whiteSpaceFactor 358 width *= whiteSpaceFactor
341 height = height * whiteSpaceFactor - 20 359 height = height * whiteSpaceFactor - 20
342 verticalWhiteSpace = 40.0 360 verticalWhiteSpace = 40.0
343 361
344 sceneRect = self.umlView.sceneRect() 362 sceneRect = self.umlView.sceneRect()
345 width += 50.0 363 width += 50.0
346 height += 50.0 364 height += 50.0
347 swidth = width < sceneRect.width() and sceneRect.width() or width 365 swidth = sceneRect.width() if width < sceneRect.width() else width
348 sheight = height < sceneRect.height() and sceneRect.height() or height 366 sheight = sceneRect.height() if height < sceneRect.height() else height
349 self.umlView.setSceneSize(swidth, sheight) 367 self.umlView.setSceneSize(swidth, sheight)
350 368
351 # distribute each generation across the width and the 369 # distribute each generation across the width and the
352 # generations across height 370 # generations across height
353 y = 10.0 371 y = 10.0
369 387
370 def __addLocalClass(self, className, _class, x, y, isRbModule=False): 388 def __addLocalClass(self, className, _class, x, y, isRbModule=False):
371 """ 389 """
372 Private method to add a class defined in the module. 390 Private method to add a class defined in the module.
373 391
374 @param className name of the class to be as a dictionary key (string) 392 @param className name of the class to be as a dictionary key
375 @param _class class to be shown (ModuleParser.Class) 393 @type str
376 @param x x-coordinate (float) 394 @param _class class to be shown
377 @param y y-coordinate (float) 395 @type ModuleParser.Class
378 @param isRbModule flag indicating a Ruby module (boolean) 396 @param x x-coordinate
397 @type float
398 @param y y-coordinate
399 @type float
400 @param isRbModule flag indicating a Ruby module
401 @type bool
379 """ 402 """
380 from .ClassItem import ClassItem, ClassModel 403 from .ClassItem import ClassItem, ClassModel
381 meths = sorted(_class.methods.keys())
382 attrs = sorted(_class.attributes.keys())
383 name = _class.name 404 name = _class.name
384 if isRbModule: 405 if isRbModule:
385 name = "{0} (Module)".format(name) 406 name = "{0} (Module)".format(name)
386 cl = ClassModel(name, meths[:], attrs[:]) 407 cl = ClassModel(
408 name,
409 sorted(_class.methods.keys())[:],
410 sorted(_class.attributes.keys())[:],
411 sorted(_class.globals.keys())[:]
412 )
387 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene, 413 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene,
388 colors=self.umlView.getDrawingColors()) 414 colors=self.umlView.getDrawingColors())
389 cw.setId(self.umlView.getItemId()) 415 cw.setId(self.umlView.getItemId())
390 self.allClasses[className] = cw 416 self.allClasses[className] = cw
391 417
394 Private method to add a class defined outside the module. 420 Private method to add a class defined outside the module.
395 421
396 If the canvas is too small to take the shape, it 422 If the canvas is too small to take the shape, it
397 is enlarged. 423 is enlarged.
398 424
399 @param _class class to be shown (string) 425 @param _class class to be shown
400 @param x x-coordinate (float) 426 @type ModuleParser.Class
401 @param y y-coordinate (float) 427 @param x x-coordinate
428 @type float
429 @param y y-coordinate
430 @type float
402 """ 431 """
403 from .ClassItem import ClassItem, ClassModel 432 from .ClassItem import ClassItem, ClassModel
404 cl = ClassModel(_class) 433 cl = ClassModel(_class)
405 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene, 434 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene,
406 colors=self.umlView.getDrawingColors()) 435 colors=self.umlView.getDrawingColors())
409 438
410 def __addPackage(self, name, modules, x, y): 439 def __addPackage(self, name, modules, x, y):
411 """ 440 """
412 Private method to add a package to the diagram. 441 Private method to add a package to the diagram.
413 442
414 @param name package name to be shown (string) 443 @param name package name to be shown
444 @type str
415 @param modules list of module names contained in the package 445 @param modules list of module names contained in the package
416 (list of strings) 446 @type list of str
417 @param x x-coordinate (float) 447 @param x x-coordinate
418 @param y y-coordinate (float) 448 @type float
449 @param y y-coordinate
450 @type float
419 """ 451 """
420 from .PackageItem import PackageItem, PackageModel 452 from .PackageItem import PackageItem, PackageModel
421 pm = PackageModel(name, modules) 453 pm = PackageModel(name, modules)
422 pw = PackageItem(pm, x, y, scene=self.scene, 454 pw = PackageItem(pm, x, y, scene=self.scene,
423 colors=self.umlView.getDrawingColors()) 455 colors=self.umlView.getDrawingColors())
427 def __createAssociations(self, routes): 459 def __createAssociations(self, routes):
428 """ 460 """
429 Private method to generate the associations between the class shapes. 461 Private method to generate the associations between the class shapes.
430 462
431 @param routes list of relationsships 463 @param routes list of relationsships
464 @type list of tuple of (str, str)
432 """ 465 """
433 from .AssociationItem import AssociationItem, AssociationType 466 from .AssociationItem import AssociationItem, AssociationType
434 for route in routes: 467 for route in routes:
435 if len(route) > 1: 468 if len(route) > 1:
436 assoc = AssociationItem( 469 assoc = AssociationItem(
443 476
444 def getPersistenceData(self): 477 def getPersistenceData(self):
445 """ 478 """
446 Public method to get a string for data to be persisted. 479 Public method to get a string for data to be persisted.
447 480
448 @return persisted data string (string) 481 @return persisted data string
482 @rtype str
449 """ 483 """
450 return "package={0}, no_attributes={1}".format( 484 return "package={0}, no_attributes={1}".format(
451 self.package, self.noAttrs) 485 self.package, self.noAttrs)
452 486
453 def parsePersistenceData(self, version, data): 487 def parsePersistenceData(self, version, data):
454 """ 488 """
455 Public method to parse persisted data. 489 Public method to parse persisted data.
456 490
457 @param version version of the data (string) 491 @param version version of the data
458 @param data persisted data to be parsed (string) 492 @type str
459 @return flag indicating success (boolean) 493 @param data persisted data to be parsed
494 @type str
495 @return flag indicating success
496 @rtype bool
460 """ 497 """
461 parts = data.split(", ") 498 parts = data.split(", ")
462 if ( 499 if (
463 len(parts) != 2 or 500 len(parts) != 2 or
464 not parts[0].startswith("package=") or 501 not parts[0].startswith("package=") or
470 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip()) 507 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip())
471 508
472 self.initialize() 509 self.initialize()
473 510
474 return True 511 return True
512
513 def toDict(self):
514 """
515 Public method to collect data to be persisted.
516
517 @return dictionary containing data to be persisted
518 @rtype dict
519 """
520 data = {
521 "project_name": self.project.getProjectName(),
522 "no_attributes": self.noAttrs,
523 }
524
525 data["package"] = (
526 Utilities.fromNativeSeparators(self.__relPackage)
527 if self.__relPackage else
528 Utilities.fromNativeSeparators(self.package)
529 )
530
531 return data
532
533 def fromDict(self, version, data):
534 """
535 Public method to populate the class with data persisted by 'toDict()'.
536
537 @param version version of the data
538 @type str
539 @param data dictionary containing the persisted data
540 @type dict
541 @return tuple containing a flag indicating success and an info
542 message in case the diagram belongs to a different project
543 @rtype tuple of (bool, str)
544 """
545 try:
546 self.noAttrs = data["no_attributes"]
547
548 package = Utilities.toNativeSeparators(data["package"])
549 if os.path.isabs(package):
550 self.package = package
551 self.__relPackage = ""
552 else:
553 # relative package paths indicate a project package
554 if data["project_name"] != self.project.getProjectName():
555 msg = self.tr(
556 "<p>The diagram belongs to project <b>{0}</b>."
557 " Please open it and try again.</p>"
558 ).format(data["project_name"])
559 return False, msg
560
561 self.__relPackage = package
562 self.package = self.project.getAbsolutePath(package)
563 except KeyError:
564 return False, ""
565
566 self.initialize()
567
568 return True, ""

eric ide

mercurial