src/eric7/Graphics/ImportsDiagramBuilder.py

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

eric ide

mercurial