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