RadonMetrics/CyclomaticComplexityDialog.py

changeset 13
22bc345844e7
child 15
62ffe3d426e5
equal deleted inserted replaced
12:32a3c9d62e90 13:22bc345844e7
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 cyclomatic complexity (McCabe
8 complexity).
9 """
10
11 from __future__ import unicode_literals
12
13 try:
14 str = unicode # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
15 except NameError:
16 pass
17
18 import os
19 import fnmatch
20 import sys
21
22 sys.path.insert(0, os.path.dirname(__file__))
23
24 from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer, QLocale
25 from PyQt5.QtWidgets import (
26 QDialog, QDialogButtonBox, QAbstractButton, QHeaderView, QTreeWidgetItem,
27 QApplication
28 )
29
30 from .Ui_CyclomaticComplexityDialog import Ui_CyclomaticComplexityDialog
31 from E5Gui.E5Application import e5App
32
33 import Preferences
34 import Utilities
35
36
37 class CyclomaticComplexityDialog(QDialog, Ui_CyclomaticComplexityDialog):
38 """
39 Class implementing a dialog to show the cyclomatic complexity (McCabe
40 complexity).
41 """
42 def __init__(self, radonService, parent=None):
43 """
44 Constructor
45
46 @param radonService reference to the service
47 @type RadonMetricsPlugin
48 @param parent reference to the parent widget
49 @type QWidget
50 """
51 super(CyclomaticComplexityDialog, 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.radonService = radonService
61 self.radonService.complexityDone.connect(self.__processResult)
62 self.radonService.error.connect(self.__processError)
63 self.radonService.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 self.explanationLabel.setText(self.tr(
76 "<table>"
77 "<tr><td colspan=3><b>Ranking:</b></td></tr>"
78 "<tr><td><b>A</b></td><td>1 - 5</td>"
79 "<td>(low risk - simple block)</td></tr>"
80 "<tr><td><b>B</b></td><td>6 - 10</td>"
81 "<td>(low risk - well structured and stable block)</td></tr>"
82 "<tr><td><b>C</b></td><td>11 - 20</td>"
83 "<td>(moderate risk - slightly complex block)</td></tr>"
84 "<tr><td><b>D</b></td><td>21 - 30</td>"
85 "<td>(more than moderate risk - more complex block)</td></tr>"
86 "<tr><td><b>E</b></td><td>31 - 40</td>"
87 "<td>(high risk - complex block, alarming)</td></tr>"
88 "<tr><td><b>F</b></td><td>&gt; 40</td>"
89 "<td>(very high risk - error-prone, unstable block)</td></tr>"
90 "</table>"
91 ))
92 self.typeLabel.setText(self.tr(
93 "<table>"
94 "<tr><td colspan=2><b>Type:</b></td></tr>"
95 "<tr><td><b>C</b></td><td>Class</td></tr>"
96 "<tr><td><b>F</b></td><td>Function</td></tr>"
97 "<tr><td><b>M</b></td><td>Method</td></tr>"
98 "</table>"
99 ))
100
101 self.__rankColors = {
102 "A": Qt.green,
103 "B": Qt.green,
104 "C": Qt.yellow,
105 "D": Qt.yellow,
106 "E": Qt.red,
107 "F": Qt.red,
108 }
109
110 def __resizeResultColumns(self):
111 """
112 Private method to resize the list columns.
113 """
114 self.resultList.header().resizeSections(QHeaderView.ResizeToContents)
115 self.resultList.header().setStretchLastSection(True)
116
117 def __createFileItem(self, filename):
118 """
119 Private method to create a new file item in the result list.
120
121 @param filename name of the file
122 @type str
123 @return reference to the created item
124 @rtype QTreeWidgetItem
125 """
126 itm = QTreeWidgetItem(
127 self.resultList,
128 [self.__project.getRelativePath(filename)])
129 itm.setExpanded(True)
130 itm.setFirstColumnSpanned(True)
131 return itm
132
133 def __createResultItem(self, parentItem, values):
134 """
135 Private slot to create a new item in the result list.
136
137 @param parentItem reference to the parent item
138 @type QTreeWidgetItem
139 @param values values to be displayed
140 @type dict
141 """
142 itm = QTreeWidgetItem(parentItem, [
143 self.__mappedType[values["type"]],
144 values["fullname"],
145 "{0:3}".format(values["complexity"]),
146 values["rank"],
147 "{0:6}".format(values["lineno"]),
148 ])
149 itm.setTextAlignment(2, Qt.Alignment(Qt.AlignRight))
150 itm.setTextAlignment(3, Qt.Alignment(Qt.AlignHCenter))
151 itm.setTextAlignment(4, Qt.Alignment(Qt.AlignRight))
152 if values["rank"] in ["A", "B", "C", "D", "E", "F"]:
153 itm.setBackground(3, self.__rankColors[values["rank"]])
154
155 if "methods" in values:
156 itm.setExpanded(True)
157 for method in values["methods"]:
158 self.__createResultItem(itm, method)
159
160 if "closures" in values and values["closures"]:
161 itm.setExpanded(True)
162 for closure in values["closures"]:
163 self.__createResultItem(itm, closure)
164
165 def __createErrorItem(self, filename, message):
166 """
167 Private slot to create a new error item in the result list.
168
169 @param filename name of the file
170 @type str
171 @param message error message
172 @type str
173 """
174 if self.__errorItem is None:
175 self.__errorItem = QTreeWidgetItem(self.resultList, [
176 self.tr("Errors")])
177 self.__errorItem.setExpanded(True)
178 self.__errorItem.setForeground(0, Qt.red)
179
180 msg = "{0} ({1})".format(self.__project.getRelativePath(filename),
181 message)
182 if not self.resultList.findItems(msg, Qt.MatchExactly):
183 itm = QTreeWidgetItem(self.__errorItem, [msg])
184 itm.setForeground(0, Qt.red)
185 itm.setFirstColumnSpanned(True)
186
187 def prepare(self, fileList, project):
188 """
189 Public method to prepare the dialog with a list of filenames.
190
191 @param fileList list of filenames
192 @type list of str
193 @param project reference to the project object
194 @type Project
195 """
196 self.__fileList = fileList[:]
197 self.__project = project
198
199 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
200 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
201 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
202
203 self.filterFrame.setVisible(True)
204
205 self.__data = self.__project.getData(
206 "OTHERTOOLSPARMS", "RadonCodeMetrics")
207 if self.__data is None or "ExcludeFiles" not in self.__data:
208 self.__data = {"ExcludeFiles": ""}
209 self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
210
211 def start(self, fn):
212 """
213 Public slot to start the cyclomatic complexity determination.
214
215 @param fn file or list of files or directory to show
216 the cyclomatic complexity for
217 @type str or list of str
218 """
219 self.__errorItem = None
220 self.resultList.clear()
221 self.cancelled = False
222 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
223 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
224 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
225 QApplication.processEvents()
226
227 if isinstance(fn, list):
228 self.files = fn
229 elif os.path.isdir(fn):
230 self.files = []
231 extensions = set(Preferences.getPython("PythonExtensions") +
232 Preferences.getPython("Python3Extensions"))
233 for ext in extensions:
234 self.files.extend(
235 Utilities.direntries(fn, True, '*{0}'.format(ext), 0))
236 else:
237 self.files = [fn]
238 self.files.sort()
239 # check for missing files
240 for f in self.files[:]:
241 if not os.path.exists(f):
242 self.files.remove(f)
243
244 self.__summary = {
245 "A": 0,
246 "B": 0,
247 "C": 0,
248 "D": 0,
249 "E": 0,
250 "F": 0,
251 }
252 self.__ccSum = 0
253 self.__ccCount = 0
254
255 self.__mappedType = {
256 "class": "C",
257 "function": "F",
258 "method": "M",
259 }
260
261 if len(self.files) > 0:
262 # disable updates of the list for speed
263 self.resultList.setUpdatesEnabled(False)
264 self.resultList.setSortingEnabled(False)
265
266 self.checkProgress.setMaximum(len(self.files))
267 self.checkProgress.setVisible(len(self.files) > 1)
268 self.checkProgressLabel.setVisible(len(self.files) > 1)
269 QApplication.processEvents()
270
271 # now go through all the files
272 self.progress = 0
273 if len(self.files) == 1 or not self.radonService.hasBatch:
274 self.__batch = False
275 self.cyclomaticComplexity()
276 else:
277 self.__batch = True
278 self.cyclomaticComplexityBatch()
279
280 def cyclomaticComplexity(self, codestring=''):
281 """
282 Public method to start a cyclomatic complexity calculation for one
283 Python file.
284
285 The results are reported to the __processResult slot.
286
287 @keyparam codestring optional sourcestring
288 @type str
289 """
290 if not self.files:
291 self.checkProgressLabel.setPath("")
292 self.checkProgress.setMaximum(1)
293 self.checkProgress.setValue(1)
294 self.__finish()
295 return
296
297 self.filename = self.files.pop(0)
298 self.checkProgress.setValue(self.progress)
299 self.checkProgressLabel.setPath(self.filename)
300 QApplication.processEvents()
301
302 if self.cancelled:
303 return
304
305 try:
306 self.source = Utilities.readEncodedFile(self.filename)[0]
307 self.source = Utilities.normalizeCode(self.source)
308 except (UnicodeError, IOError) as msg:
309 self.__createErrorItem(self.filename, str(msg).rstrip())
310 self.progress += 1
311 # Continue with next file
312 self.cyclomaticComplexity()
313 return
314
315 self.__finished = False
316 self.radonService.cyclomaticComplexity(
317 None, self.filename, self.source)
318
319 def cyclomaticComplexityBatch(self):
320 """
321 Public method to start a cyclomatic complexity calculation batch job.
322
323 The results are reported to the __processResult slot.
324 """
325 self.__lastFileItem = None
326
327 self.checkProgressLabel.setPath(self.tr("Preparing files..."))
328 progress = 0
329
330 argumentsList = []
331 for filename in self.files:
332 progress += 1
333 self.checkProgress.setValue(progress)
334 QApplication.processEvents()
335
336 try:
337 source = Utilities.readEncodedFile(filename)[0]
338 source = Utilities.normalizeCode(source)
339 except (UnicodeError, IOError) as msg:
340 self.__createErrorItem(filename, str(msg).rstrip())
341 continue
342
343 argumentsList.append((filename, source))
344
345 # reset the progress bar to the checked files
346 self.checkProgress.setValue(self.progress)
347 QApplication.processEvents()
348
349 self.__finished = False
350 self.radonService.cyclomaticComplexityBatch(argumentsList)
351
352 def __batchFinished(self, type_):
353 """
354 Private slot handling the completion of a batch job.
355
356 @param type_ type of the calculated metrics
357 @type str, one of ["raw", "mi", "cc"]
358 """
359 if type_ == "cc":
360 self.checkProgressLabel.setPath("")
361 self.checkProgress.setMaximum(1)
362 self.checkProgress.setValue(1)
363 self.__finish()
364
365 def __processError(self, type_, fn, msg):
366 """
367 Private slot to process an error indication from the service.
368
369 @param type_ type of the calculated metrics
370 @type str, one of ["raw", "mi", "cc"]
371 @param fn filename of the file
372 @type str
373 @param msg error message
374 @type str
375 """
376 if type_ == "cc":
377 self.__createErrorItem(fn, msg)
378
379 def __processResult(self, fn, result):
380 """
381 Private slot called after perfoming a cyclomatic complexity calculation
382 on one file.
383
384 @param fn filename of the file
385 @type str
386 @param result result dict
387 @type dict
388 """
389 if self.__finished:
390 return
391
392 # Check if it's the requested file, otherwise ignore signal if not
393 # in batch mode
394 if not self.__batch and fn != self.filename:
395 return
396
397 if "error" in result:
398 self.__createErrorItem(fn, result["error"])
399 else:
400 if result["result"]:
401 fitm = self.__createFileItem(fn)
402 for resultDict in result["result"]:
403 self.__createResultItem(fitm, resultDict)
404
405 self.__ccCount += result["count"]
406 self.__ccSum += result["total_cc"]
407 for rank in result["summary"]:
408 self.__summary[rank] += result["summary"][rank]
409
410 self.progress += 1
411
412 self.checkProgress.setValue(self.progress)
413 self.checkProgressLabel.setPath(self.__project.getRelativePath(fn))
414 QApplication.processEvents()
415
416 if not self.__batch:
417 self.cyclomaticComplexity()
418
419 def __finish(self):
420 """
421 Private slot called when the action or the user pressed the button.
422 """
423 from radon.complexity import cc_rank
424
425 if not self.__finished:
426 self.__finished = True
427
428 # reenable updates of the list
429 self.resultList.setSortingEnabled(True)
430 self.resultList.sortItems(1, Qt.AscendingOrder)
431 self.resultList.setUpdatesEnabled(True)
432
433 self.cancelled = True
434 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
435 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
436 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
437
438 self.resultList.header().resizeSections(
439 QHeaderView.ResizeToContents)
440 self.resultList.header().setStretchLastSection(True)
441 if qVersion() >= "5.0.0":
442 self.resultList.header().setSectionResizeMode(
443 QHeaderView.Interactive)
444 else:
445 self.resultList.header().setResizeMode(QHeaderView.Interactive)
446
447 averageCC = float(self.__ccSum) / self.__ccCount
448
449 self.summaryLabel.setText(self.tr(
450 "<b>Summary:</b><br/>"
451 "{0} blocks (classes, functions, methods) analyzed.<br/>"
452 "Average complexity: {7} ({8})"
453 "<table>"
454 "<tr><td width=30><b>A</b></td><td>{1} blocks</td></tr>"
455 "<tr><td width=30><b>B</b></td><td>{2} blocks</td></tr>"
456 "<tr><td width=30><b>C</b></td><td>{3} blocks</td></tr>"
457 "<tr><td width=30><b>D</b></td><td>{4} blocks</td></tr>"
458 "<tr><td width=30><b>E</b></td><td>{5} blocks</td></tr>"
459 "<tr><td width=30><b>F</b></td><td>{6} blocks</td></tr>"
460 "</table>"
461 ).format(
462 self.__ccCount,
463 self.__summary["A"],
464 self.__summary["B"],
465 self.__summary["C"],
466 self.__summary["D"],
467 self.__summary["E"],
468 self.__summary["F"],
469 cc_rank(averageCC),
470 self.__locale.toString(averageCC, "f", 1)
471 ))
472
473 self.checkProgress.setVisible(False)
474 self.checkProgressLabel.setVisible(False)
475
476 @pyqtSlot(QAbstractButton)
477 def on_buttonBox_clicked(self, button):
478 """
479 Private slot called by a button of the button box clicked.
480
481 @param button button that was clicked
482 @type QAbstractButton
483 """
484 if button == self.buttonBox.button(QDialogButtonBox.Close):
485 self.close()
486 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
487 if self.__batch:
488 self.radonService.cancelComplexityBatch()
489 QTimer.singleShot(1000, self.__finish)
490 else:
491 self.__finish()
492
493 @pyqtSlot()
494 def on_startButton_clicked(self):
495 """
496 Private slot to start a cyclomatic complexity run.
497 """
498 fileList = self.__fileList[:]
499
500 filterString = self.excludeFilesEdit.text()
501 if "ExcludeFiles" not in self.__data or \
502 filterString != self.__data["ExcludeFiles"]:
503 self.__data["ExcludeFiles"] = filterString
504 self.__project.setData(
505 "OTHERTOOLSPARMS", "RadonCodeMetrics", self.__data)
506 filterList = [f.strip() for f in filterString.split(",")
507 if f.strip()]
508 if filterList:
509 for filter in filterList:
510 fileList = \
511 [f for f in fileList if not fnmatch.fnmatch(f, filter)]
512
513 self.start(fileList)

eric ide

mercurial