|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show details about a package. |
|
8 """ |
|
9 |
|
10 from PyQt6.QtCore import pyqtSlot, Qt, QLocale |
|
11 from PyQt6.QtWidgets import ( |
|
12 QDialog, QDialogButtonBox, QTreeWidgetItem, QLabel, QHeaderView, |
|
13 QAbstractButton |
|
14 ) |
|
15 |
|
16 from .Ui_PipPackageDetailsDialog import Ui_PipPackageDetailsDialog |
|
17 |
|
18 |
|
19 class PipPackageDetailsDialog(QDialog, Ui_PipPackageDetailsDialog): |
|
20 """ |
|
21 Class implementing a dialog to show details about a package. |
|
22 """ |
|
23 ButtonInstall = 1 |
|
24 ButtonRemove = 2 |
|
25 ButtonUpgrade = 4 |
|
26 |
|
27 def __init__(self, detailsData, buttonsMode=0, parent=None): |
|
28 """ |
|
29 Constructor |
|
30 |
|
31 @param detailsData package details |
|
32 @type dict |
|
33 @param buttonsMode flags telling which convenience buttons to enable |
|
34 (defaults to 0) |
|
35 @type int (optional) |
|
36 @param parent reference to the parent widget (defaults to None) |
|
37 @type QWidget (optional) |
|
38 """ |
|
39 super().__init__(parent) |
|
40 self.setupUi(self) |
|
41 self.setWindowFlags(Qt.WindowType.Window) |
|
42 |
|
43 self.__pipWidget = parent |
|
44 |
|
45 self.__installButton = self.buttonBox.addButton( |
|
46 self.tr("Install"), QDialogButtonBox.ButtonRole.ActionRole) |
|
47 self.__removeButton = self.buttonBox.addButton( |
|
48 self.tr("Uninstall"), QDialogButtonBox.ButtonRole.ActionRole) |
|
49 self.__upgradeButton = self.buttonBox.addButton( |
|
50 self.tr("Upgrade"), QDialogButtonBox.ButtonRole.ActionRole) |
|
51 |
|
52 self.__locale = QLocale() |
|
53 self.__packageTypeMap = { |
|
54 "sdist": self.tr("Source"), |
|
55 "bdist_wheel": self.tr("Python Wheel"), |
|
56 "bdist_egg": self.tr("Python Egg"), |
|
57 "bdist_wininst": self.tr("MS Windows Installer"), |
|
58 "bdist_msi": self.tr("MS Windows Installer"), |
|
59 "bdist_rpm": self.tr("Unix Installer"), |
|
60 "bdist_deb": self.tr("Unix Installer"), |
|
61 "bdist_dumb": self.tr("Archive"), |
|
62 } |
|
63 self.__packageName = detailsData["info"]["name"] |
|
64 |
|
65 self.__populateDetails(detailsData["info"]) |
|
66 self.__populateDownloadUrls(detailsData["urls"]) |
|
67 self.__populateRequiresProvides(detailsData["info"]) |
|
68 |
|
69 self.__installButton.setEnabled(buttonsMode & self.ButtonInstall) |
|
70 self.__removeButton.setEnabled(buttonsMode & self.ButtonRemove) |
|
71 self.__upgradeButton.setEnabled(buttonsMode & self.ButtonUpgrade) |
|
72 |
|
73 def __populateDetails(self, detailsData): |
|
74 """ |
|
75 Private method to populate the details tab. |
|
76 |
|
77 @param detailsData package details |
|
78 @type dict |
|
79 """ |
|
80 self.packageNameLabel.setText( |
|
81 "<h1>{0} {1}</h1".format(self.__sanitize(detailsData["name"]), |
|
82 self.__sanitize(detailsData["version"]))) |
|
83 self.summaryLabel.setText( |
|
84 self.__sanitize(detailsData["summary"][:240])) |
|
85 self.descriptionEdit.setPlainText( |
|
86 self.__sanitize(detailsData["description"])) |
|
87 self.authorLabel.setText(self.__sanitize(detailsData["author"])) |
|
88 self.authorEmailLabel.setText( |
|
89 '<a href="mailto:{0}">{0}</a>'.format( |
|
90 self.__sanitize(detailsData["author_email"]))) |
|
91 self.licenseLabel.setText(self.__sanitize(detailsData["license"])) |
|
92 self.platformLabel.setText(self.__sanitize(detailsData["platform"])) |
|
93 self.homePageLabel.setText( |
|
94 '<a href="{0}">{0}</a>'.format( |
|
95 self.__sanitize(detailsData["home_page"], forUrl=True))) |
|
96 self.packageUrlLabel.setText( |
|
97 '<a href="{0}">{0}</a>'.format( |
|
98 self.__sanitize(detailsData["package_url"], forUrl=True))) |
|
99 self.releaseUrlLabel.setText( |
|
100 '<a href="{0}">{0}</a>'.format( |
|
101 self.__sanitize(detailsData["release_url"], forUrl=True))) |
|
102 self.docsUrlLabel.setText( |
|
103 '<a href="{0}">{0}</a>'.format( |
|
104 self.__sanitize(detailsData["docs_url"], forUrl=True))) |
|
105 self.classifiersList.addItems(detailsData["classifiers"]) |
|
106 |
|
107 self.buttonBox.button( |
|
108 QDialogButtonBox.StandardButton.Close).setDefault(True) |
|
109 self.buttonBox.button( |
|
110 QDialogButtonBox.StandardButton.Close).setFocus( |
|
111 Qt.FocusReason.OtherFocusReason) |
|
112 |
|
113 def __populateDownloadUrls(self, downloadsData): |
|
114 """ |
|
115 Private method to populate the download URLs tab. |
|
116 |
|
117 @param downloadsData downloads information |
|
118 @type dict |
|
119 """ |
|
120 index = self.infoWidget.indexOf(self.urls) |
|
121 if downloadsData: |
|
122 self.infoWidget.setTabEnabled(index, True) |
|
123 for download in downloadsData: |
|
124 itm = QTreeWidgetItem(self.downloadUrlsList, [ |
|
125 "", |
|
126 self.__packageTypeMap[download["packagetype"]] |
|
127 if download["packagetype"] in self.__packageTypeMap |
|
128 else "", |
|
129 download["python_version"] |
|
130 if download["python_version"] != "source" |
|
131 else "", |
|
132 self.__formatUploadDate(download["upload_time"]), |
|
133 self.__formatSize(download["size"]), |
|
134 ]) |
|
135 pgpLink = ( |
|
136 ' (<a href="{0}">pgp</a>)'.format(download["url"] + ".asc") |
|
137 if download["has_sig"] else |
|
138 "" |
|
139 ) |
|
140 urlLabel = QLabel('<a href="{0}#md5={2}">{1}</a>{3}'.format( |
|
141 download["url"], download["filename"], |
|
142 download["md5_digest"], pgpLink)) |
|
143 urlLabel.setTextInteractionFlags( |
|
144 Qt.TextInteractionFlag.LinksAccessibleByMouse) |
|
145 urlLabel.setOpenExternalLinks(True) |
|
146 self.downloadUrlsList.setItemWidget(itm, 0, urlLabel) |
|
147 header = self.downloadUrlsList.header() |
|
148 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) |
|
149 else: |
|
150 self.infoWidget.setTabEnabled(index, False) |
|
151 |
|
152 def __populateRequiresProvides(self, detailsData): |
|
153 """ |
|
154 Private method to populate the requires/provides tab. |
|
155 |
|
156 @param detailsData package details |
|
157 @type dict |
|
158 """ |
|
159 populatedItems = 0 |
|
160 |
|
161 if "requires" in detailsData and detailsData["requires"]: |
|
162 self.requiredPackagesList.addItems(detailsData["requires"]) |
|
163 populatedItems += len(detailsData["requires"]) |
|
164 if "requires_dist" in detailsData and detailsData["requires_dist"]: |
|
165 self.requiredDistributionsList.addItems( |
|
166 detailsData["requires_dist"]) |
|
167 populatedItems += len(detailsData["requires_dist"]) |
|
168 if "provides" in detailsData and detailsData["provides"]: |
|
169 self.providedPackagesList.addItems(detailsData["provides"]) |
|
170 populatedItems += len(detailsData["provides"]) |
|
171 if "provides_dist" in detailsData and detailsData["provides_dist"]: |
|
172 self.providedDistributionsList.addItems( |
|
173 detailsData["provides_dist"]) |
|
174 populatedItems += len(detailsData["provides_dist"]) |
|
175 |
|
176 index = self.infoWidget.indexOf(self.requires) |
|
177 self.infoWidget.setTabEnabled(index, populatedItems > 0) |
|
178 |
|
179 def __sanitize(self, text, forUrl=False): |
|
180 """ |
|
181 Private method to clean-up the given text. |
|
182 |
|
183 @param text raw text |
|
184 @type str |
|
185 @param forUrl flag indicating to sanitize an URL text |
|
186 @type bool |
|
187 @return processed text |
|
188 @rtype str |
|
189 """ |
|
190 if text == "UNKNOWN" or text is None: |
|
191 text = "" |
|
192 elif text == "any": |
|
193 text = self.tr("any") |
|
194 if forUrl and ( |
|
195 not isinstance(text, str) or |
|
196 not text.startswith(("http://", "https://", "ftp://")) |
|
197 ): |
|
198 # ignore if the schema is not one of the listed ones |
|
199 text = "" |
|
200 |
|
201 return text |
|
202 |
|
203 def __formatUploadDate(self, datetime): |
|
204 """ |
|
205 Private method to format the upload date. |
|
206 |
|
207 @param datetime upload date and time |
|
208 @type xmlrpc.DateTime or str |
|
209 @return formatted date string |
|
210 @rtype str |
|
211 """ |
|
212 if isinstance(datetime, str): |
|
213 return datetime.split("T")[0] |
|
214 else: |
|
215 date = datetime.value.split("T")[0] |
|
216 return "{0}-{1}-{2}".format(date[:4], date[4:6], date[6:]) |
|
217 |
|
218 def __formatSize(self, size): |
|
219 """ |
|
220 Private slot to format the size. |
|
221 |
|
222 @param size size to be formatted |
|
223 @type int |
|
224 @return formatted size |
|
225 @rtype str |
|
226 """ |
|
227 unit = "" |
|
228 if size < 1024: |
|
229 unit = self.tr("B") |
|
230 elif size < 1024 * 1024: |
|
231 size /= 1024 |
|
232 unit = self.tr("KB") |
|
233 elif size < 1024 * 1024 * 1024: |
|
234 size /= 1024 * 1024 |
|
235 unit = self.tr("MB") |
|
236 else: |
|
237 size /= 1024 * 1024 * 1024 |
|
238 unit = self.tr("GB") |
|
239 return self.tr("{0:.1f} {1}", "value, unit").format(size, unit) |
|
240 |
|
241 @pyqtSlot(QAbstractButton) |
|
242 def on_buttonBox_clicked(self, button): |
|
243 """ |
|
244 Private slot handling the user pressing an action button. |
|
245 |
|
246 @param button button activated by the user |
|
247 @type QAbstractButton |
|
248 """ |
|
249 if button is self.__installButton: |
|
250 self.__pipWidget.executeInstallPackages([self.__packageName]) |
|
251 self.__installButton.setEnabled(False) |
|
252 self.__removeButton.setEnabled(True) |
|
253 self.__upgradeButton.setEnabled(False) |
|
254 self.raise_() |
|
255 elif button is self.__removeButton: |
|
256 self.__pipWidget.executeUninstallPackages([self.__packageName]) |
|
257 self.__installButton.setEnabled(True) |
|
258 self.__removeButton.setEnabled(False) |
|
259 self.__upgradeButton.setEnabled(False) |
|
260 self.raise_() |
|
261 elif button is self.__upgradeButton: |
|
262 self.__pipWidget.executeUpgradePackages([self.__packageName]) |
|
263 self.__installButton.setEnabled(False) |
|
264 self.__removeButton.setEnabled(True) |
|
265 self.__upgradeButton.setEnabled(False) |
|
266 self.raise_() |