Plugins/UiExtensionPlugins/PipInterface/PipSearchDialog.py

changeset 6805
10fefa6f331a
parent 6797
d9e56b0aa7ac
parent 6804
dc20bde4af90
child 6806
d306647cb82d
equal deleted inserted replaced
6797:d9e56b0aa7ac 6805:10fefa6f331a
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to search PyPI.
8 """
9
10 from __future__ import unicode_literals
11
12 import textwrap
13
14 from PyQt5.QtCore import pyqtSlot, Qt, QEventLoop, QRegExp
15 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \
16 QApplication, QTreeWidgetItem, QHeaderView, QInputDialog
17
18 from E5Gui import E5MessageBox
19 try:
20 from E5Network.E5XmlRpcClient import E5XmlRpcClient
21 except ImportError:
22 from .E5XmlRpcClient import E5XmlRpcClient
23
24 from .Ui_PipSearchDialog import Ui_PipSearchDialog
25
26
27 class PipSearchDialog(QDialog, Ui_PipSearchDialog):
28 """
29 Class implementing a dialog to search PyPI.
30 """
31 VersionRole = Qt.UserRole + 1
32
33 Stopwords = {
34 "a", "and", "are", "as", "at", "be", "but", "by",
35 "for", "if", "in", "into", "is", "it",
36 "no", "not", "of", "on", "or", "such",
37 "that", "the", "their", "then", "there", "these",
38 "they", "this", "to", "was", "will",
39 }
40
41 def __init__(self, pip, indexUrl, parent=None):
42 """
43 Constructor
44
45 @param pip reference to the master object
46 @type Pip
47 @param indexUrl URL of XML RPC interface to the pypi index
48 @type str
49 @param parent reference to the parent widget
50 @type QWidget
51 """
52 super(PipSearchDialog, self).__init__(parent)
53 self.setupUi(self)
54 self.setWindowFlags(Qt.Window)
55
56 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
57
58 self.__installButton = self.buttonBox.addButton(
59 self.tr("&Install"), QDialogButtonBox.ActionRole)
60 self.__installButton.setEnabled(False)
61
62 self.__installUserButton = self.buttonBox.addButton(
63 self.tr("Install to &User-Site"), QDialogButtonBox.ActionRole)
64 self.__installUserButton.setEnabled(False)
65
66 self.__showDetailsButton = self.buttonBox.addButton(
67 self.tr("&Show Details..."), QDialogButtonBox.ActionRole)
68 self.__showDetailsButton.setEnabled(False)
69
70 self.__pip = pip
71 self.__client = E5XmlRpcClient(indexUrl, self)
72
73 self.venvComboBox.addItem(self.__pip.getDefaultEnvironmentString())
74 projectVenv = self.__pip.getProjectEnvironmentString()
75 if projectVenv:
76 self.venvComboBox.addItem(projectVenv)
77 self.venvComboBox.addItems(self.__pip.getVirtualenvNames())
78
79 self.searchEdit.setFocus(Qt.OtherFocusReason)
80
81 self.__canceled = False
82 self.__detailsData = {}
83 self.__query = []
84
85 self.__packageDetailsDialog = None
86
87 def closeEvent(self, e):
88 """
89 Protected slot implementing a close event handler.
90
91 @param e close event
92 @type QCloseEvent
93 """
94 QApplication.restoreOverrideCursor()
95
96 if self.__packageDetailsDialog is not None:
97 self.__packageDetailsDialog.close()
98
99 e.accept()
100
101 @pyqtSlot(str)
102 def on_searchEdit_textChanged(self, txt):
103 """
104 Private slot handling a change of the search term.
105
106 @param txt search term
107 @type str
108 """
109 self.searchButton.setEnabled(bool(txt))
110
111 @pyqtSlot()
112 def on_searchButton_clicked(self):
113 """
114 Private slot handling a press of the search button.
115 """
116 self.__search()
117
118 @pyqtSlot()
119 def on_resultList_itemSelectionChanged(self):
120 """
121 Private slot handling changes of the selection.
122 """
123 self.__installButton.setEnabled(
124 len(self.resultList.selectedItems()) > 0)
125 self.__installUserButton.setEnabled(
126 len(self.resultList.selectedItems()) > 0)
127 self.__showDetailsButton.setEnabled(
128 len(self.resultList.selectedItems()) == 1)
129
130 @pyqtSlot(QAbstractButton)
131 def on_buttonBox_clicked(self, button):
132 """
133 Private slot called by a button of the button box clicked.
134
135 @param button button that was clicked
136 @type QAbstractButton
137 """
138 if button == self.buttonBox.button(QDialogButtonBox.Close):
139 self.close()
140 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
141 self.__client.abort()
142 self.__canceled = True
143 elif button == self.__installButton:
144 self.__install()
145 elif button == self.__installUserButton:
146 self.__install(userSite=True)
147 elif button == self.__showDetailsButton:
148 self.__showDetails()
149
150 def __search(self):
151 """
152 Private method to perform the search.
153 """
154 self.resultList.clear()
155 self.infoLabel.clear()
156
157 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
158 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
159 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
160 self.searchButton.setEnabled(False)
161 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
162
163 QApplication.setOverrideCursor(Qt.WaitCursor)
164 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
165
166 self.__canceled = False
167
168 self.__query = [term for term in self.searchEdit.text().strip().split()
169 if term not in PipSearchDialog.Stopwords]
170 self.__client.call(
171 "search",
172 ({"name": self.__query, "summary": self.__query}, "or"),
173 self.__processSearchResult,
174 self.__searchError
175 )
176
177 def __processSearchResult(self, data):
178 """
179 Private method to process the search result data from PyPI.
180
181 @param data result data with hits in the first element
182 @type tuple
183 """
184 if data:
185 packages = self.__transformHits(data[0])
186 if packages:
187 self.infoLabel.setText(self.tr("%n package(s) found.", "",
188 len(packages)))
189 wrapper = textwrap.TextWrapper(width=80)
190 count = 0
191 total = 0
192 for package in packages:
193 if self.__canceled:
194 self.infoLabel.setText(
195 self.tr("Canceled - only {0} out of %n package(s)"
196 " shown", "", len(packages)).format(total))
197 break
198 itm = QTreeWidgetItem(
199 self.resultList, [
200 package['name'].strip(),
201 "{0:4d}".format(package['score']),
202 "\n".join([
203 wrapper.fill(line) for line in
204 package['summary'].strip().splitlines()
205 ])
206 ])
207 itm.setData(0, self.VersionRole, package['version'])
208 count += 1
209 total += 1
210 if count == 100:
211 count = 0
212 QApplication.processEvents()
213 else:
214 QApplication.restoreOverrideCursor()
215 E5MessageBox.warning(
216 self,
217 self.tr("Search PyPI"),
218 self.tr("""<p>The package search did not return"""
219 """ anything.</p>"""))
220 self.infoLabel.setText(
221 self.tr("""<p>The package search did not return"""
222 """ anything.</p>"""))
223 else:
224 QApplication.restoreOverrideCursor()
225 E5MessageBox.warning(
226 self,
227 self.tr("Search PyPI"),
228 self.tr("""<p>The package search did not return anything."""
229 """</p>"""))
230 self.infoLabel.setText(
231 self.tr("""<p>The package search did not return anything."""
232 """</p>"""))
233
234 header = self.resultList.header()
235 self.resultList.sortItems(1, Qt.DescendingOrder)
236 header.setStretchLastSection(False)
237 header.resizeSections(QHeaderView.ResizeToContents)
238 headerSize = 0
239 for col in range(header.count()):
240 headerSize += header.sectionSize(col)
241 if headerSize < header.width():
242 header.setStretchLastSection(True)
243
244 self.__finish()
245
246 def __finish(self):
247 """
248 Private slot performing the finishing actions.
249 """
250 QApplication.restoreOverrideCursor()
251 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
252 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
253 self.searchButton.setEnabled(True)
254 self.searchButton.setDefault(True)
255 self.searchEdit.setFocus(Qt.OtherFocusReason)
256
257 def __searchError(self, errorCode, errorString):
258 """
259 Private method handling a search error.
260
261 @param errorCode code of the error
262 @type int
263 @param errorString error message
264 @type str
265 """
266 self.__finish()
267 E5MessageBox.warning(
268 self,
269 self.tr("Search PyPI"),
270 self.tr("""<p>The package search failed.</p><p>Reason: {0}</p>""")
271 .format(errorString))
272 self.infoLabel.setText(self.tr("Error: {0}").format(errorString))
273
274 def __transformHits(self, hits):
275 """
276 Private method to convert the list returned from pypi into a
277 packages list.
278
279 @param hits list returned from pypi
280 @type list of dict
281 @return list of packages
282 @rtype list of dict
283 """
284 # we only include the record with the highest score
285 packages = {}
286 for hit in hits:
287 name = hit['name'].strip()
288 summary = (hit['summary'] or "").strip()
289 version = hit['version'].strip()
290 score = self.__score(name, summary)
291 # cleanup the summary
292 if summary in ["UNKNOWN", "."]:
293 summary = ""
294
295 if name not in packages:
296 packages[name] = {
297 'name': name,
298 'summary': summary,
299 'version': [version.strip()],
300 'score': score}
301 else:
302 if score > packages[name]['score']:
303 packages[name]['score'] = score
304 packages[name]['summary'] = summary
305 packages[name]['version'].append(version.strip())
306
307 return list(packages.values())
308
309 def __score(self, name, summary):
310 """
311 Private method to calculate some score for a search result.
312
313 @param name name of the returned package
314 @type str
315 @param summary summary text for the package
316 @type str
317 @return score value
318 @rtype int
319 """
320 score = 0
321 for queryTerm in self.__query:
322 if queryTerm.lower() in name.lower():
323 score += 4
324 if queryTerm.lower() == name.lower():
325 score += 4
326
327 if queryTerm.lower() in summary.lower():
328 if QRegExp(r'\b{0}\b'.format(QRegExp.escape(queryTerm)),
329 Qt.CaseInsensitive).indexIn(summary) != -1:
330 # word match gets even higher score
331 score += 2
332 else:
333 score += 1
334
335 return score
336
337 def __install(self, userSite=False):
338 """
339 Private slot to install the selected packages.
340
341 @param userSite flag indicating to install to the user directory
342 @type bool
343 """
344 venvName = self.venvComboBox.currentText()
345
346 packages = []
347 for itm in self.resultList.selectedItems():
348 packages.append(itm.text(0).strip())
349 if packages:
350 self.__pip.installPackages(packages, venvName=venvName,
351 userSite=userSite)
352
353 def __showDetails(self):
354 """
355 Private slot to show details about the selected package.
356 """
357 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
358 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
359 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
360 self.__showDetailsButton.setEnabled(False)
361 QApplication.setOverrideCursor(Qt.WaitCursor)
362 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
363
364 self.__detailsData = {}
365
366 itm = self.resultList.selectedItems()[0]
367 packageVersions = itm.data(0, self.VersionRole)
368 if len(packageVersions) == 1:
369 packageVersion = packageVersions[0]
370 elif len(packageVersions) == 0:
371 packageVersion = ""
372 else:
373 packageVersion, ok = QInputDialog.getItem(
374 self,
375 self.tr("Show Package Details"),
376 self.tr("Select the package version:"),
377 packageVersions,
378 0, False)
379 if not ok:
380 return
381
382 packageName = itm.text(0)
383 self.__client.call(
384 "release_data",
385 (packageName, packageVersion),
386 lambda d: self.__getPackageDownloadsData(packageVersion, d),
387 self.__detailsError
388 )
389
390 def __getPackageDownloadsData(self, packageVersion, data):
391 """
392 Private method to store the details data and get downloads
393 information.
394
395 @param packageVersion version info
396 @type str
397 @param data result data with package details in the first
398 element
399 @type tuple
400 """
401 if data and data[0]:
402 self.__detailsData = data[0]
403 itm = self.resultList.selectedItems()[0]
404 packageName = itm.text(0)
405 self.__client.call(
406 "release_urls",
407 (packageName, packageVersion),
408 self.__displayPackageDetails,
409 self.__detailsError
410 )
411 else:
412 self.__finish()
413 E5MessageBox.warning(
414 self,
415 self.tr("Search PyPI"),
416 self.tr("""<p>No package details info available.</p>"""))
417
418 def __displayPackageDetails(self, data):
419 """
420 Private method to display the returned package details.
421
422 @param data result data with downloads information in the first element
423 @type tuple
424 """
425 from .PipPackageDetailsDialog import PipPackageDetailsDialog
426
427 self.__finish()
428 self.__showDetailsButton.setEnabled(True)
429
430 if self.__packageDetailsDialog is not None:
431 self.__packageDetailsDialog.close()
432
433 self.__packageDetailsDialog = \
434 PipPackageDetailsDialog(self.__detailsData, data[0], self)
435 self.__packageDetailsDialog.show()
436
437 def __detailsError(self, errorCode, errorString):
438 """
439 Private method handling a details error.
440
441 @param errorCode code of the error
442 @type int
443 @param errorString error message
444 @type str
445 """
446 self.__finish()
447 self.__showDetailsButton.setEnabled(True)
448 E5MessageBox.warning(
449 self,
450 self.tr("Search PyPI"),
451 self.tr("""<p>Package details info could not be retrieved.</p>"""
452 """<p>Reason: {0}</p>""")
453 .format(errorString))
454
455 @pyqtSlot(QTreeWidgetItem, int)
456 def on_resultList_itemActivated(self, item, column):
457 """
458 Private slot reacting on an item activation.
459
460 @param item reference to the activated item
461 @type QTreeWidgetItem
462 @param column activated column
463 @type int
464 """
465 self.__showDetails()

eric ide

mercurial