1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2003 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a code metrics dialog. 8""" 9 10import os 11import fnmatch 12import collections 13 14from PyQt5.QtCore import pyqtSlot, Qt, QLocale 15from PyQt5.QtWidgets import ( 16 QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem, 17 QApplication 18) 19 20from .Ui_CodeMetricsDialog import Ui_CodeMetricsDialog 21from . import CodeMetrics 22 23import Utilities 24 25 26class CodeMetricsDialog(QDialog, Ui_CodeMetricsDialog): 27 """ 28 Class implementing a dialog to display the code metrics. 29 """ 30 def __init__(self, parent=None): 31 """ 32 Constructor 33 34 @param parent parent widget (QWidget) 35 """ 36 super().__init__(parent) 37 self.setupUi(self) 38 self.setWindowFlags(Qt.WindowType.Window) 39 40 self.buttonBox.button( 41 QDialogButtonBox.StandardButton.Close).setEnabled(False) 42 self.buttonBox.button( 43 QDialogButtonBox.StandardButton.Cancel).setDefault(True) 44 45 self.summaryList.headerItem().setText( 46 self.summaryList.columnCount(), "") 47 self.summaryList.header().resizeSection(0, 200) 48 self.summaryList.header().resizeSection(1, 100) 49 50 self.resultList.headerItem().setText(self.resultList.columnCount(), "") 51 52 self.cancelled = False 53 54 self.__menu = QMenu(self) 55 self.__menu.addAction(self.tr("Collapse All"), 56 self.__resultCollapse) 57 self.__menu.addAction(self.tr("Expand All"), self.__resultExpand) 58 self.resultList.setContextMenuPolicy( 59 Qt.ContextMenuPolicy.CustomContextMenu) 60 self.resultList.customContextMenuRequested.connect( 61 self.__showContextMenu) 62 63 self.__fileList = [] 64 self.__project = None 65 self.filterFrame.setVisible(False) 66 67 def __resizeResultColumns(self): 68 """ 69 Private method to resize the list columns. 70 """ 71 self.resultList.header().resizeSections( 72 QHeaderView.ResizeMode.ResizeToContents) 73 self.resultList.header().setStretchLastSection(True) 74 75 def __createResultItem(self, parent, values): 76 """ 77 Private slot to create a new item in the result list. 78 79 @param parent parent of the new item (QTreeWidget or QTreeWidgetItem) 80 @param values values to be displayed (list) 81 @return the generated item 82 """ 83 data = [values[0]] 84 for value in values[1:]: 85 try: 86 data.append("{0:5}".format(int(value))) 87 except ValueError: 88 data.append(value) 89 itm = QTreeWidgetItem(parent, data) 90 for col in range(1, 7): 91 itm.setTextAlignment( 92 col, Qt.Alignment(Qt.AlignmentFlag.AlignRight)) 93 return itm 94 95 def __resizeSummaryColumns(self): 96 """ 97 Private method to resize the list columns. 98 """ 99 self.summaryList.header().resizeSections( 100 QHeaderView.ResizeMode.ResizeToContents) 101 self.summaryList.header().setStretchLastSection(True) 102 103 def __createSummaryItem(self, col0, col1): 104 """ 105 Private slot to create a new item in the summary list. 106 107 @param col0 string for column 0 (string) 108 @param col1 string for column 1 (string) 109 """ 110 itm = QTreeWidgetItem(self.summaryList, [col0, col1]) 111 itm.setTextAlignment(1, Qt.Alignment(Qt.AlignmentFlag.AlignRight)) 112 113 def prepare(self, fileList, project): 114 """ 115 Public method to prepare the dialog with a list of filenames. 116 117 @param fileList list of filenames (list of strings) 118 @param project reference to the project object (Project) 119 """ 120 self.__fileList = fileList[:] 121 self.__project = project 122 123 self.buttonBox.button( 124 QDialogButtonBox.StandardButton.Close).setEnabled(True) 125 self.buttonBox.button( 126 QDialogButtonBox.StandardButton.Cancel).setEnabled(False) 127 self.buttonBox.button( 128 QDialogButtonBox.StandardButton.Close).setDefault(True) 129 130 self.filterFrame.setVisible(True) 131 132 self.__data = self.__project.getData("OTHERTOOLSPARMS", "CodeMetrics") 133 if self.__data is None or "ExcludeFiles" not in self.__data: 134 self.__data = {"ExcludeFiles": ""} 135 self.excludeFilesEdit.setText(self.__data["ExcludeFiles"]) 136 137 def start(self, fn): 138 """ 139 Public slot to start the code metrics determination. 140 141 @param fn file or list of files or directory to show 142 the code metrics for (string or list of strings) 143 """ 144 self.cancelled = False 145 self.buttonBox.button( 146 QDialogButtonBox.StandardButton.Close).setEnabled(False) 147 self.buttonBox.button( 148 QDialogButtonBox.StandardButton.Cancel).setEnabled(True) 149 self.buttonBox.button( 150 QDialogButtonBox.StandardButton.Cancel).setDefault(True) 151 QApplication.processEvents() 152 153 loc = QLocale() 154 if isinstance(fn, list): 155 files = fn 156 elif os.path.isdir(fn): 157 files = Utilities.direntries(fn, True, '*.py', False) 158 else: 159 files = [fn] 160 files.sort() 161 # check for missing files 162 for f in files[:]: 163 if not os.path.exists(f): 164 files.remove(f) 165 166 self.checkProgress.setMaximum(len(files)) 167 QApplication.processEvents() 168 169 total = collections.defaultdict(int) 170 CodeMetrics.summarize(total, 'files', len(files)) 171 172 try: 173 # disable updates of the list for speed 174 self.resultList.setUpdatesEnabled(False) 175 self.resultList.setSortingEnabled(False) 176 177 # now go through all the files 178 for progress, file in enumerate(files, start=1): 179 if self.cancelled: 180 return 181 182 stats = CodeMetrics.analyze(file, total) 183 184 v = self.__getValues(loc, stats, 'TOTAL ') 185 fitm = self.__createResultItem(self.resultList, [file] + v) 186 187 identifiers = stats.identifiers 188 for identifier in identifiers: 189 v = self.__getValues(loc, stats, identifier) 190 191 self.__createResultItem(fitm, [identifier] + v) 192 self.resultList.expandItem(fitm) 193 194 self.checkProgress.setValue(progress) 195 QApplication.processEvents() 196 finally: 197 # reenable updates of the list 198 self.resultList.setSortingEnabled(True) 199 self.resultList.setUpdatesEnabled(True) 200 self.__resizeResultColumns() 201 202 # now do the summary stuff 203 self.__createSummaryItem(self.tr("files"), 204 loc.toString(total['files'])) 205 self.__createSummaryItem(self.tr("lines"), 206 loc.toString(total['lines'])) 207 self.__createSummaryItem(self.tr("bytes"), 208 loc.toString(total['bytes'])) 209 self.__createSummaryItem(self.tr("comments"), 210 loc.toString(total['comments'])) 211 self.__createSummaryItem(self.tr("comment lines"), 212 loc.toString(total['commentlines'])) 213 self.__createSummaryItem(self.tr("empty lines"), 214 loc.toString(total['empty lines'])) 215 self.__createSummaryItem(self.tr("non-commentary lines"), 216 loc.toString(total['non-commentary lines'])) 217 self.__resizeSummaryColumns() 218 self.__finish() 219 220 def __getValues(self, loc, stats, identifier): 221 """ 222 Private method to extract the code metric values. 223 224 @param loc reference to the locale object (QLocale) 225 @param stats reference to the code metric statistics object 226 @param identifier identifier to get values for 227 @return list of values suitable for display (list of strings) 228 """ 229 counters = stats.counters.get(identifier, {}) 230 v = [] 231 for key in ('start', 'end', 'lines', 'nloc', 'commentlines', 'empty'): 232 if counters.get(key, 0): 233 v.append(loc.toString(counters[key])) 234 else: 235 v.append('') 236 return v 237 238 def __finish(self): 239 """ 240 Private slot called when the action finished or the user pressed the 241 button. 242 """ 243 self.cancelled = True 244 self.buttonBox.button( 245 QDialogButtonBox.StandardButton.Close).setEnabled(True) 246 self.buttonBox.button( 247 QDialogButtonBox.StandardButton.Cancel).setEnabled(False) 248 self.buttonBox.button( 249 QDialogButtonBox.StandardButton.Close).setDefault(True) 250 251 self.resultList.header().setSectionResizeMode( 252 QHeaderView.ResizeMode.Interactive) 253 self.summaryList.header().setSectionResizeMode( 254 QHeaderView.ResizeMode.Interactive) 255 256 def on_buttonBox_clicked(self, button): 257 """ 258 Private slot called by a button of the button box clicked. 259 260 @param button button that was clicked (QAbstractButton) 261 """ 262 if button == self.buttonBox.button( 263 QDialogButtonBox.StandardButton.Close 264 ): 265 self.close() 266 elif button == self.buttonBox.button( 267 QDialogButtonBox.StandardButton.Cancel 268 ): 269 self.__finish() 270 271 @pyqtSlot() 272 def on_startButton_clicked(self): 273 """ 274 Private slot to start a code metrics run. 275 """ 276 fileList = self.__fileList[:] 277 278 filterString = self.excludeFilesEdit.text() 279 if ("ExcludeFiles" not in self.__data or 280 filterString != self.__data["ExcludeFiles"]): 281 self.__data["ExcludeFiles"] = filterString 282 self.__project.setData("OTHERTOOLSPARMS", "CodeMetrics", 283 self.__data) 284 filterList = filterString.split(",") 285 if filterList: 286 for filterString in filterList: 287 fileList = [f for f in fileList 288 if not fnmatch.fnmatch(f, filterString.strip())] 289 290 self.resultList.clear() 291 self.summaryList.clear() 292 self.start(fileList) 293 294 def __showContextMenu(self, coord): 295 """ 296 Private slot to show the context menu of the listview. 297 298 @param coord the position of the mouse pointer (QPoint) 299 """ 300 if self.resultList.topLevelItemCount() > 0: 301 self.__menu.popup(self.mapToGlobal(coord)) 302 303 def __resultCollapse(self): 304 """ 305 Private slot to collapse all entries of the resultlist. 306 """ 307 for index in range(self.resultList.topLevelItemCount()): 308 self.resultList.topLevelItem(index).setExpanded(False) 309 310 def __resultExpand(self): 311 """ 312 Private slot to expand all entries of the resultlist. 313 """ 314 for index in range(self.resultList.topLevelItemCount()): 315 self.resultList.topLevelItem(index).setExpanded(True) 316