eric7/PipInterface/PipPackagesWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8240
93b8a353c4bf
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the pip packages management widget.
8 """
9
10 import textwrap
11 import os
12 import html.parser
13 import contextlib
14
15 from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QUrlQuery
16 from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
17 from PyQt5.QtWidgets import (
18 QWidget, QToolButton, QApplication, QHeaderView, QTreeWidgetItem,
19 QMenu, QDialog
20 )
21
22 from E5Gui.E5Application import e5App
23 from E5Gui import E5MessageBox
24 from E5Gui.E5OverrideCursor import E5OverrideCursor
25
26 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget
27
28 import UI.PixmapCache
29 import Globals
30 import Preferences
31
32
33 class 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
141 class 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)

eric ide

mercurial