1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the pip packages management widget. 8""" 9 10import textwrap 11import os 12import html.parser 13import contextlib 14 15from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QUrlQuery 16from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest 17from PyQt5.QtWidgets import ( 18 QWidget, QToolButton, QApplication, QHeaderView, QTreeWidgetItem, 19 QMenu, QDialog 20) 21 22from E5Gui.E5Application import e5App 23from E5Gui import E5MessageBox 24from E5Gui.E5OverrideCursor import E5OverrideCursor 25 26from .Ui_PipPackagesWidget import Ui_PipPackagesWidget 27 28import UI.PixmapCache 29import Globals 30import Preferences 31 32 33class PypiSearchResultsParser(html.parser.HTMLParser): 34 """ 35 Class implementing the parser for the PyPI search result page. 36 """ 37 ClassPrefix = "package-snippet__" 38 39 def __init__(self, data): 40 """ 41 Constructor 42 43 @param data data to be parsed 44 @type str 45 """ 46 super().__init__() 47 self.__results = [] 48 self.__activeClass = None 49 self.feed(data) 50 51 def __getClass(self, attrs): 52 """ 53 Private method to extract the class attribute out of the list of 54 attributes. 55 56 @param attrs list of tag attributes as (name, value) tuples 57 @type list of tuple of (str, str) 58 @return value of the 'class' attribute or None 59 @rtype str 60 """ 61 for name, value in attrs: 62 if name == "class": 63 return value 64 65 return None 66 67 def __getDate(self, attrs): 68 """ 69 Private method to extract the datetime attribute out of the list of 70 attributes and process it. 71 72 @param attrs list of tag attributes as (name, value) tuples 73 @type list of tuple of (str, str) 74 @return value of the 'class' attribute or None 75 @rtype str 76 """ 77 for name, value in attrs: 78 if name == "datetime": 79 return value.split("T")[0] 80 81 return None 82 83 def handle_starttag(self, tag, attrs): 84 """ 85 Public method to process the start tag. 86 87 @param tag tag name (all lowercase) 88 @type str 89 @param attrs list of tag attributes as (name, value) tuples 90 @type list of tuple of (str, str) 91 """ 92 if tag == "a" and self.__getClass(attrs) == "package-snippet": 93 self.__results.append({}) 94 95 if tag in ("span", "p"): 96 tagClass = self.__getClass(attrs) 97 if tagClass in ( 98 "package-snippet__name", "package-snippet__description", 99 "package-snippet__version", "package-snippet__released", 100 ): 101 self.__activeClass = tagClass 102 else: 103 self.__activeClass = None 104 elif tag == "time": 105 attributeName = self.__activeClass.replace(self.ClassPrefix, "") 106 self.__results[-1][attributeName] = self.__getDate(attrs) 107 self.__activeClass = None 108 else: 109 self.__activeClass = None 110 111 def handle_data(self, data): 112 """ 113 Public method process arbitrary data. 114 115 @param data data to be processed 116 @type str 117 """ 118 if self.__activeClass is not None: 119 attributeName = self.__activeClass.replace(self.ClassPrefix, "") 120 self.__results[-1][attributeName] = data 121 122 def handle_endtag(self, tag): 123 """ 124 Public method to process the end tag. 125 126 @param tag tag name (all lowercase) 127 @type str 128 """ 129 self.__activeClass = None 130 131 def getResults(self): 132 """ 133 Public method to get the extracted search results. 134 135 @return extracted result data 136 @rtype list of dict 137 """ 138 return self.__results 139 140 141class PipPackagesWidget(QWidget, Ui_PipPackagesWidget): 142 """ 143 Class implementing the pip packages management widget. 144 """ 145 ShowProcessGeneralMode = 0 146 ShowProcessClassifiersMode = 1 147 ShowProcessEntryPointsMode = 2 148 ShowProcessFilesListMode = 3 149 150 SearchVersionRole = Qt.ItemDataRole.UserRole + 1 151 152 def __init__(self, pip, parent=None): 153 """ 154 Constructor 155 156 @param pip reference to the global pip interface 157 @type Pip 158 @param parent reference to the parent widget 159 @type QWidget 160 """ 161 super().__init__(parent) 162 self.setupUi(self) 163 164 self.pipMenuButton.setObjectName( 165 "pip_supermenu_button") 166 self.pipMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu")) 167 self.pipMenuButton.setToolTip(self.tr("pip Menu")) 168 self.pipMenuButton.setPopupMode( 169 QToolButton.ToolButtonPopupMode.InstantPopup) 170 self.pipMenuButton.setToolButtonStyle( 171 Qt.ToolButtonStyle.ToolButtonIconOnly) 172 self.pipMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) 173 self.pipMenuButton.setAutoRaise(True) 174 self.pipMenuButton.setShowMenuInside(True) 175 176 self.refreshButton.setIcon(UI.PixmapCache.getIcon("reload")) 177 self.upgradeButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) 178 self.upgradeAllButton.setIcon(UI.PixmapCache.getIcon("2uparrow")) 179 self.uninstallButton.setIcon(UI.PixmapCache.getIcon("minus")) 180 self.showPackageDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) 181 self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find")) 182 self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext")) 183 self.installButton.setIcon(UI.PixmapCache.getIcon("plus")) 184 self.installUserSiteButton.setIcon(UI.PixmapCache.getIcon("addUser")) 185 self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) 186 187 self.__pip = pip 188 189 self.packagesList.header().setSortIndicator( 190 0, Qt.SortOrder.AscendingOrder) 191 192 self.__infoLabels = { 193 "name": self.tr("Name:"), 194 "version": self.tr("Version:"), 195 "location": self.tr("Location:"), 196 "requires": self.tr("Requires:"), 197 "summary": self.tr("Summary:"), 198 "home-page": self.tr("Homepage:"), 199 "author": self.tr("Author:"), 200 "author-email": self.tr("Author Email:"), 201 "license": self.tr("License:"), 202 "metadata-version": self.tr("Metadata Version:"), 203 "installer": self.tr("Installer:"), 204 "classifiers": self.tr("Classifiers:"), 205 "entry-points": self.tr("Entry Points:"), 206 "files": self.tr("Files:"), 207 } 208 self.infoWidget.setHeaderLabels(["Key", "Value"]) 209 210 venvManager = e5App().getObject("VirtualEnvManager") 211 venvManager.virtualEnvironmentAdded.connect( 212 self.on_refreshButton_clicked) 213 venvManager.virtualEnvironmentRemoved.connect( 214 self.on_refreshButton_clicked) 215 216 project = e5App().getObject("Project") 217 project.projectOpened.connect( 218 self.on_refreshButton_clicked) 219 project.projectClosed.connect( 220 self.on_refreshButton_clicked) 221 222 self.__initPipMenu() 223 self.__populateEnvironments() 224 self.__updateActionButtons() 225 226 self.statusLabel.hide() 227 self.searchWidget.hide() 228 229 self.__queryName = [] 230 self.__querySummary = [] 231 232 self.__replies = [] 233 234 self.__packageDetailsDialog = None 235 236 def __populateEnvironments(self): 237 """ 238 Private method to get a list of environments and populate the selector. 239 """ 240 self.environmentsComboBox.addItem("") 241 projectVenv = self.__pip.getProjectEnvironmentString() 242 if projectVenv: 243 self.environmentsComboBox.addItem(projectVenv) 244 self.environmentsComboBox.addItems( 245 self.__pip.getVirtualenvNames( 246 noRemote=True, 247 noConda=Preferences.getPip("ExcludeCondaEnvironments") 248 ) 249 ) 250 251 def __isPipAvailable(self): 252 """ 253 Private method to check, if the pip package is available for the 254 selected environment. 255 256 @return flag indicating availability 257 @rtype bool 258 """ 259 available = False 260 261 venvName = self.environmentsComboBox.currentText() 262 if venvName: 263 available = ( 264 len(self.packagesList.findItems( 265 "pip", 266 Qt.MatchFlag.MatchExactly | 267 Qt.MatchFlag.MatchCaseSensitive)) == 1 268 ) 269 270 return available 271 272 def __availablePipVersion(self): 273 """ 274 Private method to get the pip version of the selected environment. 275 276 @return tuple containing the version number or tuple with all zeros 277 in case pip is not available 278 @rtype tuple of int 279 """ 280 pipVersionTuple = (0, 0, 0) 281 venvName = self.environmentsComboBox.currentText() 282 if venvName: 283 pipList = self.packagesList.findItems( 284 "pip", 285 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive 286 ) 287 if len(pipList) > 0: 288 pipVersionTuple = Globals.versionToTuple(pipList[0].text(1)) 289 290 return pipVersionTuple 291 292 def getPip(self): 293 """ 294 Public method to get a reference to the pip interface object. 295 296 @return reference to the pip interface object 297 @rtype Pip 298 """ 299 return self.__pip 300 301 ####################################################################### 302 ## Slots handling widget signals below 303 ####################################################################### 304 305 def __selectedUpdateableItems(self): 306 """ 307 Private method to get a list of selected items that can be updated. 308 309 @return list of selected items that can be updated 310 @rtype list of QTreeWidgetItem 311 """ 312 return [ 313 itm for itm in self.packagesList.selectedItems() 314 if bool(itm.text(2)) 315 ] 316 317 def __allUpdateableItems(self): 318 """ 319 Private method to get a list of all items that can be updated. 320 321 @return list of all items that can be updated 322 @rtype list of QTreeWidgetItem 323 """ 324 updateableItems = [] 325 for index in range(self.packagesList.topLevelItemCount()): 326 itm = self.packagesList.topLevelItem(index) 327 if itm.text(2): 328 updateableItems.append(itm) 329 330 return updateableItems 331 332 def __updateActionButtons(self): 333 """ 334 Private method to set the state of the action buttons. 335 """ 336 if self.__isPipAvailable(): 337 self.upgradeButton.setEnabled( 338 bool(self.__selectedUpdateableItems())) 339 self.uninstallButton.setEnabled( 340 bool(self.packagesList.selectedItems())) 341 self.upgradeAllButton.setEnabled( 342 bool(self.__allUpdateableItems())) 343 self.showPackageDetailsButton.setEnabled( 344 len(self.packagesList.selectedItems()) == 1) 345 else: 346 self.upgradeButton.setEnabled(False) 347 self.uninstallButton.setEnabled(False) 348 self.upgradeAllButton.setEnabled(False) 349 self.showPackageDetailsButton.setEnabled(False) 350 351 def __refreshPackagesList(self): 352 """ 353 Private method to referesh the packages list. 354 """ 355 self.packagesList.clear() 356 venvName = self.environmentsComboBox.currentText() 357 if venvName: 358 interpreter = self.__pip.getVirtualenvInterpreter(venvName) 359 if interpreter: 360 self.statusLabel.show() 361 self.statusLabel.setText( 362 self.tr("Getting installed packages...")) 363 364 with E5OverrideCursor(): 365 # 1. populate with installed packages 366 self.packagesList.setUpdatesEnabled(False) 367 installedPackages = self.__pip.getInstalledPackages( 368 venvName, 369 localPackages=self.localCheckBox.isChecked(), 370 notRequired=self.notRequiredCheckBox.isChecked(), 371 usersite=self.userCheckBox.isChecked(), 372 ) 373 for package, version in installedPackages: 374 QTreeWidgetItem(self.packagesList, [package, version]) 375 self.packagesList.setUpdatesEnabled(True) 376 self.statusLabel.setText( 377 self.tr("Getting outdated packages...")) 378 QApplication.processEvents() 379 380 # 2. update with update information 381 self.packagesList.setUpdatesEnabled(False) 382 outdatedPackages = self.__pip.getOutdatedPackages( 383 venvName, 384 localPackages=self.localCheckBox.isChecked(), 385 notRequired=self.notRequiredCheckBox.isChecked(), 386 usersite=self.userCheckBox.isChecked(), 387 ) 388 for package, _version, latest in outdatedPackages: 389 items = self.packagesList.findItems( 390 package, 391 Qt.MatchFlag.MatchExactly | 392 Qt.MatchFlag.MatchCaseSensitive 393 ) 394 if items: 395 itm = items[0] 396 itm.setText(2, latest) 397 398 self.packagesList.sortItems(0, Qt.SortOrder.AscendingOrder) 399 for col in range(self.packagesList.columnCount()): 400 self.packagesList.resizeColumnToContents(col) 401 self.packagesList.setUpdatesEnabled(True) 402 self.statusLabel.hide() 403 404 self.__updateActionButtons() 405 self.__updateSearchActionButtons() 406 self.__updateSearchButton() 407 408 @pyqtSlot(int) 409 def on_environmentsComboBox_currentIndexChanged(self, index): 410 """ 411 Private slot handling the selection of a Python environment. 412 413 @param index index of the selected Python environment 414 @type int 415 """ 416 self.__refreshPackagesList() 417 418 @pyqtSlot(bool) 419 def on_localCheckBox_clicked(self, checked): 420 """ 421 Private slot handling the switching of the local mode. 422 423 @param checked state of the local check box 424 @type bool 425 """ 426 self.__refreshPackagesList() 427 428 @pyqtSlot(bool) 429 def on_notRequiredCheckBox_clicked(self, checked): 430 """ 431 Private slot handling the switching of the 'not required' mode. 432 433 @param checked state of the 'not required' check box 434 @type bool 435 """ 436 self.__refreshPackagesList() 437 438 @pyqtSlot(bool) 439 def on_userCheckBox_clicked(self, checked): 440 """ 441 Private slot handling the switching of the 'user-site' mode. 442 443 @param checked state of the 'user-site' check box 444 @type bool 445 """ 446 self.__refreshPackagesList() 447 448 @pyqtSlot() 449 def on_packagesList_itemSelectionChanged(self): 450 """ 451 Private slot handling the selection of a package. 452 """ 453 self.infoWidget.clear() 454 455 if len(self.packagesList.selectedItems()) == 1: 456 itm = self.packagesList.selectedItems()[0] 457 458 environment = self.environmentsComboBox.currentText() 459 interpreter = self.__pip.getVirtualenvInterpreter(environment) 460 if not interpreter: 461 return 462 463 args = ["-m", "pip", "show"] 464 if self.verboseCheckBox.isChecked(): 465 args.append("--verbose") 466 if self.installedFilesCheckBox.isChecked(): 467 args.append("--files") 468 args.append(itm.text(0)) 469 470 with E5OverrideCursor(): 471 success, output = self.__pip.runProcess(args, interpreter) 472 473 if success and output: 474 mode = self.ShowProcessGeneralMode 475 for line in output.splitlines(): 476 line = line.rstrip() 477 if line != "---": 478 if mode != self.ShowProcessGeneralMode: 479 if line[0] == " ": 480 QTreeWidgetItem( 481 self.infoWidget, 482 [" ", line.strip()]) 483 else: 484 mode = self.ShowProcessGeneralMode 485 if mode == self.ShowProcessGeneralMode: 486 try: 487 label, info = line.split(": ", 1) 488 except ValueError: 489 label = line[:-1] 490 info = "" 491 label = label.lower() 492 if label in self.__infoLabels: 493 QTreeWidgetItem( 494 self.infoWidget, 495 [self.__infoLabels[label], info]) 496 if label == "files": 497 mode = self.ShowProcessFilesListMode 498 elif label == "classifiers": 499 mode = self.ShowProcessClassifiersMode 500 elif label == "entry-points": 501 mode = self.ShowProcessEntryPointsMode 502 self.infoWidget.scrollToTop() 503 504 header = self.infoWidget.header() 505 header.setStretchLastSection(False) 506 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) 507 if ( 508 header.sectionSize(0) + header.sectionSize(1) < 509 header.width() 510 ): 511 header.setStretchLastSection(True) 512 513 self.__updateActionButtons() 514 515 @pyqtSlot(QTreeWidgetItem, int) 516 def on_packagesList_itemActivated(self, item, column): 517 """ 518 Private slot reacting on a package item activation. 519 520 @param item reference to the activated item 521 @type QTreeWidgetItem 522 @param column activated column 523 @type int 524 """ 525 packageName = item.text(0) 526 upgradable = bool(item.text(2)) 527 if column == 1: 528 # show details for installed version 529 packageVersion = item.text(1) 530 else: 531 # show details for available version or installed one 532 if item.text(2): 533 packageVersion = item.text(2) 534 else: 535 packageVersion = item.text(1) 536 537 self.__showPackageDetails(packageName, packageVersion, 538 upgradable=upgradable) 539 540 @pyqtSlot(bool) 541 def on_verboseCheckBox_clicked(self, checked): 542 """ 543 Private slot to handle a change of the verbose package information 544 checkbox. 545 546 @param checked state of the checkbox 547 @type bool 548 """ 549 self.on_packagesList_itemSelectionChanged() 550 551 @pyqtSlot(bool) 552 def on_installedFilesCheckBox_clicked(self, checked): 553 """ 554 Private slot to handle a change of the installed files information 555 checkbox. 556 557 @param checked state of the checkbox 558 @type bool 559 """ 560 self.on_packagesList_itemSelectionChanged() 561 562 @pyqtSlot() 563 def on_refreshButton_clicked(self): 564 """ 565 Private slot to refresh the display. 566 """ 567 currentEnvironment = self.environmentsComboBox.currentText() 568 self.environmentsComboBox.clear() 569 self.packagesList.clear() 570 571 with E5OverrideCursor(): 572 self.__populateEnvironments() 573 574 index = self.environmentsComboBox.findText( 575 currentEnvironment, 576 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive 577 ) 578 if index != -1: 579 self.environmentsComboBox.setCurrentIndex(index) 580 581 self.__updateActionButtons() 582 583 @pyqtSlot() 584 def on_upgradeButton_clicked(self): 585 """ 586 Private slot to upgrade selected packages of the selected environment. 587 """ 588 packages = [itm.text(0) for itm in self.__selectedUpdateableItems()] 589 if packages: 590 self.executeUpgradePackages(packages) 591 592 @pyqtSlot() 593 def on_upgradeAllButton_clicked(self): 594 """ 595 Private slot to upgrade all packages of the selected environment. 596 """ 597 packages = [itm.text(0) for itm in self.__allUpdateableItems()] 598 if packages: 599 self.executeUpgradePackages(packages) 600 601 @pyqtSlot() 602 def on_uninstallButton_clicked(self): 603 """ 604 Private slot to remove selected packages of the selected environment. 605 """ 606 packages = [itm.text(0) for itm in self.packagesList.selectedItems()] 607 self.executeUninstallPackages(packages) 608 609 def executeUninstallPackages(self, packages): 610 """ 611 Public method to uninstall the given list of packages. 612 613 @param packages list of package names to be uninstalled 614 @type list of str 615 """ 616 if packages: 617 ok = self.__pip.uninstallPackages( 618 packages, 619 venvName=self.environmentsComboBox.currentText()) 620 if ok: 621 self.on_refreshButton_clicked() 622 623 def executeUpgradePackages(self, packages): 624 """ 625 Public method to execute the pip upgrade command. 626 627 @param packages list of package names to be upgraded 628 @type list of str 629 """ 630 ok = self.__pip.upgradePackages( 631 packages, venvName=self.environmentsComboBox.currentText(), 632 userSite=self.userCheckBox.isChecked()) 633 if ok: 634 self.on_refreshButton_clicked() 635 636 @pyqtSlot() 637 def on_showPackageDetailsButton_clicked(self): 638 """ 639 Private slot to show information for the selected package. 640 """ 641 item = self.packagesList.selectedItems()[0] 642 if item: 643 packageName = item.text(0) 644 upgradable = bool(item.text(2)) 645 # show details for available version or installed one 646 if item.text(2): 647 packageVersion = item.text(2) 648 else: 649 packageVersion = item.text(1) 650 651 self.__showPackageDetails(packageName, packageVersion, 652 upgradable=upgradable) 653 654 ####################################################################### 655 ## Search widget related methods below 656 ####################################################################### 657 658 def __updateSearchActionButtons(self): 659 """ 660 Private method to update the action button states of the search widget. 661 """ 662 installEnable = ( 663 len(self.searchResultList.selectedItems()) > 0 and 664 self.environmentsComboBox.currentIndex() > 0 and 665 self.__isPipAvailable() 666 ) 667 self.installButton.setEnabled(installEnable) 668 self.installUserSiteButton.setEnabled(installEnable) 669 670 self.showDetailsButton.setEnabled( 671 len(self.searchResultList.selectedItems()) == 1 and 672 self.__isPipAvailable() 673 ) 674 675 def __updateSearchButton(self): 676 """ 677 Private method to update the state of the search button. 678 """ 679 self.searchButton.setEnabled( 680 bool(self.searchEditName.text()) and 681 self.__isPipAvailable() 682 ) 683 684 @pyqtSlot(bool) 685 def on_searchToggleButton_toggled(self, checked): 686 """ 687 Private slot to togle the search widget. 688 689 @param checked state of the search widget button 690 @type bool 691 """ 692 self.searchWidget.setVisible(checked) 693 694 if checked: 695 self.searchEditName.setFocus(Qt.FocusReason.OtherFocusReason) 696 self.searchEditName.selectAll() 697 698 self.__updateSearchActionButtons() 699 self.__updateSearchButton() 700 701 @pyqtSlot(str) 702 def on_searchEditName_textChanged(self, txt): 703 """ 704 Private slot handling a change of the search term. 705 706 @param txt search term 707 @type str 708 """ 709 self.__updateSearchButton() 710 711 @pyqtSlot() 712 def on_searchEditName_returnPressed(self): 713 """ 714 Private slot initiating a search via a press of the Return key. 715 """ 716 if ( 717 bool(self.searchEditName.text()) and 718 self.__isPipAvailable() 719 ): 720 self.__search() 721 722 @pyqtSlot() 723 def on_searchButton_clicked(self): 724 """ 725 Private slot handling a press of the search button. 726 """ 727 self.__search() 728 729 @pyqtSlot() 730 def on_searchResultList_itemSelectionChanged(self): 731 """ 732 Private slot handling changes of the search result selection. 733 """ 734 self.__updateSearchActionButtons() 735 736 def __search(self): 737 """ 738 Private method to perform the search by calling the PyPI search URL. 739 """ 740 self.searchResultList.clear() 741 self.searchInfoLabel.clear() 742 743 self.searchButton.setEnabled(False) 744 745 searchTerm = self.searchEditName.text().strip() 746 searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode() 747 urlQuery = QUrlQuery() 748 urlQuery.addQueryItem("q", searchTerm) 749 url = QUrl(self.__pip.getIndexUrlSearch()) 750 url.setQuery(urlQuery) 751 752 request = QNetworkRequest(QUrl(url)) 753 request.setAttribute( 754 QNetworkRequest.Attribute.CacheLoadControlAttribute, 755 QNetworkRequest.CacheLoadControl.AlwaysNetwork) 756 reply = self.__pip.getNetworkAccessManager().get(request) 757 reply.finished.connect( 758 lambda: self.__searchResponse(reply)) 759 self.__replies.append(reply) 760 761 def __searchResponse(self, reply): 762 """ 763 Private method to extract the search result data from the response. 764 765 @param reply reference to the reply object containing the data 766 @type QNetworkReply 767 """ 768 if reply in self.__replies: 769 self.__replies.remove(reply) 770 771 urlQuery = QUrlQuery(reply.url()) 772 searchTerm = urlQuery.queryItemValue("q") 773 774 if reply.error() != QNetworkReply.NetworkError.NoError: 775 E5MessageBox.warning( 776 None, 777 self.tr("Search PyPI"), 778 self.tr( 779 "<p>Received an error while searching for <b>{0}</b>.</p>" 780 "<p>Error: {1}</p>" 781 ).format(searchTerm, reply.errorString()) 782 ) 783 reply.deleteLater() 784 return 785 786 data = bytes(reply.readAll()).decode() 787 reply.deleteLater() 788 789 results = PypiSearchResultsParser(data).getResults() 790 if results: 791 if len(results) < 20: 792 msg = self.tr("%n package(s) found.", "", len(results)) 793 else: 794 msg = self.tr("Showing first 20 packages found.") 795 self.searchInfoLabel.setText(msg) 796 else: 797 E5MessageBox.warning( 798 self, 799 self.tr("Search PyPI"), 800 self.tr("""<p>There were no results for <b>{0}</b>.</p>""")) 801 self.searchInfoLabel.setText( 802 self.tr("""<p>There were no results for <b>{0}</b>.</p>""")) 803 804 wrapper = textwrap.TextWrapper(width=80) 805 for result in results: 806 try: 807 description = "\n".join([ 808 wrapper.fill(line) for line in 809 result['description'].strip().splitlines() 810 ]) 811 except KeyError: 812 description = "" 813 itm = QTreeWidgetItem( 814 self.searchResultList, [ 815 result['name'].strip(), 816 result['version'], 817 result["released"].strip(), 818 description, 819 ]) 820 itm.setData(0, self.SearchVersionRole, result['version']) 821 822 header = self.searchResultList.header() 823 header.setStretchLastSection(False) 824 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) 825 headerSize = 0 826 for col in range(header.count()): 827 headerSize += header.sectionSize(col) 828 if headerSize < header.width(): 829 header.setStretchLastSection(True) 830 831 self.__finishSearch() 832 833 def __finishSearch(self): 834 """ 835 Private slot performing the search finishing actions. 836 """ 837 self.__updateSearchActionButtons() 838 self.__updateSearchButton() 839 840 self.searchEditName.setFocus(Qt.FocusReason.OtherFocusReason) 841 842 @pyqtSlot() 843 def on_installButton_clicked(self): 844 """ 845 Private slot to handle pressing the Install button.. 846 """ 847 packages = [ 848 itm.text(0).strip() 849 for itm in self.searchResultList.selectedItems() 850 ] 851 self.executeInstallPackages(packages) 852 853 @pyqtSlot() 854 def on_installUserSiteButton_clicked(self): 855 """ 856 Private slot to handle pressing the Install to User-Site button.. 857 """ 858 packages = [ 859 itm.text(0).strip() 860 for itm in self.searchResultList.selectedItems() 861 ] 862 self.executeInstallPackages(packages, userSite=True) 863 864 def executeInstallPackages(self, packages, userSite=False): 865 """ 866 Public method to install the given list of packages. 867 868 @param packages list of package names to be installed 869 @type list of str 870 @param userSite flag indicating to install to the user directory 871 @type bool 872 """ 873 venvName = self.environmentsComboBox.currentText() 874 if venvName and packages: 875 self.__pip.installPackages(packages, venvName=venvName, 876 userSite=userSite) 877 self.on_refreshButton_clicked() 878 879 @pyqtSlot() 880 def on_showDetailsButton_clicked(self): 881 """ 882 Private slot to handle pressing the Show Details button. 883 """ 884 self.__showSearchedDetails() 885 886 @pyqtSlot(QTreeWidgetItem, int) 887 def on_searchResultList_itemActivated(self, item, column): 888 """ 889 Private slot reacting on an search result item activation. 890 891 @param item reference to the activated item 892 @type QTreeWidgetItem 893 @param column activated column 894 @type int 895 """ 896 self.__showSearchedDetails(item) 897 898 def __showSearchedDetails(self, item=None): 899 """ 900 Private slot to show details about the selected search result package. 901 902 @param item reference to the search result item to show details for 903 @type QTreeWidgetItem 904 """ 905 self.showDetailsButton.setEnabled(False) 906 907 if not item: 908 item = self.searchResultList.selectedItems()[0] 909 910 packageVersion = item.data(0, self.SearchVersionRole) 911 packageName = item.text(0) 912 913 self.__showPackageDetails(packageName, packageVersion, 914 installable=True) 915 916 def __showPackageDetails(self, packageName, packageVersion, 917 upgradable=False, installable=False): 918 """ 919 Private method to populate the package details dialog. 920 921 @param packageName name of the package to show details for 922 @type str 923 @param packageVersion version of the package 924 @type str 925 @param upgradable flag indicating that the package may be upgraded 926 (defaults to False) 927 @type bool (optional) 928 @param installable flag indicating that the package may be installed 929 (defaults to False) 930 @type bool (optional) 931 """ 932 with E5OverrideCursor(): 933 packageData = self.__pip.getPackageDetails( 934 packageName, packageVersion) 935 936 if packageData: 937 from .PipPackageDetailsDialog import PipPackageDetailsDialog 938 939 self.showDetailsButton.setEnabled(True) 940 941 if installable: 942 buttonsMode = PipPackageDetailsDialog.ButtonInstall 943 elif upgradable: 944 buttonsMode = ( 945 PipPackageDetailsDialog.ButtonRemove | 946 PipPackageDetailsDialog.ButtonUpgrade 947 ) 948 else: 949 buttonsMode = PipPackageDetailsDialog.ButtonRemove 950 951 if self.__packageDetailsDialog is not None: 952 self.__packageDetailsDialog.close() 953 954 self.__packageDetailsDialog = ( 955 PipPackageDetailsDialog(packageData, buttonsMode=buttonsMode, 956 parent=self) 957 ) 958 self.__packageDetailsDialog.show() 959 else: 960 E5MessageBox.warning( 961 self, 962 self.tr("Search PyPI"), 963 self.tr("""<p>No package details info for <b>{0}</b>""" 964 """ available.</p>""").format(packageName)) 965 966 ####################################################################### 967 ## Menu related methods below 968 ####################################################################### 969 970 def __initPipMenu(self): 971 """ 972 Private method to create the super menu and attach it to the super 973 menu button. 974 """ 975 self.__pipMenu = QMenu() 976 self.__installPipAct = self.__pipMenu.addAction( 977 self.tr("Install Pip"), 978 self.__installPip) 979 self.__installPipUserAct = self.__pipMenu.addAction( 980 self.tr("Install Pip to User-Site"), 981 self.__installPipUser) 982 self.__repairPipAct = self.__pipMenu.addAction( 983 self.tr("Repair Pip"), 984 self.__repairPip) 985 self.__pipMenu.addSeparator() 986 self.__installPackagesAct = self.__pipMenu.addAction( 987 self.tr("Install Packages"), 988 self.__installPackages) 989 self.__installLocalPackageAct = self.__pipMenu.addAction( 990 self.tr("Install Local Package"), 991 self.__installLocalPackage) 992 self.__pipMenu.addSeparator() 993 self.__installRequirementsAct = self.__pipMenu.addAction( 994 self.tr("Install Requirements"), 995 self.__installRequirements) 996 self.__reinstallPackagesAct = self.__pipMenu.addAction( 997 self.tr("Re-Install Selected Packages"), 998 self.__reinstallPackages) 999 self.__uninstallRequirementsAct = self.__pipMenu.addAction( 1000 self.tr("Uninstall Requirements"), 1001 self.__uninstallRequirements) 1002 self.__generateRequirementsAct = self.__pipMenu.addAction( 1003 self.tr("Generate Requirements..."), 1004 self.__generateRequirements) 1005 self.__pipMenu.addSeparator() 1006 self.__cacheInfoAct = self.__pipMenu.addAction( 1007 self.tr("Show Cache Info..."), 1008 self.__showCacheInfo) 1009 self.__cacheShowListAct = self.__pipMenu.addAction( 1010 self.tr("Show Cached Files..."), 1011 self.__showCacheList) 1012 self.__cacheRemoveAct = self.__pipMenu.addAction( 1013 self.tr("Remove Cached Files..."), 1014 self.__removeCachedFiles) 1015 self.__cachePurgeAct = self.__pipMenu.addAction( 1016 self.tr("Purge Cache..."), 1017 self.__purgeCache) 1018 self.__pipMenu.addSeparator() 1019 # editUserConfigAct 1020 self.__pipMenu.addAction( 1021 self.tr("Edit User Configuration..."), 1022 self.__editUserConfiguration) 1023 self.__editVirtualenvConfigAct = self.__pipMenu.addAction( 1024 self.tr("Edit Environment Configuration..."), 1025 self.__editVirtualenvConfiguration) 1026 self.__pipMenu.addSeparator() 1027 # pipConfigAct 1028 self.__pipMenu.addAction( 1029 self.tr("Configure..."), 1030 self.__pipConfigure) 1031 1032 self.__pipMenu.aboutToShow.connect(self.__aboutToShowPipMenu) 1033 1034 self.pipMenuButton.setMenu(self.__pipMenu) 1035 1036 def __aboutToShowPipMenu(self): 1037 """ 1038 Private slot to set the action enabled status. 1039 """ 1040 enable = bool(self.environmentsComboBox.currentText()) 1041 enablePip = self.__isPipAvailable() 1042 enablePipCache = self.__availablePipVersion() >= (20, 1, 0) 1043 1044 self.__installPipAct.setEnabled(not enablePip) 1045 self.__installPipUserAct.setEnabled(not enablePip) 1046 self.__repairPipAct.setEnabled(enablePip) 1047 1048 self.__installPackagesAct.setEnabled(enablePip) 1049 self.__installLocalPackageAct.setEnabled(enablePip) 1050 self.__reinstallPackagesAct.setEnabled(enablePip) 1051 1052 self.__installRequirementsAct.setEnabled(enablePip) 1053 self.__uninstallRequirementsAct.setEnabled(enablePip) 1054 self.__generateRequirementsAct.setEnabled(enablePip) 1055 1056 self.__cacheInfoAct.setEnabled(enablePipCache) 1057 self.__cacheShowListAct.setEnabled(enablePipCache) 1058 self.__cacheRemoveAct.setEnabled(enablePipCache) 1059 self.__cachePurgeAct.setEnabled(enablePipCache) 1060 1061 self.__editVirtualenvConfigAct.setEnabled(enable) 1062 1063 @pyqtSlot() 1064 def __installPip(self): 1065 """ 1066 Private slot to install pip into the selected environment. 1067 """ 1068 venvName = self.environmentsComboBox.currentText() 1069 if venvName: 1070 self.__pip.installPip(venvName) 1071 self.on_refreshButton_clicked() 1072 1073 @pyqtSlot() 1074 def __installPipUser(self): 1075 """ 1076 Private slot to install pip into the user site for the selected 1077 environment. 1078 """ 1079 venvName = self.environmentsComboBox.currentText() 1080 if venvName: 1081 self.__pip.installPip(venvName, userSite=True) 1082 self.on_refreshButton_clicked() 1083 1084 @pyqtSlot() 1085 def __repairPip(self): 1086 """ 1087 Private slot to repair the pip installation of the selected 1088 environment. 1089 """ 1090 venvName = self.environmentsComboBox.currentText() 1091 if venvName: 1092 self.__pip.repairPip(venvName) 1093 self.on_refreshButton_clicked() 1094 1095 @pyqtSlot() 1096 def __installPackages(self): 1097 """ 1098 Private slot to install packages to be given by the user. 1099 """ 1100 venvName = self.environmentsComboBox.currentText() 1101 if venvName: 1102 from .PipPackagesInputDialog import PipPackagesInputDialog 1103 dlg = PipPackagesInputDialog(self, self.tr("Install Packages")) 1104 if dlg.exec() == QDialog.DialogCode.Accepted: 1105 packages, user = dlg.getData() 1106 self.executeInstallPackages(packages, userSite=user) 1107 1108 @pyqtSlot() 1109 def __installLocalPackage(self): 1110 """ 1111 Private slot to install a package available on local storage. 1112 """ 1113 venvName = self.environmentsComboBox.currentText() 1114 if venvName: 1115 from .PipFileSelectionDialog import PipFileSelectionDialog 1116 dlg = PipFileSelectionDialog(self, "package") 1117 if dlg.exec() == QDialog.DialogCode.Accepted: 1118 package, user = dlg.getData() 1119 if package and os.path.exists(package): 1120 self.executeInstallPackages([package], userSite=user) 1121 1122 @pyqtSlot() 1123 def __reinstallPackages(self): 1124 """ 1125 Private slot to force a re-installation of the selected packages. 1126 """ 1127 packages = [itm.text(0) for itm in self.packagesList.selectedItems()] 1128 venvName = self.environmentsComboBox.currentText() 1129 if venvName and packages: 1130 self.__pip.installPackages(packages, venvName=venvName, 1131 forceReinstall=True) 1132 self.on_refreshButton_clicked() 1133 1134 @pyqtSlot() 1135 def __installRequirements(self): 1136 """ 1137 Private slot to install packages as given in a requirements file. 1138 """ 1139 venvName = self.environmentsComboBox.currentText() 1140 if venvName: 1141 self.__pip.installRequirements(venvName) 1142 self.on_refreshButton_clicked() 1143 1144 @pyqtSlot() 1145 def __uninstallRequirements(self): 1146 """ 1147 Private slot to uninstall packages as given in a requirements file. 1148 """ 1149 venvName = self.environmentsComboBox.currentText() 1150 if venvName: 1151 self.__pip.uninstallRequirements(venvName) 1152 self.on_refreshButton_clicked() 1153 1154 @pyqtSlot() 1155 def __generateRequirements(self): 1156 """ 1157 Private slot to generate the contents for a requirements file. 1158 """ 1159 venvName = self.environmentsComboBox.currentText() 1160 if venvName: 1161 from .PipFreezeDialog import PipFreezeDialog 1162 self.__freezeDialog = PipFreezeDialog(self.__pip, self) 1163 self.__freezeDialog.show() 1164 self.__freezeDialog.start(venvName) 1165 1166 @pyqtSlot() 1167 def __editUserConfiguration(self): 1168 """ 1169 Private slot to edit the user configuration. 1170 """ 1171 self.__editConfiguration() 1172 1173 @pyqtSlot() 1174 def __editVirtualenvConfiguration(self): 1175 """ 1176 Private slot to edit the configuration of the selected environment. 1177 """ 1178 venvName = self.environmentsComboBox.currentText() 1179 if venvName: 1180 self.__editConfiguration(venvName=venvName) 1181 1182 def __editConfiguration(self, venvName=""): 1183 """ 1184 Private method to edit a configuration. 1185 1186 @param venvName name of the environment to act upon 1187 @type str 1188 """ 1189 from QScintilla.MiniEditor import MiniEditor 1190 if venvName: 1191 cfgFile = self.__pip.getVirtualenvConfig(venvName) 1192 if not cfgFile: 1193 return 1194 else: 1195 cfgFile = self.__pip.getUserConfig() 1196 cfgDir = os.path.dirname(cfgFile) 1197 if not cfgDir: 1198 E5MessageBox.critical( 1199 None, 1200 self.tr("Edit Configuration"), 1201 self.tr("""No valid configuration path determined.""" 1202 """ Aborting""")) 1203 return 1204 1205 try: 1206 if not os.path.isdir(cfgDir): 1207 os.makedirs(cfgDir) 1208 except OSError: 1209 E5MessageBox.critical( 1210 None, 1211 self.tr("Edit Configuration"), 1212 self.tr("""No valid configuration path determined.""" 1213 """ Aborting""")) 1214 return 1215 1216 if not os.path.exists(cfgFile): 1217 with contextlib.suppress(OSError), open(cfgFile, "w") as f: 1218 f.write("[global]\n") 1219 1220 # check, if the destination is writeable 1221 if not os.access(cfgFile, os.W_OK): 1222 E5MessageBox.critical( 1223 None, 1224 self.tr("Edit Configuration"), 1225 self.tr("""No valid configuration path determined.""" 1226 """ Aborting""")) 1227 return 1228 1229 self.__editor = MiniEditor(cfgFile, "Properties") 1230 self.__editor.show() 1231 1232 def __pipConfigure(self): 1233 """ 1234 Private slot to open the configuration page. 1235 """ 1236 e5App().getObject("UserInterface").showPreferences("pipPage") 1237 1238 @pyqtSlot() 1239 def __showCacheInfo(self): 1240 """ 1241 Private slot to show information about the cache. 1242 """ 1243 venvName = self.environmentsComboBox.currentText() 1244 if venvName: 1245 self.__pip.showCacheInfo(venvName) 1246 1247 @pyqtSlot() 1248 def __showCacheList(self): 1249 """ 1250 Private slot to show a list of cached files. 1251 """ 1252 venvName = self.environmentsComboBox.currentText() 1253 if venvName: 1254 self.__pip.cacheList(venvName) 1255 1256 @pyqtSlot() 1257 def __removeCachedFiles(self): 1258 """ 1259 Private slot to remove files from the pip cache. 1260 """ 1261 venvName = self.environmentsComboBox.currentText() 1262 if venvName: 1263 self.__pip.cacheRemove(venvName) 1264 1265 @pyqtSlot() 1266 def __purgeCache(self): 1267 """ 1268 Private slot to empty the pip cache. 1269 """ 1270 venvName = self.environmentsComboBox.currentText() 1271 if venvName: 1272 self.__pip.cachePurge(venvName) 1273