35 |
31 |
36 from .PipVulnerabilityChecker import Package, VulnerabilityCheckError |
32 from .PipVulnerabilityChecker import Package, VulnerabilityCheckError |
37 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget |
33 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget |
38 |
34 |
39 |
35 |
40 class PypiSearchResultsParser(html.parser.HTMLParser): |
|
41 """ |
|
42 Class implementing the parser for the PyPI search result page. |
|
43 """ |
|
44 |
|
45 ClassPrefix = "package-snippet__" |
|
46 |
|
47 def __init__(self, data): |
|
48 """ |
|
49 Constructor |
|
50 |
|
51 @param data data to be parsed |
|
52 @type str |
|
53 """ |
|
54 super().__init__() |
|
55 self.__results = [] |
|
56 self.__activeClass = None |
|
57 self.feed(data) |
|
58 |
|
59 def __getClass(self, attrs): |
|
60 """ |
|
61 Private method to extract the class attribute out of the list of |
|
62 attributes. |
|
63 |
|
64 @param attrs list of tag attributes as (name, value) tuples |
|
65 @type list of tuple of (str, str) |
|
66 @return value of the 'class' attribute or None |
|
67 @rtype str |
|
68 """ |
|
69 for name, value in attrs: |
|
70 if name == "class": |
|
71 return value |
|
72 |
|
73 return None |
|
74 |
|
75 def __getDate(self, attrs): |
|
76 """ |
|
77 Private method to extract the datetime attribute out of the list of |
|
78 attributes and process it. |
|
79 |
|
80 @param attrs list of tag attributes as (name, value) tuples |
|
81 @type list of tuple of (str, str) |
|
82 @return value of the 'class' attribute or None |
|
83 @rtype str |
|
84 """ |
|
85 for name, value in attrs: |
|
86 if name == "datetime": |
|
87 return value.split("T")[0] |
|
88 |
|
89 return None |
|
90 |
|
91 def handle_starttag(self, tag, attrs): |
|
92 """ |
|
93 Public method to process the start tag. |
|
94 |
|
95 @param tag tag name (all lowercase) |
|
96 @type str |
|
97 @param attrs list of tag attributes as (name, value) tuples |
|
98 @type list of tuple of (str, str) |
|
99 """ |
|
100 if tag == "a" and self.__getClass(attrs) == "package-snippet": |
|
101 self.__results.append({}) |
|
102 |
|
103 if tag in ("span", "p"): |
|
104 tagClass = self.__getClass(attrs) |
|
105 if tagClass in ( |
|
106 "package-snippet__name", |
|
107 "package-snippet__description", |
|
108 "package-snippet__version", |
|
109 "package-snippet__released", |
|
110 "package-snippet__created", |
|
111 ): |
|
112 self.__activeClass = tagClass |
|
113 else: |
|
114 self.__activeClass = None |
|
115 elif tag == "time": |
|
116 attributeName = self.__activeClass.replace(self.ClassPrefix, "") |
|
117 self.__results[-1][attributeName] = self.__getDate(attrs) |
|
118 self.__activeClass = None |
|
119 else: |
|
120 self.__activeClass = None |
|
121 |
|
122 def handle_data(self, data): |
|
123 """ |
|
124 Public method process arbitrary data. |
|
125 |
|
126 @param data data to be processed |
|
127 @type str |
|
128 """ |
|
129 if self.__activeClass is not None: |
|
130 attributeName = self.__activeClass.replace(self.ClassPrefix, "") |
|
131 self.__results[-1][attributeName] = data |
|
132 |
|
133 def handle_endtag(self, _tag): |
|
134 """ |
|
135 Public method to process the end tag. |
|
136 |
|
137 @param _tag tag name (all lowercase) (unused) |
|
138 @type str |
|
139 """ |
|
140 self.__activeClass = None |
|
141 |
|
142 def getResults(self): |
|
143 """ |
|
144 Public method to get the extracted search results. |
|
145 |
|
146 @return extracted result data |
|
147 @rtype list of dict |
|
148 """ |
|
149 return self.__results |
|
150 |
|
151 |
|
152 class PipPackageInformationMode(enum.Enum): |
36 class PipPackageInformationMode(enum.Enum): |
153 """ |
37 """ |
154 Class defining the show information process modes. |
38 Class defining the show information process modes. |
155 """ |
39 """ |
156 |
40 |
201 self.pipMenuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) |
85 self.pipMenuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) |
202 self.pipMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) |
86 self.pipMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) |
203 self.pipMenuButton.setShowMenuInside(True) |
87 self.pipMenuButton.setShowMenuInside(True) |
204 |
88 |
205 self.refreshButton.setIcon(EricPixmapCache.getIcon("reload")) |
89 self.refreshButton.setIcon(EricPixmapCache.getIcon("reload")) |
|
90 self.installButton.setIcon(EricPixmapCache.getIcon("plus")) |
206 self.upgradeButton.setIcon(EricPixmapCache.getIcon("1uparrow")) |
91 self.upgradeButton.setIcon(EricPixmapCache.getIcon("1uparrow")) |
207 self.upgradeAllButton.setIcon(EricPixmapCache.getIcon("2uparrow")) |
92 self.upgradeAllButton.setIcon(EricPixmapCache.getIcon("2uparrow")) |
208 self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus")) |
93 self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus")) |
209 self.showPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
94 self.showPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
210 self.searchToggleButton_1.setIcon(EricPixmapCache.getIcon("find")) |
95 self.searchButton.setIcon(EricPixmapCache.getIcon("find")) |
211 self.searchToggleButton_2.setIcon(EricPixmapCache.getIcon("find")) |
96 self.cleanupButton.setIcon(EricPixmapCache.getIcon("clear")) |
212 self.searchButton.setIcon(EricPixmapCache.getIcon("findNext")) |
|
213 self.searchMoreButton.setIcon(EricPixmapCache.getIcon("plus")) |
|
214 self.installButton.setIcon(EricPixmapCache.getIcon("plus")) |
|
215 self.installUserSiteButton.setIcon(EricPixmapCache.getIcon("addUser")) |
|
216 self.showDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
|
217 |
97 |
218 self.refreshDependenciesButton.setIcon(EricPixmapCache.getIcon("reload")) |
98 self.refreshDependenciesButton.setIcon(EricPixmapCache.getIcon("reload")) |
219 self.showDepPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
99 self.showDepPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
220 self.dependencyRepairButton.setIcon(EricPixmapCache.getIcon("repair")) |
100 self.dependencyRepairButton.setIcon(EricPixmapCache.getIcon("repair")) |
221 self.dependencyRepairAllButton.setIcon(EricPixmapCache.getIcon("repairAll")) |
101 self.dependencyRepairAllButton.setIcon(EricPixmapCache.getIcon("repairAll")) |
835 packageVersion, |
712 packageVersion, |
836 vulnerabilities=vulnerabilities, |
713 vulnerabilities=vulnerabilities, |
837 upgradable=upgradable, |
714 upgradable=upgradable, |
838 ) |
715 ) |
839 |
716 |
840 ####################################################################### |
717 @pyqtSlot() |
841 ## Search widget related methods below |
718 def on_cleanupButton_clicked(self): |
842 ####################################################################### |
719 """ |
843 |
720 Private slot to cleanup the site-packages directory of the selected |
844 def __updateSearchActionButtons(self): |
721 environment. |
845 """ |
722 """ |
846 Private method to update the action button states of the search widget. |
723 envName = self.environmentsComboBox.currentText() |
847 """ |
724 if envName: |
848 installEnable = ( |
725 ok = self.__pip.runCleanup(envName=envName) |
849 len(self.searchResultList.selectedItems()) > 0 |
726 if ok: |
850 and self.environmentsComboBox.currentIndex() > 0 |
727 EricMessageBox.information( |
851 and self.__isPipAvailable() |
|
852 ) |
|
853 self.installButton.setEnabled(installEnable) |
|
854 self.installUserSiteButton.setEnabled(installEnable) |
|
855 |
|
856 self.showDetailsButton.setEnabled( |
|
857 len(self.searchResultList.selectedItems()) == 1 and self.__isPipAvailable() |
|
858 ) |
|
859 |
|
860 def __updateSearchButton(self): |
|
861 """ |
|
862 Private method to update the state of the search button. |
|
863 """ |
|
864 self.searchButton.setEnabled( |
|
865 bool(self.searchNameEdit.text()) and self.__isPipAvailable() |
|
866 ) |
|
867 |
|
868 def __updateSearchMoreButton(self, enable): |
|
869 """ |
|
870 Private method to update the state of the search more button. |
|
871 |
|
872 @param enable flag indicating the desired enable state |
|
873 @type bool |
|
874 """ |
|
875 self.searchMoreButton.setEnabled( |
|
876 enable and bool(self.searchNameEdit.text()) and self.__isPipAvailable() |
|
877 ) |
|
878 |
|
879 @pyqtSlot(bool) |
|
880 def on_searchToggleButton_1_toggled(self, checked): |
|
881 """ |
|
882 Private slot to toggle the search widget. |
|
883 |
|
884 @param checked state of the search widget button |
|
885 @type bool |
|
886 """ |
|
887 self.searchWidget.setVisible(checked) |
|
888 self.searchToggleButton_2.setChecked(checked) |
|
889 |
|
890 if checked: |
|
891 self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
|
892 self.searchNameEdit.selectAll() |
|
893 |
|
894 self.__updateSearchActionButtons() |
|
895 self.__updateSearchButton() |
|
896 self.__updateSearchMoreButton(False) |
|
897 |
|
898 @pyqtSlot(bool) |
|
899 def on_searchToggleButton_2_toggled(self, checked): |
|
900 """ |
|
901 Private slot to toggle the search widget. |
|
902 |
|
903 @param checked state of the search widget button |
|
904 @type bool |
|
905 """ |
|
906 self.searchToggleButton_1.setChecked(checked) |
|
907 |
|
908 @pyqtSlot(str) |
|
909 def on_searchNameEdit_textChanged(self, _txt): |
|
910 """ |
|
911 Private slot handling a change of the search term. |
|
912 |
|
913 @param _txt search term (unused) |
|
914 @type str |
|
915 """ |
|
916 self.__updateSearchButton() |
|
917 |
|
918 @pyqtSlot() |
|
919 def on_searchNameEdit_returnPressed(self): |
|
920 """ |
|
921 Private slot initiating a search via a press of the Return key. |
|
922 """ |
|
923 if bool(self.searchNameEdit.text()) and self.__isPipAvailable(): |
|
924 self.__searchFirst() |
|
925 |
|
926 @pyqtSlot() |
|
927 def on_searchButton_clicked(self): |
|
928 """ |
|
929 Private slot handling a press of the search button. |
|
930 """ |
|
931 self.__searchFirst() |
|
932 |
|
933 @pyqtSlot() |
|
934 def on_searchMoreButton_clicked(self): |
|
935 """ |
|
936 Private slot handling a press of the search more button. |
|
937 """ |
|
938 self.__search(self.__lastSearchPage + 1) |
|
939 |
|
940 @pyqtSlot() |
|
941 def on_searchResultList_itemSelectionChanged(self): |
|
942 """ |
|
943 Private slot handling changes of the search result selection. |
|
944 """ |
|
945 self.__updateSearchActionButtons() |
|
946 |
|
947 def __searchFirst(self): |
|
948 """ |
|
949 Private method to perform the search for packages. |
|
950 """ |
|
951 self.searchResultList.clear() |
|
952 self.searchInfoLabel.clear() |
|
953 |
|
954 self.__updateSearchMoreButton(False) |
|
955 |
|
956 self.__search() |
|
957 |
|
958 def __search(self, page=1): |
|
959 """ |
|
960 Private method to perform the search by calling the PyPI search URL. |
|
961 |
|
962 @param page search page to retrieve (defaults to 1) |
|
963 @type int (optional) |
|
964 """ |
|
965 self.__lastSearchPage = page |
|
966 |
|
967 self.searchButton.setEnabled(False) |
|
968 |
|
969 searchTerm = self.searchNameEdit.text().strip() |
|
970 searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode() |
|
971 urlQuery = QUrlQuery() |
|
972 urlQuery.addQueryItem("q", searchTerm) |
|
973 urlQuery.addQueryItem("page", str(page)) |
|
974 url = QUrl(self.__pip.getIndexUrlSearch()) |
|
975 url.setQuery(urlQuery) |
|
976 |
|
977 request = QNetworkRequest(QUrl(url)) |
|
978 request.setAttribute( |
|
979 QNetworkRequest.Attribute.CacheLoadControlAttribute, |
|
980 QNetworkRequest.CacheLoadControl.AlwaysNetwork, |
|
981 ) |
|
982 reply = self.__pip.getNetworkAccessManager().get(request) |
|
983 reply.finished.connect(lambda: self.__searchResponse(reply)) |
|
984 self.__replies.append(reply) |
|
985 |
|
986 def __searchResponse(self, reply): |
|
987 """ |
|
988 Private method to extract the search result data from the response. |
|
989 |
|
990 @param reply reference to the reply object containing the data |
|
991 @type QNetworkReply |
|
992 """ |
|
993 if reply in self.__replies: |
|
994 self.__replies.remove(reply) |
|
995 |
|
996 urlQuery = QUrlQuery(reply.url()) |
|
997 searchTerm = urlQuery.queryItemValue("q") |
|
998 |
|
999 if reply.error() != QNetworkReply.NetworkError.NoError: |
|
1000 EricMessageBox.warning( |
|
1001 None, |
|
1002 self.tr("Search PyPI"), |
|
1003 self.tr( |
|
1004 "<p>Received an error while searching for <b>{0}</b>.</p>" |
|
1005 "<p>Error: {1}</p>" |
|
1006 ).format(searchTerm, reply.errorString()), |
|
1007 ) |
|
1008 reply.deleteLater() |
|
1009 return |
|
1010 |
|
1011 data = bytes(reply.readAll()).decode() |
|
1012 reply.deleteLater() |
|
1013 |
|
1014 results = PypiSearchResultsParser(data).getResults() |
|
1015 if results: |
|
1016 # PyPI returns max. 20 entries per page |
|
1017 if len(results) < 20: |
|
1018 msg = self.tr( |
|
1019 "%n package(s) found.", |
|
1020 "", |
|
1021 (self.__lastSearchPage - 1) * 20 + len(results), |
|
1022 ) |
|
1023 self.__updateSearchMoreButton(False) |
|
1024 else: |
|
1025 msg = self.tr("Showing first {0} packages found.").format( |
|
1026 self.__lastSearchPage * 20 |
|
1027 ) |
|
1028 self.__updateSearchMoreButton(True) |
|
1029 self.searchInfoLabel.setText(msg) |
|
1030 lastItem = self.searchResultList.topLevelItem( |
|
1031 self.searchResultList.topLevelItemCount() - 1 |
|
1032 ) |
|
1033 else: |
|
1034 self.__updateSearchMoreButton(False) |
|
1035 if self.__lastSearchPage == 1: |
|
1036 EricMessageBox.warning( |
|
1037 self, |
728 self, |
1038 self.tr("Search PyPI"), |
729 self.tr("Cleanup Environment"), |
1039 self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format( |
730 self.tr("The environment cleanup was successful."), |
1040 searchTerm |
|
1041 ), |
|
1042 ) |
|
1043 self.searchInfoLabel.setText( |
|
1044 self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format( |
|
1045 searchTerm |
|
1046 ) |
|
1047 ) |
731 ) |
1048 else: |
732 else: |
1049 EricMessageBox.warning( |
733 EricMessageBox.warning( |
1050 self, |
734 self, |
1051 self.tr("Search PyPI"), |
735 self.tr("Cleanup Environment"), |
1052 self.tr( |
736 self.tr( |
1053 """<p>There were no more results for <b>{0}</b>.</p>""" |
737 "Some leftover package directories could not been removed." |
1054 ).format(searchTerm), |
738 " Delete them manually." |
|
739 ), |
1055 ) |
740 ) |
1056 lastItem = None |
741 |
1057 |
742 @pyqtSlot() |
1058 wrapper = textwrap.TextWrapper(width=80) |
743 def on_searchButton_clicked(self): |
1059 for result in results: |
744 """ |
1060 try: |
745 Private slot to open a web browser for package searching. |
1061 description = "\n".join( |
746 """ |
1062 [ |
747 url = QUrl(self.__pip.getIndexUrlSearch()) |
1063 wrapper.fill(line) |
748 |
1064 for line in result["description"].strip().splitlines() |
749 searchTerm = self.searchEdit.text().strip() |
1065 ] |
750 if searchTerm: |
1066 ) |
751 searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode() |
1067 except KeyError: |
752 urlQuery = QUrlQuery() |
1068 description = "" |
753 urlQuery.addQueryItem("q", searchTerm) |
1069 date = result["released"] if "released" in result else result["created"] |
754 url.setQuery(urlQuery) |
1070 itm = QTreeWidgetItem( |
755 |
1071 self.searchResultList, |
756 QDesktopServices.openUrl(url) |
1072 [ |
757 |
1073 result["name"].strip(), |
758 @pyqtSlot() |
1074 result["version"], |
759 def on_searchEdit_returnPressed(self): |
1075 date.strip(), |
760 """ |
1076 description, |
761 Private slot to handle the press of the Return key in the search line edit. |
1077 ], |
762 """ |
1078 ) |
763 self.on_searchButton_clicked() |
1079 itm.setData(0, self.SearchVersionRole, result["version"]) |
|
1080 |
|
1081 if lastItem: |
|
1082 self.searchResultList.scrollToItem( |
|
1083 lastItem, QAbstractItemView.ScrollHint.PositionAtTop |
|
1084 ) |
|
1085 |
|
1086 header = self.searchResultList.header() |
|
1087 header.setStretchLastSection(False) |
|
1088 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) |
|
1089 headerSize = 0 |
|
1090 for col in range(header.count()): |
|
1091 headerSize += header.sectionSize(col) |
|
1092 if headerSize < header.width(): |
|
1093 header.setStretchLastSection(True) |
|
1094 |
|
1095 self.__finishSearch() |
|
1096 |
|
1097 def __finishSearch(self): |
|
1098 """ |
|
1099 Private slot performing the search finishing actions. |
|
1100 """ |
|
1101 self.__updateSearchActionButtons() |
|
1102 self.__updateSearchButton() |
|
1103 |
|
1104 self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
|
1105 |
|
1106 @pyqtSlot() |
|
1107 def on_installButton_clicked(self): |
|
1108 """ |
|
1109 Private slot to handle pressing the Install button.. |
|
1110 """ |
|
1111 packages = [ |
|
1112 itm.text(0).strip() for itm in self.searchResultList.selectedItems() |
|
1113 ] |
|
1114 self.executeInstallPackages(packages) |
|
1115 |
|
1116 @pyqtSlot() |
|
1117 def on_installUserSiteButton_clicked(self): |
|
1118 """ |
|
1119 Private slot to handle pressing the Install to User-Site button.. |
|
1120 """ |
|
1121 packages = [ |
|
1122 itm.text(0).strip() for itm in self.searchResultList.selectedItems() |
|
1123 ] |
|
1124 self.executeInstallPackages(packages, userSite=True) |
|
1125 |
764 |
1126 def executeInstallPackages(self, packages, userSite=False): |
765 def executeInstallPackages(self, packages, userSite=False): |
1127 """ |
766 """ |
1128 Public method to install the given list of packages. |
767 Public method to install the given list of packages. |
1129 |
768 |