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")) |
|
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 self.cleanupButton.setIcon(EricPixmapCache.getIcon("clear")) |
96 self.cleanupButton.setIcon(EricPixmapCache.getIcon("clear")) |
218 |
97 |
219 self.refreshDependenciesButton.setIcon(EricPixmapCache.getIcon("reload")) |
98 self.refreshDependenciesButton.setIcon(EricPixmapCache.getIcon("reload")) |
220 self.showDepPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
99 self.showDepPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info")) |
221 self.dependencyRepairButton.setIcon(EricPixmapCache.getIcon("repair")) |
100 self.dependencyRepairButton.setIcon(EricPixmapCache.getIcon("repair")) |
862 EricMessageBox.warning( |
733 EricMessageBox.warning( |
863 self, |
734 self, |
864 self.tr("Cleanup Environment"), |
735 self.tr("Cleanup Environment"), |
865 self.tr( |
736 self.tr( |
866 "Some leftover package directories could not been removed." |
737 "Some leftover package directories could not been removed." |
867 " Delete them manually."), |
738 " Delete them manually." |
868 ) |
|
869 |
|
870 ####################################################################### |
|
871 ## Search widget related methods below |
|
872 ####################################################################### |
|
873 |
|
874 def __updateSearchActionButtons(self): |
|
875 """ |
|
876 Private method to update the action button states of the search widget. |
|
877 """ |
|
878 installEnable = ( |
|
879 len(self.searchResultList.selectedItems()) > 0 |
|
880 and self.environmentsComboBox.currentIndex() > 0 |
|
881 and self.__isPipAvailable() |
|
882 ) |
|
883 self.installButton.setEnabled(installEnable) |
|
884 self.installUserSiteButton.setEnabled(installEnable) |
|
885 |
|
886 self.showDetailsButton.setEnabled( |
|
887 len(self.searchResultList.selectedItems()) == 1 and self.__isPipAvailable() |
|
888 ) |
|
889 |
|
890 def __updateSearchButton(self): |
|
891 """ |
|
892 Private method to update the state of the search button. |
|
893 """ |
|
894 self.searchButton.setEnabled( |
|
895 bool(self.searchNameEdit.text()) and self.__isPipAvailable() |
|
896 ) |
|
897 |
|
898 def __updateSearchMoreButton(self, enable): |
|
899 """ |
|
900 Private method to update the state of the search more button. |
|
901 |
|
902 @param enable flag indicating the desired enable state |
|
903 @type bool |
|
904 """ |
|
905 self.searchMoreButton.setEnabled( |
|
906 enable and bool(self.searchNameEdit.text()) and self.__isPipAvailable() |
|
907 ) |
|
908 |
|
909 @pyqtSlot(bool) |
|
910 def on_searchToggleButton_1_toggled(self, checked): |
|
911 """ |
|
912 Private slot to toggle the search widget. |
|
913 |
|
914 @param checked state of the search widget button |
|
915 @type bool |
|
916 """ |
|
917 self.searchWidget.setVisible(checked) |
|
918 self.searchToggleButton_2.setChecked(checked) |
|
919 |
|
920 if checked: |
|
921 self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
|
922 self.searchNameEdit.selectAll() |
|
923 |
|
924 self.__updateSearchActionButtons() |
|
925 self.__updateSearchButton() |
|
926 self.__updateSearchMoreButton(False) |
|
927 |
|
928 @pyqtSlot(bool) |
|
929 def on_searchToggleButton_2_toggled(self, checked): |
|
930 """ |
|
931 Private slot to toggle the search widget. |
|
932 |
|
933 @param checked state of the search widget button |
|
934 @type bool |
|
935 """ |
|
936 self.searchToggleButton_1.setChecked(checked) |
|
937 |
|
938 @pyqtSlot(str) |
|
939 def on_searchNameEdit_textChanged(self, _txt): |
|
940 """ |
|
941 Private slot handling a change of the search term. |
|
942 |
|
943 @param _txt search term (unused) |
|
944 @type str |
|
945 """ |
|
946 self.__updateSearchButton() |
|
947 |
|
948 @pyqtSlot() |
|
949 def on_searchNameEdit_returnPressed(self): |
|
950 """ |
|
951 Private slot initiating a search via a press of the Return key. |
|
952 """ |
|
953 if bool(self.searchNameEdit.text()) and self.__isPipAvailable(): |
|
954 self.__searchFirst() |
|
955 |
|
956 @pyqtSlot() |
|
957 def on_searchButton_clicked(self): |
|
958 """ |
|
959 Private slot handling a press of the search button. |
|
960 """ |
|
961 self.__searchFirst() |
|
962 |
|
963 @pyqtSlot() |
|
964 def on_searchMoreButton_clicked(self): |
|
965 """ |
|
966 Private slot handling a press of the search more button. |
|
967 """ |
|
968 self.__search(self.__lastSearchPage + 1) |
|
969 |
|
970 @pyqtSlot() |
|
971 def on_searchResultList_itemSelectionChanged(self): |
|
972 """ |
|
973 Private slot handling changes of the search result selection. |
|
974 """ |
|
975 self.__updateSearchActionButtons() |
|
976 |
|
977 def __searchFirst(self): |
|
978 """ |
|
979 Private method to perform the search for packages. |
|
980 """ |
|
981 self.searchResultList.clear() |
|
982 self.searchInfoLabel.clear() |
|
983 |
|
984 self.__updateSearchMoreButton(False) |
|
985 |
|
986 self.__search() |
|
987 |
|
988 def __search(self, page=1): |
|
989 """ |
|
990 Private method to perform the search by calling the PyPI search URL. |
|
991 |
|
992 @param page search page to retrieve (defaults to 1) |
|
993 @type int (optional) |
|
994 """ |
|
995 self.__lastSearchPage = page |
|
996 |
|
997 self.searchButton.setEnabled(False) |
|
998 |
|
999 searchTerm = self.searchNameEdit.text().strip() |
|
1000 searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode() |
|
1001 urlQuery = QUrlQuery() |
|
1002 urlQuery.addQueryItem("q", searchTerm) |
|
1003 urlQuery.addQueryItem("page", str(page)) |
|
1004 url = QUrl(self.__pip.getIndexUrlSearch()) |
|
1005 url.setQuery(urlQuery) |
|
1006 |
|
1007 request = QNetworkRequest(QUrl(url)) |
|
1008 request.setAttribute( |
|
1009 QNetworkRequest.Attribute.CacheLoadControlAttribute, |
|
1010 QNetworkRequest.CacheLoadControl.AlwaysNetwork, |
|
1011 ) |
|
1012 reply = self.__pip.getNetworkAccessManager().get(request) |
|
1013 reply.finished.connect(lambda: self.__searchResponse(reply)) |
|
1014 self.__replies.append(reply) |
|
1015 |
|
1016 def __searchResponse(self, reply): |
|
1017 """ |
|
1018 Private method to extract the search result data from the response. |
|
1019 |
|
1020 @param reply reference to the reply object containing the data |
|
1021 @type QNetworkReply |
|
1022 """ |
|
1023 if reply in self.__replies: |
|
1024 self.__replies.remove(reply) |
|
1025 |
|
1026 urlQuery = QUrlQuery(reply.url()) |
|
1027 searchTerm = urlQuery.queryItemValue("q") |
|
1028 |
|
1029 if reply.error() != QNetworkReply.NetworkError.NoError: |
|
1030 EricMessageBox.warning( |
|
1031 None, |
|
1032 self.tr("Search PyPI"), |
|
1033 self.tr( |
|
1034 "<p>Received an error while searching for <b>{0}</b>.</p>" |
|
1035 "<p>Error: {1}</p>" |
|
1036 ).format(searchTerm, reply.errorString()), |
|
1037 ) |
|
1038 reply.deleteLater() |
|
1039 return |
|
1040 |
|
1041 data = bytes(reply.readAll()).decode() |
|
1042 reply.deleteLater() |
|
1043 |
|
1044 results = PypiSearchResultsParser(data).getResults() |
|
1045 if results: |
|
1046 # PyPI returns max. 20 entries per page |
|
1047 if len(results) < 20: |
|
1048 msg = self.tr( |
|
1049 "%n package(s) found.", |
|
1050 "", |
|
1051 (self.__lastSearchPage - 1) * 20 + len(results), |
|
1052 ) |
|
1053 self.__updateSearchMoreButton(False) |
|
1054 else: |
|
1055 msg = self.tr("Showing first {0} packages found.").format( |
|
1056 self.__lastSearchPage * 20 |
|
1057 ) |
|
1058 self.__updateSearchMoreButton(True) |
|
1059 self.searchInfoLabel.setText(msg) |
|
1060 lastItem = self.searchResultList.topLevelItem( |
|
1061 self.searchResultList.topLevelItemCount() - 1 |
|
1062 ) |
|
1063 else: |
|
1064 self.__updateSearchMoreButton(False) |
|
1065 if self.__lastSearchPage == 1: |
|
1066 EricMessageBox.warning( |
|
1067 self, |
|
1068 self.tr("Search PyPI"), |
|
1069 self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format( |
|
1070 searchTerm |
|
1071 ), |
739 ), |
1072 ) |
740 ) |
1073 self.searchInfoLabel.setText( |
741 |
1074 self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format( |
742 @pyqtSlot() |
1075 searchTerm |
743 def on_searchButton_clicked(self): |
1076 ) |
744 """ |
1077 ) |
745 Private slot to open a web browser for package searching. |
1078 else: |
746 """ |
1079 EricMessageBox.warning( |
747 url = QUrl(self.__pip.getIndexUrlSearch()) |
1080 self, |
748 |
1081 self.tr("Search PyPI"), |
749 searchTerm = self.searchEdit.text().strip() |
1082 self.tr( |
750 if searchTerm: |
1083 """<p>There were no more results for <b>{0}</b>.</p>""" |
751 searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode() |
1084 ).format(searchTerm), |
752 urlQuery = QUrlQuery() |
1085 ) |
753 urlQuery.addQueryItem("q", searchTerm) |
1086 lastItem = None |
754 url.setQuery(urlQuery) |
1087 |
755 |
1088 wrapper = textwrap.TextWrapper(width=80) |
756 QDesktopServices.openUrl(url) |
1089 for result in results: |
757 |
1090 try: |
758 @pyqtSlot() |
1091 description = "\n".join( |
759 def on_searchEdit_returnPressed(self): |
1092 [ |
760 """ |
1093 wrapper.fill(line) |
761 Private slot to handle the press of the Return key in the search line edit. |
1094 for line in result["description"].strip().splitlines() |
762 """ |
1095 ] |
763 self.on_searchButton_clicked() |
1096 ) |
|
1097 except KeyError: |
|
1098 description = "" |
|
1099 date = result["released"] if "released" in result else result["created"] |
|
1100 itm = QTreeWidgetItem( |
|
1101 self.searchResultList, |
|
1102 [ |
|
1103 result["name"].strip(), |
|
1104 result["version"], |
|
1105 date.strip(), |
|
1106 description, |
|
1107 ], |
|
1108 ) |
|
1109 itm.setData(0, self.SearchVersionRole, result["version"]) |
|
1110 |
|
1111 if lastItem: |
|
1112 self.searchResultList.scrollToItem( |
|
1113 lastItem, QAbstractItemView.ScrollHint.PositionAtTop |
|
1114 ) |
|
1115 |
|
1116 header = self.searchResultList.header() |
|
1117 header.setStretchLastSection(False) |
|
1118 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) |
|
1119 headerSize = 0 |
|
1120 for col in range(header.count()): |
|
1121 headerSize += header.sectionSize(col) |
|
1122 if headerSize < header.width(): |
|
1123 header.setStretchLastSection(True) |
|
1124 |
|
1125 self.__finishSearch() |
|
1126 |
|
1127 def __finishSearch(self): |
|
1128 """ |
|
1129 Private slot performing the search finishing actions. |
|
1130 """ |
|
1131 self.__updateSearchActionButtons() |
|
1132 self.__updateSearchButton() |
|
1133 |
|
1134 self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
|
1135 |
|
1136 @pyqtSlot() |
|
1137 def on_installButton_clicked(self): |
|
1138 """ |
|
1139 Private slot to handle pressing the Install button.. |
|
1140 """ |
|
1141 packages = [ |
|
1142 itm.text(0).strip() for itm in self.searchResultList.selectedItems() |
|
1143 ] |
|
1144 self.executeInstallPackages(packages) |
|
1145 |
|
1146 @pyqtSlot() |
|
1147 def on_installUserSiteButton_clicked(self): |
|
1148 """ |
|
1149 Private slot to handle pressing the Install to User-Site button.. |
|
1150 """ |
|
1151 packages = [ |
|
1152 itm.text(0).strip() for itm in self.searchResultList.selectedItems() |
|
1153 ] |
|
1154 self.executeInstallPackages(packages, userSite=True) |
|
1155 |
764 |
1156 def executeInstallPackages(self, packages, userSite=False): |
765 def executeInstallPackages(self, packages, userSite=False): |
1157 """ |
766 """ |
1158 Public method to install the given list of packages. |
767 Public method to install the given list of packages. |
1159 |
768 |