Plugins/UiExtensionPlugins/PipInterface/PipSearchDialog.py

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

eric ide

mercurial