Dataview Coverage unittest

Thu, 19 May 2022 14:37:01 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 19 May 2022 14:37:01 +0200
branch
unittest
changeset 9078
44d1d68096b6
parent 9075
f6f0236eacbc
child 9079
1311dc91e846

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)

docs/changelog file | annotate | diff | comparison | revisions
eric7.epj file | annotate | diff | comparison | revisions
eric7/DataViews/PyCoverageDialog.py file | annotate | diff | comparison | revisions
eric7/DataViews/PyCoverageHtmlReportDialog.py file | annotate | diff | comparison | revisions
eric7/DataViews/PyCoverageHtmlReportDialog.ui file | annotate | diff | comparison | revisions
eric7/DataViews/PyCoverageJsonReportDialog.py file | annotate | diff | comparison | revisions
eric7/DataViews/PyCoverageJsonReportDialog.ui file | annotate | diff | comparison | revisions
eric7/UI/UserInterface.py file | annotate | diff | comparison | revisions
--- 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)
         

eric ide

mercurial