VultureChecker/VultureCheckerDialog.py

changeset 2
b517a1c5d5de
child 3
0e548b994980
equal deleted inserted replaced
1:ea6aed49cd69 2:b517a1c5d5de
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to show the vulture check results.
8 """
9
10 from __future__ import unicode_literals
11
12 try:
13 str = unicode # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
14 except NameError:
15 pass
16
17 import os
18 import fnmatch
19
20 from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer, QLocale
21 from PyQt5.QtWidgets import (
22 QDialog, QDialogButtonBox, QAbstractButton, QHeaderView, QTreeWidgetItem,
23 QApplication
24 )
25
26 from .Ui_VultureCheckerDialog import Ui_VultureCheckerDialog
27
28 from E5Gui.E5Application import e5App
29
30 import Preferences
31 import Utilities
32
33 from .vulture import Item
34
35
36 class VultureCheckerDialog(QDialog, Ui_VultureCheckerDialog):
37 """
38 Class implementing a dialog to show the vulture check results.
39 """
40 FilePathRole = Qt.UserRole + 1
41
42 def __init__(self, vultureService, parent=None):
43 """
44 Constructor
45
46 @param vultureService reference to the service
47 @type VulturePlugin
48 @param parent reference to the parent widget
49 @type QWidget
50 """
51 super(VultureCheckerDialog, self).__init__(parent)
52 self.setupUi(self)
53 self.setWindowFlags(Qt.Window)
54
55 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
56 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
57
58 self.resultList.headerItem().setText(self.resultList.columnCount(), "")
59
60 self.vultureService = vultureService
61 self.vultureService.analysisDone.connect(self.__processResult)
62 self.vultureService.error.connect(self.__processError)
63 self.vultureService.batchFinished.connect(self.__batchFinished)
64
65 self.cancelled = False
66
67 self.__project = e5App().getObject("Project")
68 self.__locale = QLocale()
69 self.__finished = True
70 self.__errorItem = None
71
72 self.__fileList = []
73 self.filterFrame.setVisible(False)
74
75 def __resizeResultColumns(self):
76 """
77 Private method to resize the list columns.
78 """
79 self.resultList.header().resizeSections(QHeaderView.ResizeToContents)
80 self.resultList.header().setStretchLastSection(True)
81
82 def __createErrorItem(self, filename, message):
83 """
84 Private slot to create a new error item in the result list.
85
86 @param filename name of the file
87 @type str
88 @param message error message
89 @type str
90 """
91 if self.__errorItem is None:
92 self.__errorItem = QTreeWidgetItem(self.resultList, [
93 self.tr("Errors")])
94 self.__errorItem.setExpanded(True)
95 self.__errorItem.setForeground(0, Qt.red)
96
97 msg = "{0} ({1})".format(self.__project.getRelativePath(filename),
98 message)
99 if not self.resultList.findItems(msg, Qt.MatchExactly):
100 itm = QTreeWidgetItem(self.__errorItem, [msg])
101 itm.setForeground(0, Qt.red)
102 itm.setFirstColumnSpanned(True)
103
104 def prepare(self, fileList, project):
105 """
106 Public method to prepare the dialog with a list of filenames.
107
108 @param fileList list of filenames
109 @type list of str
110 @param project reference to the project object
111 @type Project
112 """
113 self.__fileList = fileList[:]
114 self.__project = project
115
116 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
117 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
118 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
119
120 self.filterFrame.setVisible(True)
121
122 self.__data = self.__project.getData(
123 "CHECKERSPARMS", "Vulture")
124 if self.__data is None or "ExcludeFiles" not in self.__data:
125 self.__data = {"ExcludeFiles": ""}
126 self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
127
128 def start(self, fn):
129 """
130 Public slot to start the code metrics determination.
131
132 @param fn file or list of files or directory to show
133 the code metrics for
134 @type str or list of str
135 """
136 self.__errorItem = None
137 self.resultList.clear()
138 self.cancelled = False
139 QApplication.processEvents()
140
141 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
142 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
143 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
144 QApplication.processEvents()
145
146 self.__prepareResultLists()
147
148 if isinstance(fn, list):
149 self.files = fn
150 elif os.path.isdir(fn):
151 self.files = []
152 extensions = set(Preferences.getPython("PythonExtensions") +
153 Preferences.getPython("Python3Extensions"))
154 for ext in extensions:
155 self.files.extend(
156 Utilities.direntries(fn, True, '*{0}'.format(ext), 0))
157 else:
158 self.files = [fn]
159 self.files.sort()
160 # check for missing files
161 for f in self.files[:]:
162 if not os.path.exists(f):
163 self.files.remove(f)
164
165 if len(self.files) > 0:
166 # disable updates of the list for speed
167 self.resultList.setUpdatesEnabled(False)
168 self.resultList.setSortingEnabled(False)
169
170 self.checkProgress.setMaximum(len(self.files))
171 self.checkProgress.setVisible(len(self.files) > 1)
172 self.checkProgressLabel.setVisible(len(self.files) > 1)
173 QApplication.processEvents()
174
175 # now go through all the files
176 self.progress = 0
177 if len(self.files) == 1 or not self.vultureService.hasBatch:
178 self.__batch = False
179 self.vultureCheck()
180 else:
181 self.__batch = True
182 self.vultureCheckBatch()
183
184 def vultureCheck(self, codestring=''):
185 """
186 Public method to start a vulture check for one Python file.
187
188 The results are reported to the __processResult slot.
189
190 @keyparam codestring optional sourcestring
191 @type str
192 """
193 if not self.files:
194 self.checkProgressLabel.setPath("")
195 self.checkProgress.setMaximum(1)
196 self.checkProgress.setValue(1)
197 self.__finish()
198 return
199
200 self.filename = self.files.pop(0)
201 self.checkProgress.setValue(self.progress)
202 self.checkProgressLabel.setPath(self.filename)
203 QApplication.processEvents()
204
205 if self.cancelled:
206 return
207
208 try:
209 self.source = Utilities.readEncodedFile(self.filename)[0]
210 self.source = Utilities.normalizeCode(self.source)
211 except (UnicodeError, IOError) as msg:
212 self.__createErrorItem(self.filename, str(msg).rstrip())
213 self.progress += 1
214 # Continue with next file
215 self.vultureCheck()
216 return
217
218 self.__finished = False
219 self.vultureService.vultureCheck(
220 None, self.filename, self.source)
221
222 def vultureCheckBatch(self):
223 """
224 Public method to start a vulture check batch job.
225
226 The results are reported to the __processResult slot.
227 """
228 self.__lastFileItem = None
229
230 self.checkProgressLabel.setPath(self.tr("Preparing files..."))
231 progress = 0
232
233 argumentsList = []
234 for filename in self.files:
235 progress += 1
236 self.checkProgress.setValue(progress)
237 QApplication.processEvents()
238
239 try:
240 source = Utilities.readEncodedFile(filename)[0]
241 source = Utilities.normalizeCode(source)
242 except (UnicodeError, IOError) as msg:
243 self.__createErrorItem(filename, str(msg).rstrip())
244 continue
245
246 argumentsList.append((filename, source))
247
248 # reset the progress bar to the checked files
249 self.checkProgress.setValue(self.progress)
250 QApplication.processEvents()
251
252 self.__finished = False
253 self.vultureService.vultureCheckBatch(argumentsList)
254
255 def __batchFinished(self):
256 """
257 Private slot handling the completion of a batch job.
258 """
259 self.checkProgressLabel.setPath("")
260 self.checkProgress.setMaximum(1)
261 self.checkProgress.setValue(1)
262 self.__finish()
263
264 def __processError(self, fn, msg):
265 """
266 Private slot to process an error indication from the service.
267
268 @param fn filename of the file
269 @type str
270 @param msg error message
271 @type str
272 """
273 self.__createErrorItem(fn, msg)
274
275 def __processResult(self, fn, result):
276 """
277 Private slot called after perfoming a vulture analysis on one file.
278
279 @param fn filename of the file
280 @type str
281 @param result result dict
282 @type dict
283 """
284 if self.__finished:
285 return
286
287 # Check if it's the requested file, otherwise ignore signal if not
288 # in batch mode
289 if not self.__batch and fn != self.filename:
290 return
291
292 self.checkProgressLabel.setPath(self.__project.getRelativePath(fn))
293 QApplication.processEvents()
294
295 if "error" in result:
296 self.__createErrorItem(fn, result["error"])
297 else:
298 self.__storeResult(result)
299
300 self.progress += 1
301
302 self.checkProgress.setValue(self.progress)
303 QApplication.processEvents()
304
305 if not self.__batch:
306 self.vultureCheck()
307
308 def __finish(self):
309 """
310 Private slot called when the action or the user pressed the button.
311 """
312 if not self.__finished:
313 self.__finished = True
314
315 if not self.cancelled:
316 self.__createResultItems()
317
318 # reenable updates of the list
319 self.resultList.setSortingEnabled(True)
320 self.resultList.sortItems(0, Qt.AscendingOrder)
321 self.resultList.setUpdatesEnabled(True)
322
323 self.cancelled = True
324 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
325 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
326 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
327
328 self.resultList.header().resizeSections(
329 QHeaderView.ResizeToContents)
330 self.resultList.header().setStretchLastSection(True)
331 if qVersion() >= "5.0.0":
332 self.resultList.header().setSectionResizeMode(
333 QHeaderView.Interactive)
334 else:
335 self.resultList.header().setResizeMode(QHeaderView.Interactive)
336
337 self.checkProgress.setVisible(False)
338 self.checkProgressLabel.setVisible(False)
339
340 @pyqtSlot(QAbstractButton)
341 def on_buttonBox_clicked(self, button):
342 """
343 Private slot called by a button of the button box clicked.
344
345 @param button button that was clicked
346 @type QAbstractButton
347 """
348 if button == self.buttonBox.button(QDialogButtonBox.Close):
349 self.close()
350 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
351 self.cancelled = True
352 if self.__batch:
353 self.vultureService.cancelVultureCheckBatch()
354 QTimer.singleShot(1000, self.__finish)
355 else:
356 self.__finish()
357
358 @pyqtSlot()
359 def on_startButton_clicked(self):
360 """
361 Private slot to start a code metrics run.
362 """
363 fileList = self.__fileList[:]
364
365 filterString = self.excludeFilesEdit.text()
366 if "ExcludeFiles" not in self.__data or \
367 filterString != self.__data["ExcludeFiles"]:
368 self.__data["ExcludeFiles"] = filterString
369 self.__project.setData(
370 "CHECKERSPARMS", "Vulture", self.__data)
371 filterList = [f.strip() for f in filterString.split(",")
372 if f.strip()]
373 if filterList:
374 for filter in filterList:
375 fileList = \
376 [f for f in fileList if not fnmatch.fnmatch(f, filter)]
377
378 self.start(fileList)
379
380 def clear(self):
381 """
382 Public method to clear all results.
383 """
384 self.resultList.clear()
385
386 @pyqtSlot(QTreeWidgetItem, int)
387 def on_resultList_itemActivated(self, item, column):
388 """
389 Private slot to handle the activation of a result item.
390
391 @param item reference to the activated item
392 @type QTreeWidgetItem
393 @param column column the item was activated in
394 @type int
395 """
396 if item.parent() is not None:
397 filename = item.data(0, self.FilePathRole)
398 try:
399 lineno = int(item.text(0))
400 except ValueError:
401 lineno = 1
402 if filename:
403 vm = e5App().getObject("ViewManager")
404 vm.openSourceFile(filename, lineno)
405
406 def __prepareResultLists(self):
407 """
408 Private method to prepare the result lists.
409 """
410 self.__definedAttrs = []
411 self.__definedFuncs = []
412 self.__definedProps = []
413 self.__definedVars = []
414 self.__usedAttrs = []
415 self.__usedVars = []
416 self.__tupleAssignVars = []
417 self.__namesImportedAsAliases = []
418
419 def __storeResult(self, result):
420 """
421 Private method to store the result of an analysis.
422
423 @param result result dictionary
424 @type dict
425 """
426 self.__definedAttrs.extend(
427 [self.__dict2Item(d) for d in result["DefinedAttributes"]])
428 self.__definedFuncs.extend(
429 [self.__dict2Item(d) for d in result["DefinedFunctions"]])
430 self.__definedProps.extend(
431 [self.__dict2Item(d) for d in result["DefinedProperties"]])
432 self.__definedVars.extend(
433 [self.__dict2Item(d) for d in result["DefinedVariables"]])
434 self.__usedAttrs.extend(
435 [self.__dict2Item(d) for d in result["UsedAttributes"]])
436 self.__usedVars.extend(
437 [self.__dict2Item(d) for d in result["UsedVariables"]])
438 self.__tupleAssignVars.extend(
439 [self.__dict2Item(d) for d in result["TupleVariables"]])
440 self.__namesImportedAsAliases.extend(
441 [self.__dict2Item(d) for d in result["Aliases"]])
442
443 def __dict2Item(self, d):
444 """
445 Private method to convert an item dictionary to a vulture item.
446
447 @param d item dictionary
448 @type dict
449 @return vulture item
450 @rtype vulture.Item
451 """
452 return Item(d["name"], d["type"], d["file"], d["line"])
453
454 def __getUnusedItems(self, defined, used):
455 """
456 Private method to get a list of unused items.
457
458 @param defined list of defined items
459 @type list of vulture.Item
460 @param used list of used items
461 @type list of vulture.Item
462 @return list of unused items
463 @rtype list of vulture.Item
464 """
465 return list(set(defined) - set(used))
466
467 def __unusedFunctions(self):
468 """
469 Private method to get the list of unused functions.
470
471 @return list of unused functions
472 @rtype list of vulture.Item
473 """
474 return self.__getUnusedItems(
475 self.__definedFuncs,
476 self.__usedAttrs + self.__usedVars + self.__namesImportedAsAliases)
477
478 def __unusedProperties(self):
479 """
480 Private method to get the list of unused properties.
481
482 @return list of unused properties
483 @rtype list of vulture.Item
484 """
485 return self.__getUnusedItems(self.__definedProps, self.__usedAttrs)
486
487 def __unusedVariables(self):
488 """
489 Private method to get the list of unused variables.
490
491 @return list of unused variables
492 @rtype list of vulture.Item
493 """
494 return self.__getUnusedItems(
495 self.__definedVars,
496 self.__usedAttrs + self.__usedVars + self.__tupleAssignVars +
497 self.__namesImportedAsAliases)
498
499 def __unusedAttributes(self):
500 """
501 Private method to get the list of unused attributes.
502
503 @return list of unused attributes
504 @rtype list of vulture.Item
505 """
506 return self.__getUnusedItems(self.__definedAttrs, self.__usedAttrs)
507
508 def __createResultItems(self):
509 """
510 Private method to populate the list with the analysis result.
511 """
512 def filename(item):
513 return item.file
514
515 lastFileItem = None
516 lastFileName = ""
517 for item in sorted(self.__unusedFunctions() +
518 self.__unusedProperties() +
519 self.__unusedVariables() +
520 self.__unusedAttributes(),
521 key=filename):
522 if lastFileItem is None or lastFileName != item.file:
523 lastFileItem = QTreeWidgetItem(self.resultList, [
524 self.__project.getRelativePath(item.file)])
525 lastFileItem.setData(0, self.FilePathRole, item.file)
526 lastFileItem.setExpanded(True)
527 lastFileItem.setFirstColumnSpanned(True)
528 lastFileName = item.file
529
530 itm = QTreeWidgetItem(lastFileItem, [
531 "{0:6d}".format(item.lineno), str(item), item.typ])
532 itm.setData(0, self.FilePathRole, item.file)

eric ide

mercurial