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

eric ide

mercurial