src/eric7/PipInterface/PipPackagesWidget.py

branch
eric7-maintenance
changeset 11118
967a88a16a21
parent 11063
bb05d1db9286
parent 11113
2e03383143e3
child 11155
e1843b6efa73
diff -r c2cb561a39b0 -r 967a88a16a21 src/eric7/PipInterface/PipPackagesWidget.py
--- a/src/eric7/PipInterface/PipPackagesWidget.py	Sat Nov 30 11:09:02 2024 +0100
+++ b/src/eric7/PipInterface/PipPackagesWidget.py	Tue Jan 14 17:29:56 2025 +0100
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2019 - 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+# Copyright (c) 2019 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
 #
 
 """
@@ -9,16 +9,12 @@
 
 import contextlib
 import enum
-import html.parser
 import os
-import textwrap
 
 from packaging.specifiers import InvalidSpecifier, SpecifierSet
 from PyQt6.QtCore import Qt, QUrl, QUrlQuery, pyqtSlot
-from PyQt6.QtGui import QIcon
-from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
+from PyQt6.QtGui import QDesktopServices, QIcon
 from PyQt6.QtWidgets import (
-    QAbstractItemView,
     QDialog,
     QHeaderView,
     QMenu,
@@ -37,118 +33,6 @@
 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget
 
 
-class PypiSearchResultsParser(html.parser.HTMLParser):
-    """
-    Class implementing the parser for the PyPI search result page.
-    """
-
-    ClassPrefix = "package-snippet__"
-
-    def __init__(self, data):
-        """
-        Constructor
-
-        @param data data to be parsed
-        @type str
-        """
-        super().__init__()
-        self.__results = []
-        self.__activeClass = None
-        self.feed(data)
-
-    def __getClass(self, attrs):
-        """
-        Private method to extract the class attribute out of the list of
-        attributes.
-
-        @param attrs list of tag attributes as (name, value) tuples
-        @type list of tuple of (str, str)
-        @return value of the 'class' attribute or None
-        @rtype str
-        """
-        for name, value in attrs:
-            if name == "class":
-                return value
-
-        return None
-
-    def __getDate(self, attrs):
-        """
-        Private method to extract the datetime attribute out of the list of
-        attributes and process it.
-
-        @param attrs list of tag attributes as (name, value) tuples
-        @type list of tuple of (str, str)
-        @return value of the 'class' attribute or None
-        @rtype str
-        """
-        for name, value in attrs:
-            if name == "datetime":
-                return value.split("T")[0]
-
-        return None
-
-    def handle_starttag(self, tag, attrs):
-        """
-        Public method to process the start tag.
-
-        @param tag tag name (all lowercase)
-        @type str
-        @param attrs list of tag attributes as (name, value) tuples
-        @type list of tuple of (str, str)
-        """
-        if tag == "a" and self.__getClass(attrs) == "package-snippet":
-            self.__results.append({})
-
-        if tag in ("span", "p"):
-            tagClass = self.__getClass(attrs)
-            if tagClass in (
-                "package-snippet__name",
-                "package-snippet__description",
-                "package-snippet__version",
-                "package-snippet__released",
-                "package-snippet__created",
-            ):
-                self.__activeClass = tagClass
-            else:
-                self.__activeClass = None
-        elif tag == "time":
-            attributeName = self.__activeClass.replace(self.ClassPrefix, "")
-            self.__results[-1][attributeName] = self.__getDate(attrs)
-            self.__activeClass = None
-        else:
-            self.__activeClass = None
-
-    def handle_data(self, data):
-        """
-        Public method process arbitrary data.
-
-        @param data data to be processed
-        @type str
-        """
-        if self.__activeClass is not None:
-            attributeName = self.__activeClass.replace(self.ClassPrefix, "")
-            self.__results[-1][attributeName] = data
-
-    def handle_endtag(self, _tag):
-        """
-        Public method to process the end tag.
-
-        @param _tag tag name (all lowercase) (unused)
-        @type str
-        """
-        self.__activeClass = None
-
-    def getResults(self):
-        """
-        Public method to get the extracted search results.
-
-        @return extracted result data
-        @rtype list of dict
-        """
-        return self.__results
-
-
 class PipPackageInformationMode(enum.Enum):
     """
     Class defining the show information process modes.
@@ -203,17 +87,13 @@
         self.pipMenuButton.setShowMenuInside(True)
 
         self.refreshButton.setIcon(EricPixmapCache.getIcon("reload"))
+        self.installButton.setIcon(EricPixmapCache.getIcon("plus"))
         self.upgradeButton.setIcon(EricPixmapCache.getIcon("1uparrow"))
         self.upgradeAllButton.setIcon(EricPixmapCache.getIcon("2uparrow"))
         self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus"))
         self.showPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info"))
-        self.searchToggleButton_1.setIcon(EricPixmapCache.getIcon("find"))
-        self.searchToggleButton_2.setIcon(EricPixmapCache.getIcon("find"))
-        self.searchButton.setIcon(EricPixmapCache.getIcon("findNext"))
-        self.searchMoreButton.setIcon(EricPixmapCache.getIcon("plus"))
-        self.installButton.setIcon(EricPixmapCache.getIcon("plus"))
-        self.installUserSiteButton.setIcon(EricPixmapCache.getIcon("addUser"))
-        self.showDetailsButton.setIcon(EricPixmapCache.getIcon("info"))
+        self.searchButton.setIcon(EricPixmapCache.getIcon("find"))
+        self.cleanupButton.setIcon(EricPixmapCache.getIcon("clear"))
 
         self.refreshDependenciesButton.setIcon(EricPixmapCache.getIcon("reload"))
         self.showDepPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info"))
@@ -262,13 +142,14 @@
 
         self.__packageDetailsDialog = None
 
+        self.installButton.clicked.connect(self.__installPackages)
+
         self.__initPipMenu()
         self.__populateEnvironments()
         self.__updateActionButtons()
         self.__updateDepActionButtons()
 
         self.statusLabel.hide()
-        self.searchWidget.hide()
         self.__lastSearchPage = 0
 
         self.__queryName = []
@@ -425,17 +306,21 @@
         Private method to set the state of the action buttons.
         """
         if self.__isPipAvailable():
+            self.installButton.setEnabled(True)
             self.upgradeButton.setEnabled(bool(self.__selectedUpdateableItems()))
             self.uninstallButton.setEnabled(bool(self.packagesList.selectedItems()))
             self.upgradeAllButton.setEnabled(bool(self.__allUpdateableItems()))
             self.showPackageDetailsButton.setEnabled(
                 len(self.packagesList.selectedItems()) == 1
             )
+            self.cleanupButton.setEnabled(True)
         else:
+            self.installButton.setEnabled(False)
             self.upgradeButton.setEnabled(False)
             self.uninstallButton.setEnabled(False)
             self.upgradeAllButton.setEnabled(False)
             self.showPackageDetailsButton.setEnabled(False)
+            self.cleanupButton.setEnabled(False)
 
     def __refreshPackagesList(self):
         """
@@ -488,9 +373,6 @@
 
         else:
             self.__updateActionButtons()
-            self.__updateSearchActionButtons()
-            self.__updateSearchButton()
-            self.__updateSearchMoreButton(False)
 
     def __updateOutdatedInfo(self, outdatedPackages):
         """
@@ -512,9 +394,6 @@
         )
 
         self.__updateActionButtons()
-        self.__updateSearchActionButtons()
-        self.__updateSearchButton()
-        self.__updateSearchMoreButton(False)
 
         self.statusLabel.hide()
 
@@ -531,12 +410,8 @@
                 self.environmentPathLabel.setPath(
                     self.__pip.getVirtualenvInterpreter(name)
                 )
-                self.searchNameEdit.setEnabled(True)
             else:
                 self.environmentPathLabel.setPath("")
-                self.searchNameEdit.clear()
-                self.searchNameEdit.setEnabled(False)
-                self.searchResultList.clear()
                 if self.__packageDetailsDialog is not None:
                     self.__packageDetailsDialog.close()
 
@@ -546,6 +421,8 @@
                 self.__refreshPackagesList()
             self.__selectedEnvironment = name
 
+            self.__updateActionButtons()
+
     @pyqtSlot()
     def on_localCheckBox_clicked(self):
         """
@@ -837,291 +714,53 @@
                 upgradable=upgradable,
             )
 
-    #######################################################################
-    ## Search widget related methods below
-    #######################################################################
-
-    def __updateSearchActionButtons(self):
-        """
-        Private method to update the action button states of the search widget.
+    @pyqtSlot()
+    def on_cleanupButton_clicked(self):
         """
-        installEnable = (
-            len(self.searchResultList.selectedItems()) > 0
-            and self.environmentsComboBox.currentIndex() > 0
-            and self.__isPipAvailable()
-        )
-        self.installButton.setEnabled(installEnable)
-        self.installUserSiteButton.setEnabled(installEnable)
-
-        self.showDetailsButton.setEnabled(
-            len(self.searchResultList.selectedItems()) == 1 and self.__isPipAvailable()
-        )
-
-    def __updateSearchButton(self):
-        """
-        Private method to update the state of the search button.
-        """
-        self.searchButton.setEnabled(
-            bool(self.searchNameEdit.text()) and self.__isPipAvailable()
-        )
-
-    def __updateSearchMoreButton(self, enable):
-        """
-        Private method to update the state of the search more button.
-
-        @param enable flag indicating the desired enable state
-        @type bool
-        """
-        self.searchMoreButton.setEnabled(
-            enable and bool(self.searchNameEdit.text()) and self.__isPipAvailable()
-        )
-
-    @pyqtSlot(bool)
-    def on_searchToggleButton_1_toggled(self, checked):
+        Private slot to cleanup the site-packages directory of the selected
+        environment.
         """
-        Private slot to toggle the search widget.
-
-        @param checked state of the search widget button
-        @type bool
-        """
-        self.searchWidget.setVisible(checked)
-        self.searchToggleButton_2.setChecked(checked)
-
-        if checked:
-            self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason)
-            self.searchNameEdit.selectAll()
-
-            self.__updateSearchActionButtons()
-            self.__updateSearchButton()
-            self.__updateSearchMoreButton(False)
-
-    @pyqtSlot(bool)
-    def on_searchToggleButton_2_toggled(self, checked):
-        """
-        Private slot to toggle the search widget.
-
-        @param checked state of the search widget button
-        @type bool
-        """
-        self.searchToggleButton_1.setChecked(checked)
-
-    @pyqtSlot(str)
-    def on_searchNameEdit_textChanged(self, _txt):
-        """
-        Private slot handling a change of the search term.
-
-        @param _txt search term (unused)
-        @type str
-        """
-        self.__updateSearchButton()
-
-    @pyqtSlot()
-    def on_searchNameEdit_returnPressed(self):
-        """
-        Private slot initiating a search via a press of the Return key.
-        """
-        if bool(self.searchNameEdit.text()) and self.__isPipAvailable():
-            self.__searchFirst()
+        envName = self.environmentsComboBox.currentText()
+        if envName:
+            ok = self.__pip.runCleanup(envName=envName)
+            if ok:
+                EricMessageBox.information(
+                    self,
+                    self.tr("Cleanup Environment"),
+                    self.tr("The environment cleanup was successful."),
+                )
+            else:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("Cleanup Environment"),
+                    self.tr(
+                        "Some leftover package directories could not been removed."
+                        " Delete them manually."
+                    ),
+                )
 
     @pyqtSlot()
     def on_searchButton_clicked(self):
         """
-        Private slot handling a press of the search button.
+        Private slot to open a web browser for package searching.
         """
-        self.__searchFirst()
+        url = QUrl(self.__pip.getIndexUrlSearch())
 
-    @pyqtSlot()
-    def on_searchMoreButton_clicked(self):
-        """
-        Private slot handling a press of the search more button.
-        """
-        self.__search(self.__lastSearchPage + 1)
+        searchTerm = self.searchEdit.text().strip()
+        if searchTerm:
+            searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode()
+            urlQuery = QUrlQuery()
+            urlQuery.addQueryItem("q", searchTerm)
+            url.setQuery(urlQuery)
+
+        QDesktopServices.openUrl(url)
 
     @pyqtSlot()
-    def on_searchResultList_itemSelectionChanged(self):
-        """
-        Private slot handling changes of the search result selection.
-        """
-        self.__updateSearchActionButtons()
-
-    def __searchFirst(self):
-        """
-        Private method to perform the search for packages.
-        """
-        self.searchResultList.clear()
-        self.searchInfoLabel.clear()
-
-        self.__updateSearchMoreButton(False)
-
-        self.__search()
-
-    def __search(self, page=1):
-        """
-        Private method to perform the search by calling the PyPI search URL.
-
-        @param page search page to retrieve (defaults to 1)
-        @type int (optional)
-        """
-        self.__lastSearchPage = page
-
-        self.searchButton.setEnabled(False)
-
-        searchTerm = self.searchNameEdit.text().strip()
-        searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode()
-        urlQuery = QUrlQuery()
-        urlQuery.addQueryItem("q", searchTerm)
-        urlQuery.addQueryItem("page", str(page))
-        url = QUrl(self.__pip.getIndexUrlSearch())
-        url.setQuery(urlQuery)
-
-        request = QNetworkRequest(QUrl(url))
-        request.setAttribute(
-            QNetworkRequest.Attribute.CacheLoadControlAttribute,
-            QNetworkRequest.CacheLoadControl.AlwaysNetwork,
-        )
-        reply = self.__pip.getNetworkAccessManager().get(request)
-        reply.finished.connect(lambda: self.__searchResponse(reply))
-        self.__replies.append(reply)
-
-    def __searchResponse(self, reply):
-        """
-        Private method to extract the search result data from the response.
-
-        @param reply reference to the reply object containing the data
-        @type QNetworkReply
+    def on_searchEdit_returnPressed(self):
         """
-        if reply in self.__replies:
-            self.__replies.remove(reply)
-
-        urlQuery = QUrlQuery(reply.url())
-        searchTerm = urlQuery.queryItemValue("q")
-
-        if reply.error() != QNetworkReply.NetworkError.NoError:
-            EricMessageBox.warning(
-                None,
-                self.tr("Search PyPI"),
-                self.tr(
-                    "<p>Received an error while searching for <b>{0}</b>.</p>"
-                    "<p>Error: {1}</p>"
-                ).format(searchTerm, reply.errorString()),
-            )
-            reply.deleteLater()
-            return
-
-        data = bytes(reply.readAll()).decode()
-        reply.deleteLater()
-
-        results = PypiSearchResultsParser(data).getResults()
-        if results:
-            # PyPI returns max. 20 entries per page
-            if len(results) < 20:
-                msg = self.tr(
-                    "%n package(s) found.",
-                    "",
-                    (self.__lastSearchPage - 1) * 20 + len(results),
-                )
-                self.__updateSearchMoreButton(False)
-            else:
-                msg = self.tr("Showing first {0} packages found.").format(
-                    self.__lastSearchPage * 20
-                )
-                self.__updateSearchMoreButton(True)
-            self.searchInfoLabel.setText(msg)
-            lastItem = self.searchResultList.topLevelItem(
-                self.searchResultList.topLevelItemCount() - 1
-            )
-        else:
-            self.__updateSearchMoreButton(False)
-            if self.__lastSearchPage == 1:
-                EricMessageBox.warning(
-                    self,
-                    self.tr("Search PyPI"),
-                    self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format(
-                        searchTerm
-                    ),
-                )
-                self.searchInfoLabel.setText(
-                    self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format(
-                        searchTerm
-                    )
-                )
-            else:
-                EricMessageBox.warning(
-                    self,
-                    self.tr("Search PyPI"),
-                    self.tr(
-                        """<p>There were no more results for <b>{0}</b>.</p>"""
-                    ).format(searchTerm),
-                )
-            lastItem = None
-
-        wrapper = textwrap.TextWrapper(width=80)
-        for result in results:
-            try:
-                description = "\n".join(
-                    [
-                        wrapper.fill(line)
-                        for line in result["description"].strip().splitlines()
-                    ]
-                )
-            except KeyError:
-                description = ""
-            date = result["released"] if "released" in result else result["created"]
-            itm = QTreeWidgetItem(
-                self.searchResultList,
-                [
-                    result["name"].strip(),
-                    result["version"],
-                    date.strip(),
-                    description,
-                ],
-            )
-            itm.setData(0, self.SearchVersionRole, result["version"])
-
-        if lastItem:
-            self.searchResultList.scrollToItem(
-                lastItem, QAbstractItemView.ScrollHint.PositionAtTop
-            )
-
-        header = self.searchResultList.header()
-        header.setStretchLastSection(False)
-        header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
-        headerSize = 0
-        for col in range(header.count()):
-            headerSize += header.sectionSize(col)
-        if headerSize < header.width():
-            header.setStretchLastSection(True)
-
-        self.__finishSearch()
-
-    def __finishSearch(self):
+        Private slot to handle the press of the Return key in the search line edit.
         """
-        Private slot performing the search finishing actions.
-        """
-        self.__updateSearchActionButtons()
-        self.__updateSearchButton()
-
-        self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason)
-
-    @pyqtSlot()
-    def on_installButton_clicked(self):
-        """
-        Private slot to handle pressing the Install button..
-        """
-        packages = [
-            itm.text(0).strip() for itm in self.searchResultList.selectedItems()
-        ]
-        self.executeInstallPackages(packages)
-
-    @pyqtSlot()
-    def on_installUserSiteButton_clicked(self):
-        """
-        Private slot to handle pressing the Install to User-Site button..
-        """
-        packages = [
-            itm.text(0).strip() for itm in self.searchResultList.selectedItems()
-        ]
-        self.executeInstallPackages(packages, userSite=True)
+        self.on_searchButton_clicked()
 
     def executeInstallPackages(self, packages, userSite=False):
         """
@@ -1137,42 +776,6 @@
             self.__pip.installPackages(packages, venvName=venvName, userSite=userSite)
             self.on_refreshButton_clicked()
 
-    @pyqtSlot()
-    def on_showDetailsButton_clicked(self):
-        """
-        Private slot to handle pressing the Show Details button.
-        """
-        self.__showSearchedDetails()
-
-    @pyqtSlot(QTreeWidgetItem, int)
-    def on_searchResultList_itemActivated(self, item, column):
-        """
-        Private slot reacting on an search result item activation.
-
-        @param item reference to the activated item
-        @type QTreeWidgetItem
-        @param column activated column
-        @type int
-        """
-        self.__showSearchedDetails(item)
-
-    def __showSearchedDetails(self, item=None):
-        """
-        Private slot to show details about the selected search result package.
-
-        @param item reference to the search result item to show details for
-        @type QTreeWidgetItem
-        """
-        self.showDetailsButton.setEnabled(False)
-
-        if not item:
-            item = self.searchResultList.selectedItems()[0]
-
-        packageVersion = item.data(0, self.SearchVersionRole)
-        packageName = item.text(0)
-
-        self.__showPackageDetails(packageName, packageVersion, installable=True)
-
     def __showPackageDetails(
         self,
         packageName,
@@ -1203,8 +806,6 @@
             packageData = self.__pip.getPackageDetails(packageName, packageVersion)
 
         if packageData:
-            self.showDetailsButton.setEnabled(True)
-
             if installable:
                 buttonsMode = PipPackageDetailsDialog.ButtonInstall
             elif upgradable:

eric ide

mercurial