src/eric7/Graphics/ApplicationDiagramBuilder.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8943
23f9c7b9e18e
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing an imports diagram of the application.
8 """
9
10 import glob
11 import os
12 import time
13
14 from PyQt6.QtWidgets import QApplication, QInputDialog
15
16 from EricWidgets import EricMessageBox
17 from EricWidgets.EricProgressDialog import EricProgressDialog
18
19 from .UMLDiagramBuilder import UMLDiagramBuilder
20
21 import Utilities
22 import Preferences
23
24
25 class ApplicationDiagramBuilder(UMLDiagramBuilder):
26 """
27 Class implementing a builder for imports diagrams of the application.
28 """
29 def __init__(self, dialog, view, project, noModules=False):
30 """
31 Constructor
32
33 @param dialog reference to the UML dialog
34 @type UMLDialog
35 @param view reference to the view object
36 @type UMLGraphicsView
37 @param project reference to the project object
38 @type Project
39 @param noModules flag indicating, that no module names should be
40 shown
41 @type bool
42 """
43 super().__init__(dialog, view, project)
44 self.setObjectName("ApplicationDiagram")
45
46 self.noModules = noModules
47
48 self.umlView.setDiagramName(
49 self.tr("Application Diagram {0}").format(
50 self.project.getProjectName()))
51
52 def __buildModulesDict(self):
53 """
54 Private method to build a dictionary of modules contained in the
55 application.
56
57 @return dictionary of modules contained in the application
58 @rtype dict
59 """
60 import Utilities.ModuleParser
61 extensions = (
62 Preferences.getPython("Python3Extensions") +
63 ['.rb']
64 )
65 moduleDict = {}
66 mods = self.project.pdata["SOURCES"]
67 modules = []
68 for module in mods:
69 modules.append(Utilities.normabsjoinpath(
70 self.project.ppath, module))
71 tot = len(modules)
72 progress = EricProgressDialog(
73 self.tr("Parsing modules..."),
74 None, 0, tot, self.tr("%v/%m Modules"), self.parent())
75 progress.setWindowTitle(self.tr("Application Diagram"))
76 try:
77 progress.show()
78 QApplication.processEvents()
79
80 now = time.monotonic()
81 for prog, module in enumerate(modules):
82 progress.setValue(prog)
83 if time.monotonic() - now > 0.01:
84 QApplication.processEvents()
85 now = time.monotonic()
86 if module.endswith("__init__.py"):
87 continue
88 try:
89 mod = Utilities.ModuleParser.readModule(
90 module, extensions=extensions, caching=False)
91 except ImportError:
92 continue
93 else:
94 name = mod.name
95 moduleDict[name] = mod
96 finally:
97 progress.setValue(tot)
98 progress.deleteLater()
99 return moduleDict
100
101 def __findApplicationRoot(self):
102 """
103 Private method to find the application root path.
104
105 @return application root path
106 @rtype str
107 """
108 candidates = []
109 path = self.project.getProjectPath()
110 init = os.path.join(path, "__init__.py")
111 if os.path.exists(init):
112 # project is a package
113 return path
114 else:
115 # check, if any of the top directories is a package
116 for entry in [e for e in os.listdir(path) if not e.startswith(".")]:
117 fullpath = os.path.join(path, entry)
118 if os.path.isdir(fullpath):
119 init = os.path.join(fullpath, "__init__.py")
120 if os.path.exists(init):
121 candidates.append(fullpath)
122
123 # check, if project uses the 'src' layout
124 if os.path.exists(os.path.join(path, "src")):
125 srcPath = os.path.join(path, "src")
126 for entry in [e for e in os.listdir(srcPath) if not e.startswith(".")]:
127 fullpath = os.path.join(srcPath, entry)
128 if os.path.isdir(fullpath):
129 init = os.path.join(fullpath, "__init__.py")
130 if os.path.exists(init):
131 candidates.append(fullpath)
132
133 if len(candidates) == 1:
134 return candidates[0]
135 elif len(candidates) > 1:
136 root, ok = QInputDialog.getItem(
137 None,
138 self.tr("Application Diagram"),
139 self.tr("Select the application directory:"),
140 sorted(candidates),
141 0, True)
142 if ok:
143 return root
144 else:
145 EricMessageBox.warning(
146 None,
147 self.tr("Application Diagram"),
148 self.tr("""No application package could be detected."""
149 """ Aborting..."""))
150 return None
151
152 def buildDiagram(self):
153 """
154 Public method to build the packages shapes of the diagram.
155 """
156 rpath = self.__findApplicationRoot()
157 if rpath is None:
158 # no root path detected
159 return
160
161 root = os.path.splitdrive(rpath)[1].replace(os.sep, '.')[1:]
162
163 packages = {}
164 self.__shapes = {}
165
166 modules = self.__buildModulesDict()
167
168 # step 1: build a dictionary of packages
169 for module in sorted(modules.keys()):
170 if "." in module:
171 packageName, moduleName = module.rsplit(".", 1)
172 else:
173 packageName, moduleName = "", module
174 if packageName in packages:
175 packages[packageName][0].append(moduleName)
176 else:
177 packages[packageName] = ([moduleName], [])
178
179 # step 2: assign modules to dictionaries and update import relationship
180 for module in sorted(modules.keys()):
181 package = module.rsplit(".", 1)[0]
182 impLst = []
183 for moduleImport in modules[module].imports:
184 if moduleImport in modules:
185 impLst.append(moduleImport)
186 else:
187 if moduleImport.find('.') == -1:
188 n = "{0}.{1}".format(modules[module].package,
189 moduleImport)
190 if n in modules:
191 impLst.append(n)
192 else:
193 n = "{0}.{1}".format(root, moduleImport)
194 if n in modules:
195 impLst.append(n)
196 elif n in packages:
197 n = "{0}.<<Dummy>>".format(n)
198 impLst.append(n)
199 else:
200 n = "{0}.{1}".format(root, moduleImport)
201 if n in modules:
202 impLst.append(n)
203 for moduleImport in list(modules[module].from_imports.keys()):
204 if moduleImport.startswith('.'):
205 dots = len(moduleImport) - len(moduleImport.lstrip('.'))
206 if dots == 1:
207 moduleImport = moduleImport[1:]
208 elif dots > 1:
209 packagePath = os.path.dirname(modules[module].file)
210 hasInit = True
211 ppath = packagePath
212 while hasInit:
213 ppath = os.path.dirname(ppath)
214 hasInit = len(glob.glob(os.path.join(
215 ppath, '__init__.*'))) > 0
216 shortPackage = (
217 packagePath.replace(ppath, '')
218 .replace(os.sep, '.')[1:]
219 )
220 packageList = shortPackage.split('.')[1:]
221 packageListLen = len(packageList)
222 moduleImport = '.'.join(
223 packageList[:packageListLen - dots + 1] +
224 [moduleImport[dots:]])
225
226 if moduleImport in modules:
227 impLst.append(moduleImport)
228 else:
229 if moduleImport.find('.') == -1:
230 n = "{0}.{1}".format(modules[module].package,
231 moduleImport)
232 if n in modules:
233 impLst.append(n)
234 else:
235 n = "{0}.{1}".format(root, moduleImport)
236 if n in modules:
237 impLst.append(n)
238 elif n in packages:
239 n = "{0}.<<Dummy>>".format(n)
240 impLst.append(n)
241 else:
242 n = "{0}.{1}".format(root, moduleImport)
243 if n in modules:
244 impLst.append(n)
245 for moduleImport in impLst:
246 impPackage = moduleImport.rsplit(".", 1)[0]
247 try:
248 if (
249 impPackage not in packages[package][1] and
250 impPackage != package
251 ):
252 packages[package][1].append(impPackage)
253 except KeyError:
254 continue
255
256 for package in sorted(packages.keys()):
257 if package:
258 relPackage = package.replace(root, '')
259 if relPackage and relPackage[0] == '.':
260 relPackage = relPackage[1:]
261 else:
262 relPackage = self.tr("<<Application>>")
263 else:
264 relPackage = self.tr("<<Others>>")
265 shape = self.__addPackage(
266 relPackage, packages[package][0], 0.0, 0.0)
267 self.__shapes[package] = (shape, packages[package][1])
268
269 # build a list of routes
270 nodes = []
271 routes = []
272 for module in self.__shapes:
273 nodes.append(module)
274 for rel in self.__shapes[module][1]:
275 route = (module, rel)
276 if route not in routes:
277 routes.append(route)
278
279 self.__arrangeNodes(nodes, routes[:])
280 self.__createAssociations(routes)
281 self.umlView.autoAdjustSceneSize(limit=True)
282
283 def __addPackage(self, name, modules, x, y):
284 """
285 Private method to add a package to the diagram.
286
287 @param name package name to be shown
288 @type str
289 @param modules list of module names contained in the package
290 @type list of str
291 @param x x-coordinate
292 @type float
293 @param y y-coordinate
294 @type float
295 @return reference to the package item
296 @rtype PackageItem
297 """
298 from .PackageItem import PackageItem, PackageModel
299 modules.sort()
300 pm = PackageModel(name, modules)
301 pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene,
302 colors=self.umlView.getDrawingColors())
303 pw.setId(self.umlView.getItemId())
304 return pw
305
306 def __arrangeNodes(self, nodes, routes, whiteSpaceFactor=1.2):
307 """
308 Private method to arrange the shapes on the canvas.
309
310 The algorithm is borrowed from Boa Constructor.
311
312 @param nodes list of nodes to arrange
313 @type list of str
314 @param routes list of routes
315 @type list of tuple of (str, str)
316 @param whiteSpaceFactor factor to increase whitespace between
317 items
318 @type float
319 """
320 from . import GraphicsUtilities
321 generations = GraphicsUtilities.sort(nodes, routes)
322
323 # calculate width and height of all elements
324 sizes = []
325 for generation in generations:
326 sizes.append([])
327 for child in generation:
328 sizes[-1].append(
329 self.__shapes[child][0].sceneBoundingRect())
330
331 # calculate total width and total height
332 width = 0
333 height = 0
334 widths = []
335 heights = []
336 for generation in sizes:
337 currentWidth = 0
338 currentHeight = 0
339
340 for rect in generation:
341 if rect.height() > currentHeight:
342 currentHeight = rect.height()
343 currentWidth += rect.width()
344
345 # update totals
346 if currentWidth > width:
347 width = currentWidth
348 height += currentHeight
349
350 # store generation info
351 widths.append(currentWidth)
352 heights.append(currentHeight)
353
354 # add in some whitespace
355 width *= whiteSpaceFactor
356 height = height * whiteSpaceFactor - 20
357 verticalWhiteSpace = 40.0
358
359 sceneRect = self.umlView.sceneRect()
360 width += 50.0
361 height += 50.0
362 swidth = sceneRect.width() if width < sceneRect.width() else width
363 sheight = sceneRect.height() if height < sceneRect.height() else height
364 self.umlView.setSceneSize(swidth, sheight)
365
366 # distribute each generation across the width and the
367 # generations across height
368 y = 10.0
369 for currentWidth, currentHeight, generation in (
370 zip(reversed(widths), reversed(heights), reversed(generations))
371 ):
372 x = 10.0
373 # whiteSpace is the space between any two elements
374 whiteSpace = (
375 (width - currentWidth - 20) /
376 (len(generation) - 1.0 or 2.0)
377 )
378 for name in generation:
379 shape = self.__shapes[name][0]
380 shape.setPos(x, y)
381 rect = shape.sceneBoundingRect()
382 x = x + rect.width() + whiteSpace
383 y = y + currentHeight + verticalWhiteSpace
384
385 def __createAssociations(self, routes):
386 """
387 Private method to generate the associations between the module shapes.
388
389 @param routes list of associations
390 @type list of tuple of (str, str)
391 """
392 from .AssociationItem import AssociationItem, AssociationType
393 for route in routes:
394 assoc = AssociationItem(
395 self.__shapes[route[0]][0],
396 self.__shapes[route[1]][0],
397 AssociationType.IMPORTS,
398 colors=self.umlView.getDrawingColors())
399 self.scene.addItem(assoc)
400
401 def parsePersistenceData(self, version, data):
402 """
403 Public method to parse persisted data.
404
405 @param version version of the data
406 @type str
407 @param data persisted data to be parsed
408 @type str
409 @return flag indicating success
410 @rtype bool
411 """
412 parts = data.split(", ")
413 if (
414 len(parts) != 2 or
415 not parts[0].startswith("project=") or
416 not parts[1].startswith("no_modules=")
417 ):
418 return False
419
420 projectFile = parts[0].split("=", 1)[1].strip()
421 if projectFile != self.project.getProjectFile():
422 res = EricMessageBox.yesNo(
423 None,
424 self.tr("Load Diagram"),
425 self.tr(
426 """<p>The diagram belongs to the project <b>{0}</b>."""
427 """ Shall this project be opened?</p>""").format(
428 projectFile))
429 if res:
430 self.project.openProject(projectFile)
431
432 self.noModules = Utilities.toBool(parts[1].split("=", 1)[1].strip())
433
434 self.initialize()
435
436 return True
437
438 def toDict(self):
439 """
440 Public method to collect data to be persisted.
441
442 @return dictionary containing data to be persisted
443 @rtype dict
444 """
445 return {
446 "project_name": self.project.getProjectName(),
447 "no_modules": self.noModules,
448 }
449
450 def fromDict(self, version, data):
451 """
452 Public method to populate the class with data persisted by 'toDict()'.
453
454 @param version version of the data
455 @type str
456 @param data dictionary containing the persisted data
457 @type dict
458 @return tuple containing a flag indicating success and an info
459 message in case the diagram belongs to a different project
460 @rtype tuple of (bool, str)
461 """
462 try:
463 self.noModules = data["no_modules"]
464
465 if data["project_name"] != self.project.getProjectName():
466 msg = self.tr(
467 "<p>The diagram belongs to project <b>{0}</b>."
468 " Please open it and try again.</p>"
469 ).format(data["project_name"])
470 return False, msg
471 except KeyError:
472 return False, ""
473
474 self.initialize()
475
476 return True, ""

eric ide

mercurial