eric7/Graphics/ImportsDiagramBuilder.py

branch
eric7
changeset 8312
800c432b34c8
parent 8295
3f5e8b0a338e
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing an imports diagram of a package.
8 """
9
10 import glob
11 import os
12
13 from PyQt5.QtWidgets import QApplication, QGraphicsTextItem
14
15 from E5Gui.E5ProgressDialog import E5ProgressDialog
16
17 from .UMLDiagramBuilder import UMLDiagramBuilder
18
19 import Utilities
20 import Preferences
21
22
23 class ImportsDiagramBuilder(UMLDiagramBuilder):
24 """
25 Class implementing a builder for imports diagrams of a package.
26
27 Note: Only package internal imports are shown in order to maintain
28 some readability.
29 """
30 def __init__(self, dialog, view, project, package,
31 showExternalImports=False):
32 """
33 Constructor
34
35 @param dialog reference to the UML dialog
36 @type UMLDialog
37 @param view reference to the view object
38 @type UMLGraphicsView
39 @param project reference to the project object
40 @type Project
41 @param package name of a python package to show the import
42 relationships
43 @type str
44 @param showExternalImports flag indicating to show exports from
45 outside the package
46 @type bool
47 """
48 super().__init__(dialog, view, project)
49 self.setObjectName("ImportsDiagram")
50
51 self.showExternalImports = showExternalImports
52 self.packagePath = os.path.abspath(package)
53
54 self.__relPackagePath = (
55 self.project.getRelativePath(self.packagePath)
56 if self.project.isProjectSource(self.packagePath) else
57 ""
58 )
59
60 def initialize(self):
61 """
62 Public method to initialize the object.
63 """
64 self.package = os.path.splitdrive(self.packagePath)[1].replace(
65 os.sep, '.')[1:]
66 hasInit = True
67 ppath = self.packagePath
68 while hasInit:
69 ppath = os.path.dirname(ppath)
70 hasInit = len(glob.glob(os.path.join(ppath, '__init__.*'))) > 0
71 self.shortPackage = self.packagePath.replace(ppath, '').replace(
72 os.sep, '.')[1:]
73
74 pname = self.project.getProjectName()
75 name = (
76 self.tr("Imports Diagramm {0}: {1}").format(
77 pname, self.project.getRelativePath(self.packagePath))
78 if pname else
79 self.tr("Imports Diagramm: {0}").format(self.packagePath)
80 )
81 self.umlView.setDiagramName(name)
82
83 def __buildModulesDict(self):
84 """
85 Private method to build a dictionary of modules contained in the
86 package.
87
88 @return dictionary of modules contained in the package
89 @rtype dict
90 """
91 import Utilities.ModuleParser
92 extensions = (
93 Preferences.getPython("Python3Extensions")
94 )
95 moduleDict = {}
96 modules = []
97 for ext in (
98 Preferences.getPython("Python3Extensions")
99 ):
100 modules.extend(glob.glob(Utilities.normjoinpath(
101 self.packagePath, '*{0}'.format(ext))))
102
103 tot = len(modules)
104 progress = E5ProgressDialog(
105 self.tr("Parsing modules..."),
106 None, 0, tot, self.tr("%v/%m Modules"), self.parent())
107 progress.setWindowTitle(self.tr("Imports Diagramm"))
108 try:
109 progress.show()
110 QApplication.processEvents()
111 for prog, module in enumerate(modules):
112 progress.setValue(prog)
113 QApplication.processEvents()
114 try:
115 mod = Utilities.ModuleParser.readModule(
116 module, extensions=extensions, caching=False)
117 except ImportError:
118 continue
119 else:
120 name = mod.name
121 if name.startswith(self.package):
122 name = name[len(self.package) + 1:]
123 moduleDict[name] = mod
124 finally:
125 progress.setValue(tot)
126 progress.deleteLater()
127 return moduleDict
128
129 def buildDiagram(self):
130 """
131 Public method to build the modules shapes of the diagram.
132 """
133 initlist = glob.glob(os.path.join(self.packagePath, '__init__.*'))
134 if len(initlist) == 0:
135 ct = QGraphicsTextItem(None)
136 ct.setHtml(self.buildErrorMessage(
137 self.tr("The directory <b>'{0}'</b> is not a Python"
138 " package.").format(self.package)
139 ))
140 self.scene.addItem(ct)
141 return
142
143 self.__shapes = {}
144
145 modules = self.__buildModulesDict()
146 externalMods = []
147 packageList = self.shortPackage.split('.')
148 packageListLen = len(packageList)
149 for module in sorted(modules.keys()):
150 impLst = []
151 for importName in modules[module].imports:
152 n = (
153 importName[len(self.package) + 1:]
154 if importName.startswith(self.package) else
155 importName
156 )
157 if importName in modules:
158 impLst.append(n)
159 elif self.showExternalImports:
160 impLst.append(n)
161 if n not in externalMods:
162 externalMods.append(n)
163 for importName in list(modules[module].from_imports.keys()):
164 if importName.startswith('.'):
165 dots = len(importName) - len(importName.lstrip('.'))
166 if dots == 1:
167 n = importName[1:]
168 importName = n
169 else:
170 if self.showExternalImports:
171 n = '.'.join(
172 packageList[:packageListLen - dots + 1] +
173 [importName[dots:]])
174 else:
175 n = importName
176 elif importName.startswith(self.package):
177 n = importName[len(self.package) + 1:]
178 else:
179 n = importName
180 if importName in modules:
181 impLst.append(n)
182 elif self.showExternalImports:
183 impLst.append(n)
184 if n not in externalMods:
185 externalMods.append(n)
186
187 classNames = []
188 for class_ in list(modules[module].classes.keys()):
189 className = modules[module].classes[class_].name
190 if className not in classNames:
191 classNames.append(className)
192 shape = self.__addModule(module, classNames, 0.0, 0.0)
193 self.__shapes[module] = (shape, impLst)
194
195 for module in externalMods:
196 shape = self.__addModule(module, [], 0.0, 0.0)
197 self.__shapes[module] = (shape, [])
198
199 # build a list of routes
200 nodes = []
201 routes = []
202 for module in self.__shapes:
203 nodes.append(module)
204 for rel in self.__shapes[module][1]:
205 route = (module, rel)
206 if route not in routes:
207 routes.append(route)
208
209 self.__arrangeNodes(nodes, routes[:])
210 self.__createAssociations(routes)
211 self.umlView.autoAdjustSceneSize(limit=True)
212
213 def __addModule(self, name, classes, x, y):
214 """
215 Private method to add a module to the diagram.
216
217 @param name module name to be shown
218 @type str
219 @param classes list of class names contained in the module
220 @type list of str
221 @param x x-coordinate
222 @type float
223 @param y y-coordinate
224 @type float
225 @return reference to the imports item
226 @rtype ModuleItem
227 """
228 from .ModuleItem import ModuleItem, ModuleModel
229 classes.sort()
230 impM = ModuleModel(name, classes)
231 impW = ModuleItem(impM, x, y, scene=self.scene,
232 colors=self.umlView.getDrawingColors())
233 impW.setId(self.umlView.getItemId())
234 return impW
235
236 def __arrangeNodes(self, nodes, routes, whiteSpaceFactor=1.2):
237 """
238 Private method to arrange the shapes on the canvas.
239
240 The algorithm is borrowed from Boa Constructor.
241
242 @param nodes list of nodes to arrange
243 @type list of str
244 @param routes list of routes
245 @type list of tuple of (str, str)
246 @param whiteSpaceFactor factor to increase whitespace between
247 items
248 @type float
249 """
250 from . import GraphicsUtilities
251 generations = GraphicsUtilities.sort(nodes, routes)
252
253 # calculate width and height of all elements
254 sizes = []
255 for generation in generations:
256 sizes.append([])
257 for child in generation:
258 sizes[-1].append(
259 self.__shapes[child][0].sceneBoundingRect())
260
261 # calculate total width and total height
262 width = 0
263 height = 0
264 widths = []
265 heights = []
266 for generation in sizes:
267 currentWidth = 0
268 currentHeight = 0
269
270 for rect in generation:
271 if rect.height() > currentHeight:
272 currentHeight = rect.height()
273 currentWidth += rect.width()
274
275 # update totals
276 if currentWidth > width:
277 width = currentWidth
278 height += currentHeight
279
280 # store generation info
281 widths.append(currentWidth)
282 heights.append(currentHeight)
283
284 # add in some whitespace
285 width *= whiteSpaceFactor
286 height = height * whiteSpaceFactor - 20
287 verticalWhiteSpace = 40.0
288
289 sceneRect = self.umlView.sceneRect()
290 width += 50.0
291 height += 50.0
292 swidth = sceneRect.width() if width < sceneRect.width() else width
293 sheight = sceneRect.height() if height < sceneRect.height() else height
294 self.umlView.setSceneSize(swidth, sheight)
295
296 # distribute each generation across the width and the
297 # generations across height
298 y = 10.0
299 for currentWidth, currentHeight, generation in (
300 zip(reversed(widths), reversed(heights), reversed(generations))
301 ):
302 x = 10.0
303 # whiteSpace is the space between any two elements
304 whiteSpace = (
305 (width - currentWidth - 20) /
306 (len(generation) - 1.0 or 2.0)
307 )
308 for name in generation:
309 shape = self.__shapes[name][0]
310 shape.setPos(x, y)
311 rect = shape.sceneBoundingRect()
312 x = x + rect.width() + whiteSpace
313 y = y + currentHeight + verticalWhiteSpace
314
315 def __createAssociations(self, routes):
316 """
317 Private method to generate the associations between the module shapes.
318
319 @param routes list of associations
320 @type list of tuple of (str, str)
321 """
322 from .AssociationItem import AssociationItem, AssociationType
323 for route in routes:
324 assoc = AssociationItem(
325 self.__shapes[route[0]][0],
326 self.__shapes[route[1]][0],
327 AssociationType.IMPORTS,
328 colors=self.umlView.getDrawingColors())
329 self.scene.addItem(assoc)
330
331 def getPersistenceData(self):
332 """
333 Public method to get a string for data to be persisted.
334
335 @return persisted data string
336 @rtype str
337 """
338 return "package={0}, show_external={1}".format(
339 self.packagePath, self.showExternalImports)
340
341 def parsePersistenceData(self, version, data):
342 """
343 Public method to parse persisted data.
344
345 @param version version of the data
346 @type str
347 @param data persisted data to be parsed
348 @type str
349 @return flag indicating success
350 @rtype bool
351 """
352 parts = data.split(", ")
353 if (
354 len(parts) != 2 or
355 not parts[0].startswith("package=") or
356 not parts[1].startswith("show_external=")
357 ):
358 return False
359
360 self.packagePath = parts[0].split("=", 1)[1].strip()
361 self.showExternalImports = Utilities.toBool(
362 parts[1].split("=", 1)[1].strip())
363
364 self.initialize()
365
366 return True
367
368 def toDict(self):
369 """
370 Public method to collect data to be persisted.
371
372 @return dictionary containing data to be persisted
373 @rtype dict
374 """
375 data = {
376 "project_name": self.project.getProjectName(),
377 "show_external": self.showExternalImports,
378 }
379
380 data["package"] = (
381 Utilities.fromNativeSeparators(self.__relPackagePath)
382 if self.__relPackagePath else
383 Utilities.fromNativeSeparators(self.packagePath)
384 )
385
386 return data
387
388 def fromDict(self, version, data):
389 """
390 Public method to populate the class with data persisted by 'toDict()'.
391
392 @param version version of the data
393 @type str
394 @param data dictionary containing the persisted data
395 @type dict
396 @return tuple containing a flag indicating success and an info
397 message in case the diagram belongs to a different project
398 @rtype tuple of (bool, str)
399 """
400 try:
401 self.showExternalImports = data["show_external"]
402
403 packagePath = Utilities.toNativeSeparators(data["package"])
404 if os.path.isabs(packagePath):
405 self.packagePath = packagePath
406 self.__relPackagePath = ""
407 else:
408 # relative package paths indicate a project package
409 if data["project_name"] != self.project.getProjectName():
410 msg = self.tr(
411 "<p>The diagram belongs to project <b>{0}</b>."
412 " Please open it and try again.</p>"
413 ).format(data["project_name"])
414 return False, msg
415
416 self.__relPackagePath = packagePath
417 self.package = self.project.getAbsolutePath(packagePath)
418 except KeyError:
419 return False, ""
420
421 self.initialize()
422
423 return True, ""

eric ide

mercurial