PipInterface/PipPackagesWidget.py

branch
pypi
changeset 6793
cca6a35f3ad2
parent 6792
9dd854f05c83
child 6795
6e2ed2aac325
equal deleted inserted replaced
6792:9dd854f05c83 6793:cca6a35f3ad2
7 Module implementing the pip packages management widget. 7 Module implementing the pip packages management widget.
8 """ 8 """
9 9
10 from __future__ import unicode_literals 10 from __future__ import unicode_literals
11 11
12 from PyQt5.QtCore import pyqtSlot, Qt 12 import textwrap
13
14 from PyQt5.QtCore import pyqtSlot, Qt, QEventLoop, QRegExp
13 from PyQt5.QtGui import QCursor 15 from PyQt5.QtGui import QCursor
14 from PyQt5.QtWidgets import QWidget, QToolButton, QApplication, QHeaderView, \ 16 from PyQt5.QtWidgets import QWidget, QToolButton, QApplication, QHeaderView, \
15 QTreeWidgetItem 17 QTreeWidgetItem
16 18
17 from E5Gui.E5Application import e5App 19 from E5Gui.E5Application import e5App
20 from E5Gui import E5MessageBox
21
22 from E5Network.E5XmlRpcClient import E5XmlRpcClient
18 23
19 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget 24 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget
20 25
21 import UI.PixmapCache 26 import UI.PixmapCache
22 27
29 """ 34 """
30 ShowProcessGeneralMode = 0 35 ShowProcessGeneralMode = 0
31 ShowProcessClassifiersMode = 1 36 ShowProcessClassifiersMode = 1
32 ShowProcessEntryPointsMode = 2 37 ShowProcessEntryPointsMode = 2
33 ShowProcessFilesListMode = 3 38 ShowProcessFilesListMode = 3
39
40 SearchStopwords = {
41 "a", "and", "are", "as", "at", "be", "but", "by",
42 "for", "if", "in", "into", "is", "it",
43 "no", "not", "of", "on", "or", "such",
44 "that", "the", "their", "then", "there", "these",
45 "they", "this", "to", "was", "will",
46 }
47 SearchVersionRole = Qt.UserRole + 1
34 48
35 def __init__(self, parent=None): 49 def __init__(self, parent=None):
36 """ 50 """
37 Constructor 51 Constructor
38 52
53 self.pipMenuButton.setShowMenuInside(True) 67 self.pipMenuButton.setShowMenuInside(True)
54 68
55 self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find")) 69 self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find"))
56 70
57 self.__pip = Pip(self) 71 self.__pip = Pip(self)
72 self.__client = E5XmlRpcClient(self.__pip.getIndexUrlXml(), self)
58 73
59 self.packagesList.header().setSortIndicator(0, Qt.AscendingOrder) 74 self.packagesList.header().setSortIndicator(0, Qt.AscendingOrder)
60 75
61 self.__infoLabels = { 76 self.__infoLabels = {
62 "name": self.tr("Name:"), 77 "name": self.tr("Name:"),
103 projectVenv = self.__pip.getProjectEnvironmentString() 118 projectVenv = self.__pip.getProjectEnvironmentString()
104 if projectVenv: 119 if projectVenv:
105 self.environmentsComboBox.addItem(projectVenv) 120 self.environmentsComboBox.addItem(projectVenv)
106 self.environmentsComboBox.addItems(self.__pip.getVirtualenvNames()) 121 self.environmentsComboBox.addItems(self.__pip.getVirtualenvNames())
107 122
123 def __isPipAvailable(self):
124 """
125 Private method to check, if the pip package is available for the
126 selected environment.
127
128 @return flag indicating availability
129 @rtype bool
130 """
131 available = False
132
133 venvName = self.environmentsComboBox.currentText()
134 if venvName:
135 available = len(self.packagesList.findItems(
136 "pip", Qt.MatchExactly | Qt.MatchCaseSensitive)) == 1
137
138 return available
139
108 ####################################################################### 140 #######################################################################
109 ## Slots handling widget signals below 141 ## Slots handling widget signals below
110 ####################################################################### 142 #######################################################################
111 143
112 def __selectedUpdateableItems(self): 144 def __selectedUpdateableItems(self):
138 170
139 def __updateActionButtons(self): 171 def __updateActionButtons(self):
140 """ 172 """
141 Private method to set the state of the action buttons. 173 Private method to set the state of the action buttons.
142 """ 174 """
143 self.upgradeButton.setEnabled( 175 if self.__isPipAvailable():
144 bool(self.__selectedUpdateableItems())) 176 self.upgradeButton.setEnabled(
145 self.uninstallButton.setEnabled( 177 bool(self.__selectedUpdateableItems()))
146 bool(self.packagesList.selectedItems())) 178 self.uninstallButton.setEnabled(
147 self.upgradeAllButton.setEnabled( 179 bool(self.packagesList.selectedItems()))
148 bool(self.__allUpdateableItems())) 180 self.upgradeAllButton.setEnabled(
181 bool(self.__allUpdateableItems()))
182 else:
183 self.upgradeButton.setEnabled(False)
184 self.uninstallButton.setEnabled(False)
185 self.upgradeAllButton.setEnabled(False)
149 186
150 def __refreshPackagesList(self): 187 def __refreshPackagesList(self):
151 """ 188 """
152 Private method to referesh the packages list. 189 Private method to referesh the packages list.
153 """ 190 """
199 QApplication.restoreOverrideCursor() 236 QApplication.restoreOverrideCursor()
200 self.statusLabel.hide() 237 self.statusLabel.hide()
201 238
202 self.__updateActionButtons() 239 self.__updateActionButtons()
203 self.__updateSearchActionButtons() 240 self.__updateSearchActionButtons()
241 self.__updateSearchButton()
204 242
205 @pyqtSlot(int) 243 @pyqtSlot(int)
206 def on_environmentsComboBox_currentIndexChanged(self, index): 244 def on_environmentsComboBox_currentIndexChanged(self, index):
207 """ 245 """
208 Private slot handling the selection of a conda environment. 246 Private slot handling the selection of a conda environment.
407 445
408 def __updateSearchActionButtons(self): 446 def __updateSearchActionButtons(self):
409 """ 447 """
410 Private method to update the action button states of the search widget. 448 Private method to update the action button states of the search widget.
411 """ 449 """
412 # TODO: adjust this like search dialog 450 installEnable = (
413 enable = len(self.searchResultList.selectedItems()) == 1 451 len(self.searchResultList.selectedItems()) > 0 and
414 self.installButton.setEnabled( 452 self.environmentsComboBox.currentIndex() > 0 and
415 enable and self.environmentsComboBox.currentIndex() > 0) 453 self.__isPipAvailable()
454 )
455 self.installButton.setEnabled(installEnable)
456 self.installUserSiteButton.setEnabled(installEnable)
457
416 self.showDetailsButton.setEnabled( 458 self.showDetailsButton.setEnabled(
417 enable and bool(self.searchResultList.selectedItems()[0].parent())) 459 len(self.searchResultList.selectedItems()) == 1 and
460 self.__isPipAvailable()
461 )
462
463 def __updateSearchButton(self):
464 """
465 Private method to update the state of the search button.
466 """
467 self.searchButton.setEnabled(
468 bool(self.searchEdit.text()) and
469 self.__isPipAvailable()
470 )
418 471
419 @pyqtSlot(bool) 472 @pyqtSlot(bool)
420 def on_searchToggleButton_toggled(self, checked): 473 def on_searchToggleButton_toggled(self, checked):
421 """ 474 """
422 Private slot to togle the search widget. 475 Private slot to togle the search widget.
429 if checked: 482 if checked:
430 self.searchEdit.setFocus(Qt.OtherFocusReason) 483 self.searchEdit.setFocus(Qt.OtherFocusReason)
431 self.searchEdit.selectAll() 484 self.searchEdit.selectAll()
432 485
433 self.__updateSearchActionButtons() 486 self.__updateSearchActionButtons()
487 self.__updateSearchButton()
488
489 @pyqtSlot(str)
490 def on_searchEdit_textChanged(self, txt):
491 """
492 Private slot handling a change of the search term.
493
494 @param txt search term
495 @type str
496 """
497 self.__updateSearchButton()
498
499 @pyqtSlot()
500 def on_searchButton_clicked(self):
501 """
502 Private slot handling a press of the search button.
503 """
504 self.__search()
505
506 @pyqtSlot()
507 def on_searchResultList_itemSelectionChanged(self):
508 """
509 Private slot handling changes of the search result selection.
510 """
511 self.__updateSearchActionButtons()
512
513 def __search(self):
514 """
515 Private method to perform the search.
516 """
517 self.searchResultList.clear()
518 self.searchInfoLabel.clear()
519
520 self.searchButton.setEnabled(False)
521 QApplication.setOverrideCursor(Qt.WaitCursor)
522 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
523
524 self.__query = [term for term in self.searchEdit.text().strip().split()
525 if term not in self.SearchStopwords]
526 self.__client.call(
527 "search",
528 ({"name": self.__query, "summary": self.__query}, "or"),
529 self.__processSearchResult,
530 self.__searchError
531 )
532
533 def __processSearchResult(self, data):
534 """
535 Private method to process the search result data from PyPI.
536
537 @param data result data with hits in the first element
538 @type tuple
539 """
540 if data:
541 packages = self.__transformHits(data[0])
542 if packages:
543 self.searchInfoLabel.setText(
544 self.tr("%n package(s) found.", "", len(packages)))
545 wrapper = textwrap.TextWrapper(width=80)
546 count = 0
547 total = 0
548 for package in packages:
549 itm = QTreeWidgetItem(
550 self.searchResultList, [
551 package['name'].strip(),
552 "{0:4d}".format(package['score']),
553 "\n".join([
554 wrapper.fill(line) for line in
555 package['summary'].strip().splitlines()
556 ])
557 ])
558 itm.setData(0, self.SearchVersionRole, package['version'])
559 count += 1
560 total += 1
561 if count == 100:
562 count = 0
563 QApplication.processEvents()
564 else:
565 QApplication.restoreOverrideCursor()
566 E5MessageBox.warning(
567 self,
568 self.tr("Search PyPI"),
569 self.tr("""<p>The package search did not return"""
570 """ anything.</p>"""))
571 self.searchInfoLabel.setText(
572 self.tr("""<p>The package search did not return"""
573 """ anything.</p>"""))
574 else:
575 QApplication.restoreOverrideCursor()
576 E5MessageBox.warning(
577 self,
578 self.tr("Search PyPI"),
579 self.tr("""<p>The package search did not return anything."""
580 """</p>"""))
581 self.searchInfoLabel.setText(
582 self.tr("""<p>The package search did not return anything."""
583 """</p>"""))
584
585 header = self.searchResultList.header()
586 self.searchResultList.sortItems(1, Qt.DescendingOrder)
587 header.setStretchLastSection(False)
588 header.resizeSections(QHeaderView.ResizeToContents)
589 headerSize = 0
590 for col in range(header.count()):
591 headerSize += header.sectionSize(col)
592 if headerSize < header.width():
593 header.setStretchLastSection(True)
594
595 self.__finishSearch()
596
597 def __finishSearch(self):
598 """
599 Private slot performing the search finishing actions.
600 """
601 QApplication.restoreOverrideCursor()
602
603 self.__updateSearchActionButtons()
604 self.__updateSearchButton()
605
606 self.searchEdit.setFocus(Qt.OtherFocusReason)
607
608 def __searchError(self, errorCode, errorString):
609 """
610 Private method handling a search error.
611
612 @param errorCode code of the error
613 @type int
614 @param errorString error message
615 @type str
616 """
617 self.__finish()
618 E5MessageBox.warning(
619 self,
620 self.tr("Search PyPI"),
621 self.tr("""<p>The package search failed.</p><p>Reason: {0}</p>""")
622 .format(errorString))
623 self.searchInfoLabel.setText(self.tr("Error: {0}").format(errorString))
624
625 def __transformHits(self, hits):
626 """
627 Private method to convert the list returned from pypi into a
628 packages list.
629
630 @param hits list returned from pypi
631 @type list of dict
632 @return list of packages
633 @rtype list of dict
634 """
635 # we only include the record with the highest score
636 packages = {}
637 for hit in hits:
638 name = hit['name'].strip()
639 summary = (hit['summary'] or "").strip()
640 version = hit['version'].strip()
641 score = self.__score(name, summary)
642 # cleanup the summary
643 if summary in ["UNKNOWN", "."]:
644 summary = ""
645
646 if name not in packages:
647 packages[name] = {
648 'name': name,
649 'summary': summary,
650 'version': [version.strip()],
651 'score': score}
652 else:
653 if score > packages[name]['score']:
654 packages[name]['score'] = score
655 packages[name]['summary'] = summary
656 packages[name]['version'].append(version.strip())
657
658 return list(packages.values())
659
660 def __score(self, name, summary):
661 """
662 Private method to calculate some score for a search result.
663
664 @param name name of the returned package
665 @type str
666 @param summary summary text for the package
667 @type str
668 @return score value
669 @rtype int
670 """
671 score = 0
672 for queryTerm in self.__query:
673 if queryTerm.lower() in name.lower():
674 score += 4
675 if queryTerm.lower() == name.lower():
676 score += 4
677
678 if queryTerm.lower() in summary.lower():
679 if QRegExp(r'\b{0}\b'.format(QRegExp.escape(queryTerm)),
680 Qt.CaseInsensitive).indexIn(summary) != -1:
681 # word match gets even higher score
682 score += 2
683 else:
684 score += 1
685
686 return score
434 687
435 ####################################################################### 688 #######################################################################
436 ## Menu related methods below 689 ## Menu related methods below
437 ####################################################################### 690 #######################################################################
438 691

eric ide

mercurial