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