diff -r e9e7eca7efee -r bf71ee032bb4 src/eric7/DataViews/PyCoverageDialog.py --- a/src/eric7/DataViews/PyCoverageDialog.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/DataViews/PyCoverageDialog.py Wed Jul 13 14:55:47 2022 +0200 @@ -13,8 +13,12 @@ from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl from PyQt6.QtGui import QDesktopServices from PyQt6.QtWidgets import ( - QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem, - QApplication + QDialog, + QDialogButtonBox, + QMenu, + QHeaderView, + QTreeWidgetItem, + QApplication, ) from EricWidgets import EricMessageBox @@ -30,61 +34,56 @@ class PyCoverageDialog(QDialog, Ui_PyCoverageDialog): """ Class implementing a dialog to display the collected code coverage data. - + @signal openFile(str) emitted to open the given file in an editor """ + openFile = pyqtSignal(str) - + def __init__(self, parent=None): """ Constructor - + @param parent parent widget @type QWidget """ super().__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.WindowType.Window) - - self.buttonBox.button( - QDialogButtonBox.StandardButton.Close).setEnabled(False) - self.buttonBox.button( - QDialogButtonBox.StandardButton.Cancel).setDefault(True) - - self.summaryList.headerItem().setText( - self.summaryList.columnCount(), "") + + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True) + + self.summaryList.headerItem().setText(self.summaryList.columnCount(), "") self.resultList.headerItem().setText(self.resultList.columnCount(), "") - + self.cancelled = False - self.path = '.' + self.path = "." self.reload = False - - self.excludeList = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]'] - + + self.excludeList = ["# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]"] + self.__reportsMenu = QMenu(self.tr("Create Report"), self) self.__reportsMenu.addAction(self.tr("HTML Report"), self.__htmlReport) self.__reportsMenu.addSeparator() self.__reportsMenu.addAction(self.tr("JSON Report"), self.__jsonReport) self.__reportsMenu.addAction(self.tr("LCOV Report"), self.__lcovReport) - + self.__menu = QMenu(self) self.__menu.addSeparator() - self.openAct = self.__menu.addAction( - self.tr("Open"), self.__openFile) + self.openAct = self.__menu.addAction(self.tr("Open"), self.__openFile) self.__menu.addSeparator() self.__menu.addMenu(self.__reportsMenu) self.__menu.addSeparator() - self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase) - self.resultList.setContextMenuPolicy( - Qt.ContextMenuPolicy.CustomContextMenu) - self.resultList.customContextMenuRequested.connect( - self.__showContextMenu) - + self.__menu.addAction(self.tr("Erase Coverage Info"), self.__erase) + self.resultList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.resultList.customContextMenuRequested.connect(self.__showContextMenu) + def __format_lines(self, lines): """ Private method to format a list of integers into string by coalescing groups. - + @param lines list of integers @type list of int @return string representing the list @@ -94,7 +93,7 @@ lines.sort() maxValue = lines[-1] start = None - + i = lines[0] while i <= maxValue: try: @@ -111,12 +110,12 @@ i = lines[ind + 1] if start: pairs.append((start, end)) - + def stringify(pair): """ Private helper function to generate a string representation of a pair. - + @param pair pair of integers @type tuple of (int, int @return representation of the pair @@ -127,14 +126,15 @@ return "{0:d}".format(start) else: return "{0:d}-{1:d}".format(start, end) - + return ", ".join(map(stringify, pairs)) - - def __createResultItem(self, file, statements, executed, coverage, - excluded, missing): + + def __createResultItem( + self, file, statements, executed, coverage, excluded, missing + ): """ Private method to create an entry in the result list. - + @param file filename of file @type str @param statements number of statements @@ -148,14 +148,17 @@ @param missing list of lines without coverage @type str """ - itm = QTreeWidgetItem(self.resultList, [ - file, - str(statements), - str(executed), - "{0:.0f}%".format(coverage), - excluded, - missing - ]) + itm = QTreeWidgetItem( + self.resultList, + [ + file, + str(statements), + str(executed), + "{0:.0f}%".format(coverage), + excluded, + missing, + ], + ) for col in range(1, 4): itm.setTextAlignment(col, Qt.AlignmentFlag.AlignRight) if statements != executed: @@ -163,11 +166,11 @@ font.setBold(True) for col in range(itm.columnCount()): itm.setFont(col, font) - + def start(self, cfn, fn): """ Public slot to start the coverage data evaluation. - + @param cfn basename of the coverage file @type str @param fn file or list of files or directory to be checked @@ -177,76 +180,72 @@ self.resultList.clear() self.summaryList.clear() self.cancelled = False - self.buttonBox.button( - QDialogButtonBox.StandardButton.Close).setEnabled(False) - self.buttonBox.button( - QDialogButtonBox.StandardButton.Cancel).setEnabled(True) - self.buttonBox.button( - QDialogButtonBox.StandardButton.Cancel).setDefault(True) - + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True) + self.__cfn = cfn self.__fn = fn - + self.cfn = ( cfn - if cfn.endswith(".coverage") else - "{0}.coverage".format(os.path.splitext(cfn)[0]) + if cfn.endswith(".coverage") + else "{0}.coverage".format(os.path.splitext(cfn)[0]) ) - + if isinstance(fn, list): files = fn self.path = os.path.dirname(cfn) elif os.path.isdir(fn): - files = Utilities.direntries(fn, True, '*.py', False) + files = Utilities.direntries(fn, True, "*.py", False) self.path = fn else: files = [fn] self.path = os.path.dirname(cfn) files.sort() - + cover = Coverage(data_file=self.cfn) cover.load() - + # set the exclude pattern self.excludeCombo.clear() self.excludeCombo.addItems(self.excludeList) - + self.checkProgress.setMaximum(len(files)) QApplication.processEvents() - + total_statements = 0 total_executed = 0 total_exceptions = 0 - + cover.exclude(self.excludeList[0]) - + try: # disable updates of the list for speed self.resultList.setUpdatesEnabled(False) self.resultList.setSortingEnabled(False) - + # now go through all the files now = time.monotonic() for progress, file in enumerate(files, start=1): if self.cancelled: return - + try: - statements, excluded, missing, readable = ( - cover.analysis2(file)[1:]) - readableEx = (excluded and self.__format_lines(excluded) or - '') + statements, excluded, missing, readable = cover.analysis2(file)[1:] + readableEx = excluded and self.__format_lines(excluded) or "" n = len(statements) m = n - len(missing) pc = 100.0 * m / n if n > 0 else 100.0 self.__createResultItem( - file, str(n), str(m), pc, readableEx, readable) - + file, str(n), str(m), pc, readableEx, readable + ) + total_statements += n total_executed += m except CoverageException: total_exceptions += 1 - + self.checkProgress.setValue(progress) if time.monotonic() - now > 0.01: QApplication.processEvents() @@ -256,73 +255,69 @@ self.resultList.setSortingEnabled(True) self.resultList.setUpdatesEnabled(True) self.checkProgress.reset() - + # show summary info if len(files) > 1: if total_statements > 0: pc = 100.0 * total_executed / total_statements else: pc = 100.0 - itm = QTreeWidgetItem(self.summaryList, [ - str(total_statements), - str(total_executed), - "{0:.0f}%".format(pc) - ]) + itm = QTreeWidgetItem( + self.summaryList, + [str(total_statements), str(total_executed), "{0:.0f}%".format(pc)], + ) for col in range(0, 3): itm.setTextAlignment(col, Qt.AlignmentFlag.AlignRight) else: self.summaryGroup.hide() - + if total_exceptions: EricMessageBox.warning( self, self.tr("Parse Error"), - self.tr("""%n file(s) could not be parsed. Coverage""" - """ info for these is not available.""", "", - total_exceptions)) - + self.tr( + """%n file(s) could not be parsed. Coverage""" + """ info for these is not available.""", + "", + total_exceptions, + ), + ) + self.__finish() - + def __finish(self): """ Private slot called when the action finished or the user pressed the button. """ self.cancelled = True - self.buttonBox.button( - QDialogButtonBox.StandardButton.Close).setEnabled(True) - self.buttonBox.button( - QDialogButtonBox.StandardButton.Cancel).setEnabled(False) - self.buttonBox.button( - QDialogButtonBox.StandardButton.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True) QApplication.processEvents() - self.resultList.header().resizeSections( - QHeaderView.ResizeMode.ResizeToContents) + self.resultList.header().resizeSections(QHeaderView.ResizeMode.ResizeToContents) self.resultList.header().setStretchLastSection(True) self.summaryList.header().resizeSections( - QHeaderView.ResizeMode.ResizeToContents) + QHeaderView.ResizeMode.ResizeToContents + ) self.summaryList.header().setStretchLastSection(True) - + def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. - + @param button button that was clicked @type QAbstractButton """ - if button == self.buttonBox.button( - QDialogButtonBox.StandardButton.Close - ): + if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close): self.close() - elif button == self.buttonBox.button( - QDialogButtonBox.StandardButton.Cancel - ): + elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel): self.__finish() - + def __showContextMenu(self, coord): """ Private slot to show the context menu of the listview. - + @param coord position of the mouse pointer @type QPoint """ @@ -331,21 +326,20 @@ self.openAct.setEnabled(True) else: self.openAct.setEnabled(False) - self.__reportsMenu.setEnabled( - bool(self.resultList.topLevelItemCount())) + self.__reportsMenu.setEnabled(bool(self.resultList.topLevelItemCount())) self.__menu.popup(self.mapToGlobal(coord)) - + def __openFile(self, itm=None): """ Private slot to open the selected file. - + @param itm reference to the item to be opened @type QTreeWidgetItem """ if itm is None: itm = self.resultList.currentItem() fn = itm.text(0) - + try: vm = ericApp().getObject("ViewManager") vm.openSourceFile(fn) @@ -353,11 +347,11 @@ editor.codeCoverageShowAnnotations(coverageFile=self.cfn) except KeyError: self.openFile.emit(fn) - + def __prepareReportGeneration(self): """ Private method to prepare a report generation. - + @return tuple containing a reference to the Coverage object and the list of files to report @rtype tuple of (Coverage, list of str) @@ -365,53 +359,59 @@ count = self.resultList.topLevelItemCount() if count == 0: return None, [] - + # get list of all filenames - files = [ - self.resultList.topLevelItem(index).text(0) - for index in range(count) - ] - + files = [self.resultList.topLevelItem(index).text(0) for index in range(count)] + cover = Coverage(data_file=self.cfn) cover.exclude(self.excludeList[0]) cover.load() - + return cover, files - + @pyqtSlot() def __htmlReport(self): """ Private slot to generate a HTML report of the shown data. """ from .PyCoverageHtmlReportDialog import PyCoverageHtmlReportDialog - + dlg = PyCoverageHtmlReportDialog(os.path.dirname(self.cfn), self) if dlg.exec() == QDialog.DialogCode.Accepted: title, outputDirectory, extraCSS, openReport = dlg.getData() - + cover, files = self.__prepareReportGeneration() - cover.html_report(morfs=files, directory=outputDirectory, - ignore_errors=True, extra_css=extraCSS, - title=title) - + cover.html_report( + morfs=files, + directory=outputDirectory, + ignore_errors=True, + extra_css=extraCSS, + title=title, + ) + if openReport: - QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.join( - outputDirectory, "index.html"))) - + QDesktopServices.openUrl( + QUrl.fromLocalFile(os.path.join(outputDirectory, "index.html")) + ) + @pyqtSlot() def __jsonReport(self): """ Private slot to generate a JSON report of the shown data. """ from .PyCoverageJsonReportDialog import PyCoverageJsonReportDialog - + dlg = PyCoverageJsonReportDialog(os.path.dirname(self.cfn), self) if dlg.exec() == QDialog.DialogCode.Accepted: filename, compact = dlg.getData() cover, files = self.__prepareReportGeneration() - cover.json_report(morfs=files, outfile=filename, - ignore_errors=True, pretty_print=not compact) - + cover.json_report( + morfs=files, + outfile=filename, + ignore_errors=True, + pretty_print=not compact, + ) + @pyqtSlot() def __lcovReport(self): """ @@ -419,7 +419,7 @@ """ from EricWidgets import EricPathPickerDialog from EricWidgets.EricPathPicker import EricPathPickerModes - + filename, ok = EricPathPickerDialog.getPath( self, self.tr("LCOV Report"), @@ -427,28 +427,27 @@ mode=EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE, path=os.path.join(os.path.dirname(self.cfn), "coverage.lcov"), defaultDirectory=os.path.dirname(self.cfn), - filters=self.tr("LCOV Files (*.lcov);;All Files (*)") + filters=self.tr("LCOV Files (*.lcov);;All Files (*)"), ) if ok: cover, files = self.__prepareReportGeneration() - cover.lcov_report(morfs=files, outfile=filename, - ignore_errors=True) - + cover.lcov_report(morfs=files, outfile=filename, ignore_errors=True) + def __erase(self): """ Private slot to handle the erase context menu action. - + This method erases the collected coverage data that is stored in the .coverage file. """ cover = Coverage(data_file=self.cfn) cover.load() cover.erase() - + self.reloadButton.setEnabled(False) self.resultList.clear() self.summaryList.clear() - + @pyqtSlot() def on_reloadButton_clicked(self): """ @@ -460,12 +459,12 @@ self.excludeList.remove(excludePattern) self.excludeList.insert(0, excludePattern) self.start(self.__cfn, self.__fn) - + @pyqtSlot(QTreeWidgetItem, int) def on_resultList_itemActivated(self, item, column): """ Private slot to handle the activation of an item. - + @param item reference to the activated item (QTreeWidgetItem) @param column column the item was activated in (integer) """