Thu, 19 May 2022 14:37:01 +0200
Dataview Coverage
- added support to write coverage reports as HTML, JSON or LCOV files
- removed the support for writing annotated sources (deprecated in coverage.py)
--- a/docs/changelog Wed May 18 11:02:59 2022 +0200 +++ b/docs/changelog Thu May 19 14:37:01 2022 +0200 @@ -2,6 +2,10 @@ ---------- Version 22.6: - bug fixes +- Dataview Coverage + -- added support to write coverage reports as HTML, JSON or LCOV files + -- removed the support for writing annotated sources + (deprecated in coverage.py) - Mercurial Interface -- added configuration option to override the automatic search for the hg executable
--- a/eric7.epj Wed May 18 11:02:59 2022 +0200 +++ b/eric7.epj Thu May 19 14:37:01 2022 +0200 @@ -277,6 +277,7 @@ "eric7/Cooperation/ChatWidget.ui", "eric7/DataViews/CodeMetricsDialog.ui", "eric7/DataViews/PyCoverageDialog.ui", + "eric7/DataViews/PyCoverageHtmlReportDialog.ui", "eric7/DataViews/PyProfileDialog.ui", "eric7/Debugger/CallTraceViewer.ui", "eric7/Debugger/EditBreakpointDialog.ui", @@ -988,11 +989,11 @@ "eric7/DataViews/CodeMetrics.py", "eric7/DataViews/CodeMetricsDialog.py", "eric7/DataViews/PyCoverageDialog.py", + "eric7/DataViews/PyCoverageHtmlReportDialog.py", "eric7/DataViews/PyProfileDialog.py", "eric7/DataViews/__init__.py", "eric7/DebugClients/Python/AsyncFile.py", "eric7/DebugClients/Python/BreakpointWatch.py", - "eric7/DebugClients/Python/DCTestResult.py", "eric7/DebugClients/Python/DebugBase.py", "eric7/DebugClients/Python/DebugClient.py", "eric7/DebugClients/Python/DebugClientBase.py",
--- a/eric7/DataViews/PyCoverageDialog.py Wed May 18 11:02:59 2022 +0200 +++ b/eric7/DataViews/PyCoverageDialog.py Thu May 19 14:37:01 2022 +0200 @@ -7,11 +7,11 @@ Module implementing a Python code coverage dialog. """ -import contextlib import os import time -from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt +from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl +from PyQt6.QtGui import QDesktopServices from PyQt6.QtWidgets import ( QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem, QApplication @@ -19,7 +19,6 @@ from EricWidgets import EricMessageBox from EricWidgets.EricApplication import ericApp -from EricWidgets.EricProgressDialog import EricProgressDialog from .Ui_PyCoverageDialog import Ui_PyCoverageDialog @@ -62,16 +61,17 @@ 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.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.__menu.addSeparator() - self.annotate = self.__menu.addAction( - self.tr('Annotate'), self.__annotate) - self.__menu.addAction(self.tr('Annotate all'), self.__annotateAll) - self.__menu.addAction( - self.tr('Delete annotated files'), self.__deleteAnnotated) + self.__menu.addMenu(self.__reportsMenu) self.__menu.addSeparator() self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase) self.resultList.setContextMenuPolicy( @@ -314,11 +314,11 @@ """ itm = self.resultList.itemAt(coord) if itm: - self.annotate.setEnabled(True) self.openAct.setEnabled(True) else: - self.annotate.setEnabled(False) self.openAct.setEnabled(False) + self.__reportsMenu.setEnabled( + bool(self.resultList.topLevelItemCount())) self.__menu.popup(self.mapToGlobal(coord)) def __openFile(self, itm=None): @@ -340,58 +340,85 @@ except KeyError: self.openFile.emit(fn) - # TODO: Coverage.annotate is deprecated - def __annotate(self): + def __prepareReportGeneration(self): """ - Private slot to handle the annotate context menu action. - - This method produces an annotated coverage file of the - selected file. - """ - itm = self.resultList.currentItem() - fn = itm.text(0) + Private method to prepare a report generation. - cover = Coverage(data_file=self.cfn) - cover.exclude(self.excludeList[0]) - cover.load() - cover.annotate([fn], None, True) - - # TODO: Coverage.annotate is deprecated - def __annotateAll(self): + @return tuple containing a reference to the Coverage object and the + list of files to report + @rtype tuple of (Coverage, list of str) """ - Private slot to handle the annotate all context menu action. - - This method produce an annotated coverage file of every - file listed in the listview. - """ - amount = self.resultList.topLevelItemCount() - if amount == 0: - return + count = self.resultList.topLevelItemCount() + if count == 0: + return None, [] # get list of all filenames - files = [] - for index in range(amount): - itm = self.resultList.topLevelItem(index) - files.append(itm.text(0)) + 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() - # now process them - progress = EricProgressDialog( - self.tr("Annotating files..."), self.tr("Abort"), - 0, len(files), self.tr("%v/%m Files"), self) - progress.setMinimumDuration(0) - progress.setWindowTitle(self.tr("Coverage")) + 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) + + if openReport: + 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 - for count, file in enumerate(files): - progress.setValue(count) - if progress.wasCanceled(): - break - cover.annotate([file], None) # , True) + 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) + + @pyqtSlot() + def __lcovReport(self): + """ + Private slot to generate a LCOV report of the shown data. + """ + from EricWidgets import EricPathPickerDialog + from EricWidgets.EricPathPicker import EricPathPickerModes - progress.setValue(len(files)) + filename, ok = EricPathPickerDialog.getPath( + self, + self.tr("LCOV Report"), + self.tr("Enter the path of the output file:"), + 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 (*)") + ) + if ok: + cover, files = self.__prepareReportGeneration() + cover.lcov_report(morfs=files, outfile=filename, + ignore_errors=True) def __erase(self): """ @@ -408,18 +435,6 @@ self.resultList.clear() self.summaryList.clear() - def __deleteAnnotated(self): - """ - Private slot to handle the delete annotated context menu action. - - This method deletes all annotated files. These are files - ending with ',cover'. - """ - files = Utilities.direntries(self.path, True, '*,cover', False) - for file in files: - with contextlib.suppress(OSError): - os.remove(file) - @pyqtSlot() def on_reloadButton_clicked(self): """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/DataViews/PyCoverageHtmlReportDialog.py Thu May 19 14:37:01 2022 +0200 @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to enter the parameters for a coverage HTML +report. +""" + +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import QDialog, QDialogButtonBox + +from EricWidgets.EricPathPicker import EricPathPickerModes + +from .Ui_PyCoverageHtmlReportDialog import Ui_PyCoverageHtmlReportDialog + + +class PyCoverageHtmlReportDialog(QDialog, Ui_PyCoverageHtmlReportDialog): + """ + Class implementing a dialog to enter the parameters for a coverage HTML + report. + """ + def __init__(self, defaultDirectory, parent=None): + """ + Constructor + + @param defaultDirectory default directory for selecting the output + directory + @type str + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.outputDirectoryPicker.setMode( + EricPathPickerModes.DIRECTORY_SHOW_FILES_MODE) + self.outputDirectoryPicker.setDefaultDirectory(defaultDirectory) + + self.extraCssPicker.setMode( + EricPathPickerModes.OPEN_FILE_MODE) + + self.buttonBox.button( + QDialogButtonBox.StandardButton.Ok).setEnabled(False) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + @pyqtSlot(str) + def on_outputDirectoryPicker_textChanged(self, directory): + """ + Private slot handling a change of the output directory. + + @param directory current text of the directory picker + @type str + """ + self.buttonBox.button( + QDialogButtonBox.StandardButton.Ok).setEnabled(bool(directory)) + + def getData(self): + """ + Public method to get the entered data. + + @return tuple containing the report title, the output directory, the + path of a file containing extra CSS and a flag indicating to open + the generated report in a browser + + @rtype tuple of (str, str, str, bool) + """ + title = self.titleEdit.text() + return ( + title if bool(title) else None, + self.outputDirectoryPicker.currentText(), + self.extraCssPicker.currentText(), + self.openReportCheckBox.isChecked(), + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/DataViews/PyCoverageHtmlReportDialog.ui Thu May 19 14:37:01 2022 +0200 @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PyCoverageHtmlReportDialog</class> + <widget class="QDialog" name="PyCoverageHtmlReportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>154</height> + </rect> + </property> + <property name="windowTitle"> + <string>HTML Report</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Title:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="EricPathPicker" name="extraCssPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path of a file containing additional CSS definitions</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Extra CSS:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="titleEdit"> + <property name="toolTip"> + <string>Enter the title for the HTML report</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Output Directory:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="EricPathPicker" name="outputDirectoryPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path of the output directory</string> + </property> + </widget> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QCheckBox" name="openReportCheckBox"> + <property name="toolTip"> + <string>Select to open the generated report</string> + </property> + <property name="text"> + <string>Open Report</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>EricPathPicker</class> + <extends>QWidget</extends> + <header>EricWidgets/EricPathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>titleEdit</tabstop> + <tabstop>outputDirectoryPicker</tabstop> + <tabstop>extraCssPicker</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PyCoverageHtmlReportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PyCoverageHtmlReportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/DataViews/PyCoverageJsonReportDialog.py Thu May 19 14:37:01 2022 +0200 @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to enter the parameters for a coverage JSON +report. +""" + +import os + +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import QDialog, QDialogButtonBox + +from EricWidgets.EricPathPicker import EricPathPickerModes + +from .Ui_PyCoverageJsonReportDialog import Ui_PyCoverageJsonReportDialog + + +class PyCoverageJsonReportDialog(QDialog, Ui_PyCoverageJsonReportDialog): + """ + Class implementing a dialog to enter the parameters for a coverage JSON + report. + """ + def __init__(self, defaultDirectory, parent=None): + """ + Constructor + + @param defaultDirectory default directory for selecting the output + directory + @type str + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.outputFilePicker.setMode( + EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE) + self.outputFilePicker.setDefaultDirectory(defaultDirectory) + self.outputFilePicker.setFilters( + self.tr("JSON Files (*.json);;All Files (*)")) + self.outputFilePicker.setText( + os.path.join(defaultDirectory, "coverage.json")) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + @pyqtSlot(str) + def on_outputFilePicker_textChanged(self, filename): + """ + Private slot handling a change of the output file. + + @param filename current text of the file picker + @type str + """ + self.buttonBox.button( + QDialogButtonBox.StandardButton.Ok).setEnabled(bool(filename)) + + def getData(self): + """ + Public method to get the entered data. + + @return tuple containing the output file and a flag indicating the + creation of a compact JSON file + + @rtype tuple of (str, bool) + """ + return ( + self.outputFilePicker.currentText(), + self.compactCheckBox.isChecked(), + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/DataViews/PyCoverageJsonReportDialog.ui Thu May 19 14:37:01 2022 +0200 @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PyCoverageJsonReportDialog</class> + <widget class="QDialog" name="PyCoverageJsonReportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>98</height> + </rect> + </property> + <property name="windowTitle"> + <string>JSON Report</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <widget class="QCheckBox" name="compactCheckBox"> + <property name="toolTip"> + <string>Select to open the generated report</string> + </property> + <property name="text"> + <string>Compact Format</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Output File:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="EricPathPicker" name="outputFilePicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path of the output file</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>EricPathPicker</class> + <extends>QWidget</extends> + <header>EricWidgets/EricPathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>outputFilePicker</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PyCoverageJsonReportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PyCoverageJsonReportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- a/eric7/UI/UserInterface.py Wed May 18 11:02:59 2022 +0200 +++ b/eric7/UI/UserInterface.py Thu May 19 14:37:01 2022 +0200 @@ -606,6 +606,7 @@ self.__initExternalToolsActions() # redirect handling of http and https URLs to ourselves + QDesktopServices.setUrlHandler("file", self.handleUrl) QDesktopServices.setUrlHandler("http", self.handleUrl) QDesktopServices.setUrlHandler("https", self.handleUrl)