|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a code metrics dialog. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import fnmatch |
|
12 import collections |
|
13 |
|
14 from PyQt5.QtCore import pyqtSlot, Qt, QLocale |
|
15 from PyQt5.QtWidgets import ( |
|
16 QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem, |
|
17 QApplication |
|
18 ) |
|
19 |
|
20 from .Ui_CodeMetricsDialog import Ui_CodeMetricsDialog |
|
21 from . import CodeMetrics |
|
22 |
|
23 import Utilities |
|
24 |
|
25 |
|
26 class 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) |