|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog showing an imports diagram of the application. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 import os |
|
13 import glob |
|
14 |
|
15 from PyQt5.QtWidgets import QApplication |
|
16 |
|
17 from E5Gui import E5MessageBox |
|
18 from E5Gui.E5ProgressDialog import E5ProgressDialog |
|
19 |
|
20 from .UMLDiagramBuilder import UMLDiagramBuilder |
|
21 |
|
22 import Utilities |
|
23 import Preferences |
|
24 |
|
25 |
|
26 class ApplicationDiagramBuilder(UMLDiagramBuilder): |
|
27 """ |
|
28 Class implementing a builder for imports diagrams of the application. |
|
29 """ |
|
30 def __init__(self, dialog, view, project, noModules=False): |
|
31 """ |
|
32 Constructor |
|
33 |
|
34 @param dialog reference to the UML dialog (UMLDialog) |
|
35 @param view reference to the view object (UMLGraphicsView) |
|
36 @param project reference to the project object (Project) |
|
37 @keyparam noModules flag indicating, that no module names should be |
|
38 shown (boolean) |
|
39 """ |
|
40 super(ApplicationDiagramBuilder, self).__init__(dialog, view, project) |
|
41 self.setObjectName("ApplicationDiagram") |
|
42 |
|
43 self.noModules = noModules |
|
44 |
|
45 self.umlView.setDiagramName( |
|
46 self.tr("Application Diagram {0}").format( |
|
47 self.project.getProjectName())) |
|
48 |
|
49 def __buildModulesDict(self): |
|
50 """ |
|
51 Private method to build a dictionary of modules contained in the |
|
52 application. |
|
53 |
|
54 @return dictionary of modules contained in the application. |
|
55 """ |
|
56 import Utilities.ModuleParser |
|
57 extensions = Preferences.getPython("PythonExtensions") + \ |
|
58 Preferences.getPython("Python3Extensions") + ['.rb'] |
|
59 moduleDict = {} |
|
60 mods = self.project.pdata["SOURCES"] |
|
61 modules = [] |
|
62 for module in mods: |
|
63 modules.append(Utilities.normabsjoinpath( |
|
64 self.project.ppath, module)) |
|
65 tot = len(modules) |
|
66 progress = E5ProgressDialog( |
|
67 self.tr("Parsing modules..."), |
|
68 None, 0, tot, self.tr("%v/%m Modules"), self.parent()) |
|
69 progress.setWindowTitle(self.tr("Application Diagram")) |
|
70 try: |
|
71 prog = 0 |
|
72 progress.show() |
|
73 QApplication.processEvents() |
|
74 |
|
75 for module in modules: |
|
76 progress.setValue(prog) |
|
77 QApplication.processEvents() |
|
78 prog += 1 |
|
79 if module.endswith("__init__.py"): |
|
80 continue |
|
81 try: |
|
82 mod = Utilities.ModuleParser.readModule( |
|
83 module, extensions=extensions, caching=False) |
|
84 except ImportError: |
|
85 continue |
|
86 else: |
|
87 name = mod.name |
|
88 moduleDict[name] = mod |
|
89 finally: |
|
90 progress.setValue(tot) |
|
91 progress.deleteLater() |
|
92 return moduleDict |
|
93 |
|
94 def buildDiagram(self): |
|
95 """ |
|
96 Public method to build the packages shapes of the diagram. |
|
97 """ |
|
98 project = os.path.splitdrive(self.project.getProjectPath())[1]\ |
|
99 .replace(os.sep, '.')[1:] |
|
100 packages = {} |
|
101 shapes = {} |
|
102 p = 10 |
|
103 y = 10 |
|
104 maxHeight = 0 |
|
105 sceneRect = self.umlView.sceneRect() |
|
106 |
|
107 modules = self.__buildModulesDict() |
|
108 sortedkeys = sorted(modules.keys()) |
|
109 |
|
110 # step 1: build a dictionary of packages |
|
111 for module in sortedkeys: |
|
112 li = module.split('.') |
|
113 package = '.'.join(li[:-1]) |
|
114 if package in packages: |
|
115 packages[package][0].append(li[-1]) |
|
116 else: |
|
117 packages[package] = ([li[-1]], []) |
|
118 |
|
119 # step 2: assign modules to dictionaries and update import relationship |
|
120 for module in sortedkeys: |
|
121 li = module.split('.') |
|
122 package = '.'.join(li[:-1]) |
|
123 impLst = [] |
|
124 for i in modules[module].imports: |
|
125 if i in modules: |
|
126 impLst.append(i) |
|
127 else: |
|
128 if i.find('.') == -1: |
|
129 n = "{0}.{1}".format(modules[module].package, i) |
|
130 if n in modules: |
|
131 impLst.append(n) |
|
132 else: |
|
133 n = "{0}.{1}".format(project, i) |
|
134 if n in modules: |
|
135 impLst.append(n) |
|
136 elif n in packages: |
|
137 n = "{0}.<<Dummy>>".format(n) |
|
138 impLst.append(n) |
|
139 else: |
|
140 n = "{0}.{1}".format(project, i) |
|
141 if n in modules: |
|
142 impLst.append(n) |
|
143 for i in list(modules[module].from_imports.keys()): |
|
144 if i.startswith('.'): |
|
145 dots = len(i) - len(i.lstrip('.')) |
|
146 if dots == 1: |
|
147 i = i[1:] |
|
148 elif dots > 1: |
|
149 packagePath = os.path.dirname(modules[module].file) |
|
150 hasInit = True |
|
151 ppath = packagePath |
|
152 while hasInit: |
|
153 ppath = os.path.dirname(ppath) |
|
154 hasInit = len(glob.glob(os.path.join( |
|
155 ppath, '__init__.*'))) > 0 |
|
156 shortPackage = packagePath.replace(ppath, '')\ |
|
157 .replace(os.sep, '.')[1:] |
|
158 packageList = shortPackage.split('.')[1:] |
|
159 packageListLen = len(packageList) |
|
160 i = '.'.join( |
|
161 packageList[:packageListLen - dots + 1] + |
|
162 [i[dots:]]) |
|
163 |
|
164 if i in modules: |
|
165 impLst.append(i) |
|
166 else: |
|
167 if i.find('.') == -1: |
|
168 n = "{0}.{1}".format(modules[module].package, i) |
|
169 if n in modules: |
|
170 impLst.append(n) |
|
171 else: |
|
172 n = "{0}.{1}".format(project, i) |
|
173 if n in modules: |
|
174 impLst.append(n) |
|
175 elif n in packages: |
|
176 n = "{0}.<<Dummy>>".format(n) |
|
177 impLst.append(n) |
|
178 else: |
|
179 n = "{0}.{1}".format(project, i) |
|
180 if n in modules: |
|
181 impLst.append(n) |
|
182 for imp in impLst: |
|
183 impPackage = '.'.join(imp.split('.')[:-1]) |
|
184 if impPackage not in packages[package][1] and \ |
|
185 not impPackage == package: |
|
186 packages[package][1].append(impPackage) |
|
187 |
|
188 sortedkeys = sorted(packages.keys()) |
|
189 for package in sortedkeys: |
|
190 if package: |
|
191 relPackage = package.replace(project, '') |
|
192 if relPackage and relPackage[0] == '.': |
|
193 relPackage = relPackage[1:] |
|
194 else: |
|
195 relPackage = self.tr("<<Application>>") |
|
196 else: |
|
197 relPackage = self.tr("<<Others>>") |
|
198 shape = self.__addPackage( |
|
199 relPackage, packages[package][0], 0.0, 0.0) |
|
200 shapeRect = shape.sceneBoundingRect() |
|
201 shapes[package] = (shape, packages[package][1]) |
|
202 pn = p + shapeRect.width() + 10 |
|
203 maxHeight = max(maxHeight, shapeRect.height()) |
|
204 if pn > sceneRect.width(): |
|
205 p = 10 |
|
206 y += maxHeight + 10 |
|
207 maxHeight = shapeRect.height() |
|
208 shape.setPos(p, y) |
|
209 p += shapeRect.width() + 10 |
|
210 else: |
|
211 shape.setPos(p, y) |
|
212 p = pn |
|
213 |
|
214 rect = self.umlView._getDiagramRect(10) |
|
215 sceneRect = self.umlView.sceneRect() |
|
216 if rect.width() > sceneRect.width(): |
|
217 sceneRect.setWidth(rect.width()) |
|
218 if rect.height() > sceneRect.height(): |
|
219 sceneRect.setHeight(rect.height()) |
|
220 self.umlView.setSceneSize(sceneRect.width(), sceneRect.height()) |
|
221 |
|
222 self.__createAssociations(shapes) |
|
223 self.umlView.autoAdjustSceneSize(limit=True) |
|
224 |
|
225 def __addPackage(self, name, modules, x, y): |
|
226 """ |
|
227 Private method to add a package to the diagram. |
|
228 |
|
229 @param name package name to be shown (string) |
|
230 @param modules list of module names contained in the package |
|
231 (list of strings) |
|
232 @param x x-coordinate (float) |
|
233 @param y y-coordinate (float) |
|
234 @return reference to the package item (PackageItem) |
|
235 """ |
|
236 from .PackageItem import PackageItem, PackageModel |
|
237 modules.sort() |
|
238 pm = PackageModel(name, modules) |
|
239 pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene) |
|
240 pw.setId(self.umlView.getItemId()) |
|
241 return pw |
|
242 |
|
243 def __createAssociations(self, shapes): |
|
244 """ |
|
245 Private method to generate the associations between the package shapes. |
|
246 |
|
247 @param shapes list of shapes |
|
248 """ |
|
249 from .AssociationItem import AssociationItem, Imports |
|
250 for package in shapes: |
|
251 for rel in shapes[package][1]: |
|
252 assoc = AssociationItem( |
|
253 shapes[package][0], shapes[rel][0], |
|
254 Imports) |
|
255 self.scene.addItem(assoc) |
|
256 |
|
257 def getPersistenceData(self): |
|
258 """ |
|
259 Public method to get a string for data to be persisted. |
|
260 |
|
261 @return persisted data string (string) |
|
262 """ |
|
263 return "project={0}, no_modules={1}".format( |
|
264 self.project.getProjectFile(), self.noModules) |
|
265 |
|
266 def parsePersistenceData(self, version, data): |
|
267 """ |
|
268 Public method to parse persisted data. |
|
269 |
|
270 @param version version of the data (string) |
|
271 @param data persisted data to be parsed (string) |
|
272 @return flag indicating success (boolean) |
|
273 """ |
|
274 parts = data.split(", ") |
|
275 if len(parts) != 2 or \ |
|
276 not parts[0].startswith("project=") or \ |
|
277 not parts[1].startswith("no_modules="): |
|
278 return False |
|
279 |
|
280 projectFile = parts[0].split("=", 1)[1].strip() |
|
281 if projectFile != self.project.getProjectFile(): |
|
282 res = E5MessageBox.yesNo( |
|
283 None, |
|
284 self.tr("Load Diagram"), |
|
285 self.tr( |
|
286 """<p>The diagram belongs to the project <b>{0}</b>.""" |
|
287 """ Shall this project be opened?</p>""").format( |
|
288 projectFile)) |
|
289 if res: |
|
290 self.project.openProject(projectFile) |
|
291 |
|
292 self.noModules = Utilities.toBool(parts[1].split("=", 1)[1].strip()) |
|
293 |
|
294 self.initialize() |
|
295 |
|
296 return True |