src/eric7/PipInterface/PipPackagesWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9121
6ac528d4f318
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2022 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 packaging.specifiers import SpecifierSet
16
17 from PyQt6.QtCore import pyqtSlot, Qt, QUrl, QUrlQuery
18 from PyQt6.QtGui import QIcon
19 from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
20 from PyQt6.QtWidgets import (
21 QWidget, QToolButton, QApplication, QHeaderView, QTreeWidgetItem,
22 QMenu, QDialog, QAbstractItemView
23 )
24
25 from EricWidgets.EricApplication import ericApp
26 from EricWidgets import EricMessageBox
27 from EricGui.EricOverrideCursor import EricOverrideCursor
28
29 from .PipVulnerabilityChecker import Package, VulnerabilityCheckError
30 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget
31
32 import UI.PixmapCache
33 import Globals
34 import Preferences
35
36
37 class PypiSearchResultsParser(html.parser.HTMLParser):
38 """
39 Class implementing the parser for the PyPI search result page.
40 """
41 ClassPrefix = "package-snippet__"
42
43 def __init__(self, data):
44 """
45 Constructor
46
47 @param data data to be parsed
48 @type str
49 """
50 super().__init__()
51 self.__results = []
52 self.__activeClass = None
53 self.feed(data)
54
55 def __getClass(self, attrs):
56 """
57 Private method to extract the class attribute out of the list of
58 attributes.
59
60 @param attrs list of tag attributes as (name, value) tuples
61 @type list of tuple of (str, str)
62 @return value of the 'class' attribute or None
63 @rtype str
64 """
65 for name, value in attrs:
66 if name == "class":
67 return value
68
69 return None
70
71 def __getDate(self, attrs):
72 """
73 Private method to extract the datetime attribute out of the list of
74 attributes and process it.
75
76 @param attrs list of tag attributes as (name, value) tuples
77 @type list of tuple of (str, str)
78 @return value of the 'class' attribute or None
79 @rtype str
80 """
81 for name, value in attrs:
82 if name == "datetime":
83 return value.split("T")[0]
84
85 return None
86
87 def handle_starttag(self, tag, attrs):
88 """
89 Public method to process the start tag.
90
91 @param tag tag name (all lowercase)
92 @type str
93 @param attrs list of tag attributes as (name, value) tuples
94 @type list of tuple of (str, str)
95 """
96 if tag == "a" and self.__getClass(attrs) == "package-snippet":
97 self.__results.append({})
98
99 if tag in ("span", "p"):
100 tagClass = self.__getClass(attrs)
101 if tagClass in (
102 "package-snippet__name", "package-snippet__description",
103 "package-snippet__version", "package-snippet__released",
104 "package-snippet__created",
105 ):
106 self.__activeClass = tagClass
107 else:
108 self.__activeClass = None
109 elif tag == "time":
110 attributeName = self.__activeClass.replace(self.ClassPrefix, "")
111 self.__results[-1][attributeName] = self.__getDate(attrs)
112 self.__activeClass = None
113 else:
114 self.__activeClass = None
115
116 def handle_data(self, data):
117 """
118 Public method process arbitrary data.
119
120 @param data data to be processed
121 @type str
122 """
123 if self.__activeClass is not None:
124 attributeName = self.__activeClass.replace(self.ClassPrefix, "")
125 self.__results[-1][attributeName] = data
126
127 def handle_endtag(self, tag):
128 """
129 Public method to process the end tag.
130
131 @param tag tag name (all lowercase)
132 @type str
133 """
134 self.__activeClass = None
135
136 def getResults(self):
137 """
138 Public method to get the extracted search results.
139
140 @return extracted result data
141 @rtype list of dict
142 """
143 return self.__results
144
145
146 class PipPackagesWidget(QWidget, Ui_PipPackagesWidget):
147 """
148 Class implementing the pip packages management widget.
149 """
150 ShowProcessGeneralMode = 0
151 ShowProcessClassifiersMode = 1
152 ShowProcessEntryPointsMode = 2
153 ShowProcessFilesListMode = 3
154
155 SearchVersionRole = Qt.ItemDataRole.UserRole + 1
156 VulnerabilityRole = Qt.ItemDataRole.UserRole + 2
157
158 PackageColumn = 0
159 InstalledVersionColumn = 1
160 AvailableVersionColumn = 2
161 VulnerabilityColumn = 3
162
163 DepPackageColumn = 0
164 DepInstalledVersionColumn = 1
165 DepRequiredVersionColumn = 2
166
167 def __init__(self, pip, parent=None):
168 """
169 Constructor
170
171 @param pip reference to the global pip interface
172 @type Pip
173 @param parent reference to the parent widget
174 @type QWidget
175 """
176 super().__init__(parent)
177 self.setupUi(self)
178
179 self.layout().setContentsMargins(0, 3, 0, 0)
180
181 self.viewToggleButton.setIcon(UI.PixmapCache.getIcon("viewListTree"))
182
183 self.pipMenuButton.setObjectName(
184 "pip_supermenu_button")
185 self.pipMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
186 self.pipMenuButton.setToolTip(self.tr("pip Menu"))
187 self.pipMenuButton.setPopupMode(
188 QToolButton.ToolButtonPopupMode.InstantPopup)
189 self.pipMenuButton.setToolButtonStyle(
190 Qt.ToolButtonStyle.ToolButtonIconOnly)
191 self.pipMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
192 self.pipMenuButton.setAutoRaise(True)
193 self.pipMenuButton.setShowMenuInside(True)
194
195 self.refreshButton.setIcon(
196 UI.PixmapCache.getIcon("reload"))
197 self.upgradeButton.setIcon(
198 UI.PixmapCache.getIcon("1uparrow"))
199 self.upgradeAllButton.setIcon(
200 UI.PixmapCache.getIcon("2uparrow"))
201 self.uninstallButton.setIcon(
202 UI.PixmapCache.getIcon("minus"))
203 self.showPackageDetailsButton.setIcon(
204 UI.PixmapCache.getIcon("info"))
205 self.searchToggleButton.setIcon(
206 UI.PixmapCache.getIcon("find"))
207 self.searchButton.setIcon(
208 UI.PixmapCache.getIcon("findNext"))
209 self.searchMoreButton.setIcon(
210 UI.PixmapCache.getIcon("plus"))
211 self.installButton.setIcon(
212 UI.PixmapCache.getIcon("plus"))
213 self.installUserSiteButton.setIcon(
214 UI.PixmapCache.getIcon("addUser"))
215 self.showDetailsButton.setIcon(
216 UI.PixmapCache.getIcon("info"))
217
218 self.refreshDependenciesButton.setIcon(
219 UI.PixmapCache.getIcon("reload"))
220 self.showDepPackageDetailsButton.setIcon(
221 UI.PixmapCache.getIcon("info"))
222
223 self.__pip = pip
224
225 self.packagesList.header().setSortIndicator(
226 PipPackagesWidget.PackageColumn, Qt.SortOrder.AscendingOrder)
227 self.dependenciesList.header().setSortIndicator(
228 PipPackagesWidget.DepPackageColumn, Qt.SortOrder.AscendingOrder)
229
230 self.__infoLabels = {
231 "name": self.tr("Name:"),
232 "version": self.tr("Version:"),
233 "location": self.tr("Location:"),
234 "requires": self.tr("Requires:"),
235 "summary": self.tr("Summary:"),
236 "home-page": self.tr("Homepage:"),
237 "author": self.tr("Author:"),
238 "author-email": self.tr("Author Email:"),
239 "license": self.tr("License:"),
240 "metadata-version": self.tr("Metadata Version:"),
241 "installer": self.tr("Installer:"),
242 "classifiers": self.tr("Classifiers:"),
243 "entry-points": self.tr("Entry Points:"),
244 "files": self.tr("Files:"),
245 }
246 self.infoWidget.setHeaderLabels(["Key", "Value"])
247 self.dependencyInfoWidget.setHeaderLabels(["Key", "Value"])
248
249 venvManager = ericApp().getObject("VirtualEnvManager")
250 venvManager.virtualEnvironmentAdded.connect(
251 self.on_refreshButton_clicked)
252 venvManager.virtualEnvironmentRemoved.connect(
253 self.on_refreshButton_clicked)
254 self.__selectedEnvironment = None
255
256 project = ericApp().getObject("Project")
257 project.projectOpened.connect(
258 self.__projectOpened)
259 project.projectClosed.connect(
260 self.__projectClosed)
261
262 self.__initPipMenu()
263 self.__populateEnvironments()
264 self.__updateActionButtons()
265 self.__updateDepActionButtons()
266
267 self.statusLabel.hide()
268 self.searchWidget.hide()
269 self.__lastSearchPage = 0
270
271 self.__queryName = []
272 self.__querySummary = []
273
274 self.__replies = []
275
276 self.__packageDetailsDialog = None
277
278 self.viewsStackWidget.setCurrentWidget(self.packagesPage)
279
280 @pyqtSlot()
281 def __projectOpened(self):
282 """
283 Private slot to handle the projectOpened signal.
284 """
285 projectVenv = self.__pip.getProjectEnvironmentString()
286 if projectVenv:
287 self.environmentsComboBox.insertItem(1, projectVenv)
288
289 @pyqtSlot(bool)
290 def __projectClosed(self, shutdown):
291 """
292 Private slot to handle the projectClosed signal.
293
294 @param shutdown flag indicating the IDE shutdown
295 @type bool
296 """
297 if not shutdown:
298 # the project entry is always at index 1
299 self.environmentsComboBox.removeItem(1)
300
301 def __populateEnvironments(self):
302 """
303 Private method to get a list of environments and populate the selector.
304 """
305 self.environmentsComboBox.addItem("")
306 projectVenv = self.__pip.getProjectEnvironmentString()
307 if projectVenv:
308 self.environmentsComboBox.addItem(projectVenv)
309 self.environmentsComboBox.addItems(
310 self.__pip.getVirtualenvNames(
311 noRemote=True,
312 noConda=Preferences.getPip("ExcludeCondaEnvironments")
313 )
314 )
315
316 def __isPipAvailable(self):
317 """
318 Private method to check, if the pip package is available for the
319 selected environment.
320
321 @return flag indicating availability
322 @rtype bool
323 """
324 available = False
325
326 venvName = self.environmentsComboBox.currentText()
327 if venvName:
328 available = (
329 len(self.packagesList.findItems(
330 "pip",
331 Qt.MatchFlag.MatchExactly |
332 Qt.MatchFlag.MatchCaseSensitive)) == 1
333 )
334
335 return available
336
337 def __availablePipVersion(self):
338 """
339 Private method to get the pip version of the selected environment.
340
341 @return tuple containing the version number or tuple with all zeros
342 in case pip is not available
343 @rtype tuple of int
344 """
345 pipVersionTuple = (0, 0, 0)
346 venvName = self.environmentsComboBox.currentText()
347 if venvName:
348 pipList = self.packagesList.findItems(
349 "pip",
350 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
351 )
352 if len(pipList) > 0:
353 pipVersionTuple = Globals.versionToTuple(
354 pipList[0].text(PipPackagesWidget.InstalledVersionColumn))
355
356 return pipVersionTuple
357
358 def getPip(self):
359 """
360 Public method to get a reference to the pip interface object.
361
362 @return reference to the pip interface object
363 @rtype Pip
364 """
365 return self.__pip
366
367 #######################################################################
368 ## Slots handling widget signals below
369 #######################################################################
370
371 def __selectedUpdateableItems(self):
372 """
373 Private method to get a list of selected items that can be updated.
374
375 @return list of selected items that can be updated
376 @rtype list of QTreeWidgetItem
377 """
378 return [
379 itm for itm in self.packagesList.selectedItems()
380 if bool(itm.text(PipPackagesWidget.AvailableVersionColumn))
381 ]
382
383 def __allUpdateableItems(self):
384 """
385 Private method to get a list of all items that can be updated.
386
387 @return list of all items that can be updated
388 @rtype list of QTreeWidgetItem
389 """
390 updateableItems = []
391 for index in range(self.packagesList.topLevelItemCount()):
392 itm = self.packagesList.topLevelItem(index)
393 if itm.text(PipPackagesWidget.AvailableVersionColumn):
394 updateableItems.append(itm)
395
396 return updateableItems
397
398 def __updateActionButtons(self):
399 """
400 Private method to set the state of the action buttons.
401 """
402 if self.__isPipAvailable():
403 self.upgradeButton.setEnabled(
404 bool(self.__selectedUpdateableItems()))
405 self.uninstallButton.setEnabled(
406 bool(self.packagesList.selectedItems()))
407 self.upgradeAllButton.setEnabled(
408 bool(self.__allUpdateableItems()))
409 self.showPackageDetailsButton.setEnabled(
410 len(self.packagesList.selectedItems()) == 1)
411 else:
412 self.upgradeButton.setEnabled(False)
413 self.uninstallButton.setEnabled(False)
414 self.upgradeAllButton.setEnabled(False)
415 self.showPackageDetailsButton.setEnabled(False)
416
417 def __refreshPackagesList(self):
418 """
419 Private method to refresh the packages list.
420 """
421 self.packagesList.clear()
422 venvName = self.environmentsComboBox.currentText()
423 if venvName:
424 interpreter = self.__pip.getVirtualenvInterpreter(venvName)
425 if interpreter:
426 self.statusLabel.show()
427 self.statusLabel.setText(
428 self.tr("Getting installed packages..."))
429
430 with EricOverrideCursor():
431 # 1. populate with installed packages
432 self.packagesList.setUpdatesEnabled(False)
433 installedPackages = self.__pip.getInstalledPackages(
434 venvName,
435 localPackages=self.localCheckBox.isChecked(),
436 notRequired=self.notRequiredCheckBox.isChecked(),
437 usersite=self.userCheckBox.isChecked(),
438 )
439 for package, version in installedPackages:
440 QTreeWidgetItem(self.packagesList,
441 [package, version, "", ""])
442 self.packagesList.setUpdatesEnabled(True)
443 self.statusLabel.setText(
444 self.tr("Getting outdated packages..."))
445 QApplication.processEvents()
446
447 # 2. update with update information
448 self.packagesList.setUpdatesEnabled(False)
449 outdatedPackages = self.__pip.getOutdatedPackages(
450 venvName,
451 localPackages=self.localCheckBox.isChecked(),
452 notRequired=self.notRequiredCheckBox.isChecked(),
453 usersite=self.userCheckBox.isChecked(),
454 )
455 for package, _version, latest in outdatedPackages:
456 items = self.packagesList.findItems(
457 package,
458 Qt.MatchFlag.MatchExactly |
459 Qt.MatchFlag.MatchCaseSensitive
460 )
461 if items:
462 itm = items[0]
463 itm.setText(
464 PipPackagesWidget.AvailableVersionColumn,
465 latest)
466
467 self.packagesList.sortItems(
468 PipPackagesWidget.PackageColumn,
469 Qt.SortOrder.AscendingOrder)
470 for col in range(self.packagesList.columnCount()):
471 self.packagesList.resizeColumnToContents(col)
472 self.packagesList.setUpdatesEnabled(True)
473
474 # 3. update with vulnerability information
475 if self.vulnerabilityCheckBox.isChecked():
476 self.__updateVulnerabilityData()
477 self.statusLabel.hide()
478
479 self.__updateActionButtons()
480 self.__updateSearchActionButtons()
481 self.__updateSearchButton()
482 self.__updateSearchMoreButton(False)
483
484 @pyqtSlot(str)
485 def on_environmentsComboBox_currentTextChanged(self, name):
486 """
487 Private slot handling the selection of a Python environment.
488
489 @param name name of the selected Python environment
490 @type str
491 """
492 if name != self.__selectedEnvironment:
493 if self.viewToggleButton.isChecked():
494 self.__refreshDependencyTree()
495 else:
496 self.__refreshPackagesList()
497 self.__selectedEnvironment = name
498
499 @pyqtSlot()
500 def on_localCheckBox_clicked(self):
501 """
502 Private slot handling the switching of the local mode.
503 """
504 self.__refreshPackagesList()
505
506 @pyqtSlot()
507 def on_notRequiredCheckBox_clicked(self):
508 """
509 Private slot handling the switching of the 'not required' mode.
510 """
511 self.__refreshPackagesList()
512
513 @pyqtSlot()
514 def on_userCheckBox_clicked(self):
515 """
516 Private slot handling the switching of the 'user-site' mode.
517 """
518 self.__refreshPackagesList()
519
520 def __showPackageInformation(self, packageName, infoWidget):
521 """
522 Private method to show information for a package.
523
524 @param packageName name of the package
525 @type str
526 @param infoWidget reference to the widget to contain the information
527 @type QTreeWidget
528 """
529 environment = self.environmentsComboBox.currentText()
530 interpreter = self.__pip.getVirtualenvInterpreter(environment)
531 if not interpreter:
532 return
533
534 args = ["-m", "pip", "show"]
535 if self.verboseCheckBox.isChecked():
536 args.append("--verbose")
537 if self.installedFilesCheckBox.isChecked():
538 args.append("--files")
539 args.append(packageName)
540
541 with EricOverrideCursor():
542 success, output = self.__pip.runProcess(args, interpreter)
543
544 if success and output:
545 mode = self.ShowProcessGeneralMode
546 for line in output.splitlines():
547 line = line.rstrip()
548 if line != "---":
549 if mode != self.ShowProcessGeneralMode:
550 if line[0] == " ":
551 QTreeWidgetItem(
552 infoWidget,
553 [" ", line.strip()])
554 else:
555 mode = self.ShowProcessGeneralMode
556 if mode == self.ShowProcessGeneralMode:
557 try:
558 label, info = line.split(": ", 1)
559 except ValueError:
560 label = line[:-1]
561 info = ""
562 label = label.lower()
563 if label in self.__infoLabels:
564 QTreeWidgetItem(
565 infoWidget,
566 [self.__infoLabels[label], info])
567 if label == "files":
568 mode = self.ShowProcessFilesListMode
569 elif label == "classifiers":
570 mode = self.ShowProcessClassifiersMode
571 elif label == "entry-points":
572 mode = self.ShowProcessEntryPointsMode
573 infoWidget.scrollToTop()
574
575 header = infoWidget.header()
576 header.setStretchLastSection(False)
577 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
578 if (
579 header.sectionSize(0) + header.sectionSize(1) <
580 header.width()
581 ):
582 header.setStretchLastSection(True)
583
584 @pyqtSlot()
585 def on_packagesList_itemSelectionChanged(self):
586 """
587 Private slot reacting on a change of selected items.
588 """
589 if len(self.packagesList.selectedItems()) == 0:
590 self.infoWidget.clear()
591
592 @pyqtSlot(QTreeWidgetItem, int)
593 def on_packagesList_itemPressed(self, item, column):
594 """
595 Private slot reacting on a package item being pressed.
596
597 @param item reference to the pressed item
598 @type QTreeWidgetItem
599 @param column pressed column
600 @type int
601 """
602 self.infoWidget.clear()
603
604 if item is not None:
605 if (
606 column == PipPackagesWidget.VulnerabilityColumn and
607 bool(item.text(PipPackagesWidget.VulnerabilityColumn))
608 ):
609 self.__showVulnerabilityInformation(
610 item.text(PipPackagesWidget.PackageColumn),
611 item.text(PipPackagesWidget.InstalledVersionColumn),
612 item.data(PipPackagesWidget.VulnerabilityColumn,
613 PipPackagesWidget.VulnerabilityRole)
614 )
615 else:
616 self.__showPackageInformation(
617 item.text(PipPackagesWidget.PackageColumn),
618 self.infoWidget
619 )
620
621 self.__updateActionButtons()
622
623 @pyqtSlot(QTreeWidgetItem, int)
624 def on_packagesList_itemActivated(self, item, column):
625 """
626 Private slot reacting on a package item being activated.
627
628 @param item reference to the activated item
629 @type QTreeWidgetItem
630 @param column activated column
631 @type int
632 """
633 packageName = item.text(PipPackagesWidget.PackageColumn)
634 upgradable = bool(item.text(PipPackagesWidget.AvailableVersionColumn))
635 if column == PipPackagesWidget.InstalledVersionColumn:
636 # show details for installed version
637 packageVersion = item.text(
638 PipPackagesWidget.InstalledVersionColumn)
639 else:
640 # show details for available version or installed one
641 if item.text(PipPackagesWidget.AvailableVersionColumn):
642 packageVersion = item.text(
643 PipPackagesWidget.AvailableVersionColumn)
644 else:
645 packageVersion = item.text(
646 PipPackagesWidget.InstalledVersionColumn)
647
648 self.__showPackageDetails(packageName, packageVersion,
649 upgradable=upgradable)
650
651 @pyqtSlot(bool)
652 def on_verboseCheckBox_clicked(self, checked):
653 """
654 Private slot to handle a change of the verbose package information
655 checkbox.
656
657 @param checked state of the checkbox
658 @type bool
659 """
660 self.on_packagesList_itemPressed(self.packagesList.currentItem(),
661 self.packagesList.currentColumn())
662
663 @pyqtSlot(bool)
664 def on_installedFilesCheckBox_clicked(self, checked):
665 """
666 Private slot to handle a change of the installed files information
667 checkbox.
668
669 @param checked state of the checkbox
670 @type bool
671 """
672 self.on_packagesList_itemPressed(self.packagesList.currentItem(),
673 self.packagesList.currentColumn())
674
675 @pyqtSlot()
676 def on_refreshButton_clicked(self):
677 """
678 Private slot to refresh the display.
679 """
680 currentEnvironment = self.environmentsComboBox.currentText()
681 self.environmentsComboBox.clear()
682 self.packagesList.clear()
683
684 with EricOverrideCursor():
685 self.__populateEnvironments()
686
687 index = self.environmentsComboBox.findText(
688 currentEnvironment,
689 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
690 )
691 if index != -1:
692 self.environmentsComboBox.setCurrentIndex(index)
693
694 self.__updateActionButtons()
695
696 @pyqtSlot()
697 def on_upgradeButton_clicked(self):
698 """
699 Private slot to upgrade selected packages of the selected environment.
700 """
701 packages = [itm.text(PipPackagesWidget.PackageColumn)
702 for itm in self.__selectedUpdateableItems()]
703 if packages:
704 self.executeUpgradePackages(packages)
705
706 @pyqtSlot()
707 def on_upgradeAllButton_clicked(self):
708 """
709 Private slot to upgrade all packages of the selected environment.
710 """
711 packages = [itm.text(PipPackagesWidget.PackageColumn)
712 for itm in self.__allUpdateableItems()]
713 if packages:
714 self.executeUpgradePackages(packages)
715
716 @pyqtSlot()
717 def on_uninstallButton_clicked(self):
718 """
719 Private slot to remove selected packages of the selected environment.
720 """
721 packages = [itm.text(PipPackagesWidget.PackageColumn)
722 for itm in self.packagesList.selectedItems()]
723 self.executeUninstallPackages(packages)
724
725 def executeUninstallPackages(self, packages):
726 """
727 Public method to uninstall the given list of packages.
728
729 @param packages list of package names to be uninstalled
730 @type list of str
731 """
732 if packages:
733 ok = self.__pip.uninstallPackages(
734 packages,
735 venvName=self.environmentsComboBox.currentText())
736 if ok:
737 self.on_refreshButton_clicked()
738
739 def executeUpgradePackages(self, packages):
740 """
741 Public method to execute the pip upgrade command.
742
743 @param packages list of package names to be upgraded
744 @type list of str
745 """
746 ok = self.__pip.upgradePackages(
747 packages, venvName=self.environmentsComboBox.currentText(),
748 userSite=self.userCheckBox.isChecked())
749 if ok:
750 self.on_refreshButton_clicked()
751
752 @pyqtSlot()
753 def on_showPackageDetailsButton_clicked(self):
754 """
755 Private slot to show information for the selected package.
756 """
757 item = self.packagesList.selectedItems()[0]
758 if item:
759 packageName = item.text(PipPackagesWidget.PackageColumn)
760 upgradable = bool(item.text(
761 PipPackagesWidget.AvailableVersionColumn))
762 # show details for available version or installed one
763 if item.text(PipPackagesWidget.AvailableVersionColumn):
764 packageVersion = item.text(
765 PipPackagesWidget.AvailableVersionColumn)
766 else:
767 packageVersion = item.text(
768 PipPackagesWidget.InstalledVersionColumn)
769
770 self.__showPackageDetails(packageName, packageVersion,
771 upgradable=upgradable)
772
773 #######################################################################
774 ## Search widget related methods below
775 #######################################################################
776
777 def __updateSearchActionButtons(self):
778 """
779 Private method to update the action button states of the search widget.
780 """
781 installEnable = (
782 len(self.searchResultList.selectedItems()) > 0 and
783 self.environmentsComboBox.currentIndex() > 0 and
784 self.__isPipAvailable()
785 )
786 self.installButton.setEnabled(installEnable)
787 self.installUserSiteButton.setEnabled(installEnable)
788
789 self.showDetailsButton.setEnabled(
790 len(self.searchResultList.selectedItems()) == 1 and
791 self.__isPipAvailable()
792 )
793
794 def __updateSearchButton(self):
795 """
796 Private method to update the state of the search button.
797 """
798 self.searchButton.setEnabled(
799 bool(self.searchEditName.text()) and
800 self.__isPipAvailable()
801 )
802
803 def __updateSearchMoreButton(self, enable):
804 """
805 Private method to update the state of the search more button.
806
807 @param enable flag indicating the desired enable state
808 @type bool
809 """
810 self.searchMoreButton.setEnabled(
811 enable and
812 bool(self.searchEditName.text()) and
813 self.__isPipAvailable()
814 )
815
816 @pyqtSlot(bool)
817 def on_searchToggleButton_toggled(self, checked):
818 """
819 Private slot to togle the search widget.
820
821 @param checked state of the search widget button
822 @type bool
823 """
824 self.searchWidget.setVisible(checked)
825
826 if checked:
827 self.searchEditName.setFocus(Qt.FocusReason.OtherFocusReason)
828 self.searchEditName.selectAll()
829
830 self.__updateSearchActionButtons()
831 self.__updateSearchButton()
832 self.__updateSearchMoreButton(False)
833
834 @pyqtSlot(str)
835 def on_searchEditName_textChanged(self, txt):
836 """
837 Private slot handling a change of the search term.
838
839 @param txt search term
840 @type str
841 """
842 self.__updateSearchButton()
843
844 @pyqtSlot()
845 def on_searchEditName_returnPressed(self):
846 """
847 Private slot initiating a search via a press of the Return key.
848 """
849 if (
850 bool(self.searchEditName.text()) and
851 self.__isPipAvailable()
852 ):
853 self.__searchFirst()
854
855 @pyqtSlot()
856 def on_searchButton_clicked(self):
857 """
858 Private slot handling a press of the search button.
859 """
860 self.__searchFirst()
861
862 @pyqtSlot()
863 def on_searchMoreButton_clicked(self):
864 """
865 Private slot handling a press of the search more button.
866 """
867 self.__search(self.__lastSearchPage + 1)
868
869 @pyqtSlot()
870 def on_searchResultList_itemSelectionChanged(self):
871 """
872 Private slot handling changes of the search result selection.
873 """
874 self.__updateSearchActionButtons()
875
876 def __searchFirst(self):
877 """
878 Private method to perform the search for packages.
879 """
880 self.searchResultList.clear()
881 self.searchInfoLabel.clear()
882
883 self.__updateSearchMoreButton(False)
884
885 self.__search()
886
887 def __search(self, page=1):
888 """
889 Private method to perform the search by calling the PyPI search URL.
890
891 @param page search page to retrieve (defaults to 1)
892 @type int (optional)
893 """
894 self.__lastSearchPage = page
895
896 self.searchButton.setEnabled(False)
897
898 searchTerm = self.searchEditName.text().strip()
899 searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode()
900 urlQuery = QUrlQuery()
901 urlQuery.addQueryItem("q", searchTerm)
902 urlQuery.addQueryItem("page", str(page))
903 url = QUrl(self.__pip.getIndexUrlSearch())
904 url.setQuery(urlQuery)
905
906 request = QNetworkRequest(QUrl(url))
907 request.setAttribute(
908 QNetworkRequest.Attribute.CacheLoadControlAttribute,
909 QNetworkRequest.CacheLoadControl.AlwaysNetwork)
910 reply = self.__pip.getNetworkAccessManager().get(request)
911 reply.finished.connect(
912 lambda: self.__searchResponse(reply))
913 self.__replies.append(reply)
914
915 def __searchResponse(self, reply):
916 """
917 Private method to extract the search result data from the response.
918
919 @param reply reference to the reply object containing the data
920 @type QNetworkReply
921 """
922 if reply in self.__replies:
923 self.__replies.remove(reply)
924
925 urlQuery = QUrlQuery(reply.url())
926 searchTerm = urlQuery.queryItemValue("q")
927
928 if reply.error() != QNetworkReply.NetworkError.NoError:
929 EricMessageBox.warning(
930 None,
931 self.tr("Search PyPI"),
932 self.tr(
933 "<p>Received an error while searching for <b>{0}</b>.</p>"
934 "<p>Error: {1}</p>"
935 ).format(searchTerm, reply.errorString())
936 )
937 reply.deleteLater()
938 return
939
940 data = bytes(reply.readAll()).decode()
941 reply.deleteLater()
942
943 results = PypiSearchResultsParser(data).getResults()
944 if results:
945 # PyPI returns max. 20 entries per page
946 if len(results) < 20:
947 msg = self.tr("%n package(s) found.", "",
948 (self.__lastSearchPage - 1) * 20 + len(results))
949 self.__updateSearchMoreButton(False)
950 else:
951 msg = self.tr("Showing first {0} packages found.").format(
952 self.__lastSearchPage * 20)
953 self.__updateSearchMoreButton(True)
954 self.searchInfoLabel.setText(msg)
955 lastItem = self.searchResultList.topLevelItem(
956 self.searchResultList.topLevelItemCount() - 1)
957 else:
958 self.__updateSearchMoreButton(False)
959 if self.__lastSearchPage == 1:
960 EricMessageBox.warning(
961 self,
962 self.tr("Search PyPI"),
963 self.tr("""<p>There were no results for <b>{0}</b>.</p>""")
964 .format(searchTerm)
965 )
966 self.searchInfoLabel.setText(
967 self.tr("""<p>There were no results for <b>{0}</b>.</p>""")
968 .format(searchTerm)
969 )
970 else:
971 EricMessageBox.warning(
972 self,
973 self.tr("Search PyPI"),
974 self.tr("""<p>There were no more results for"""
975 """ <b>{0}</b>.</p>""").format(searchTerm)
976 )
977 lastItem = None
978
979 wrapper = textwrap.TextWrapper(width=80)
980 for result in results:
981 try:
982 description = "\n".join([
983 wrapper.fill(line) for line in
984 result['description'].strip().splitlines()
985 ])
986 except KeyError:
987 description = ""
988 date = (
989 result["released"]
990 if "released" in result else
991 result["created"]
992 )
993 itm = QTreeWidgetItem(
994 self.searchResultList, [
995 result['name'].strip(),
996 result['version'],
997 date.strip(),
998 description,
999 ])
1000 itm.setData(0, self.SearchVersionRole, result['version'])
1001
1002 if lastItem:
1003 self.searchResultList.scrollToItem(
1004 lastItem,
1005 QAbstractItemView.ScrollHint.PositionAtTop)
1006
1007 header = self.searchResultList.header()
1008 header.setStretchLastSection(False)
1009 header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
1010 headerSize = 0
1011 for col in range(header.count()):
1012 headerSize += header.sectionSize(col)
1013 if headerSize < header.width():
1014 header.setStretchLastSection(True)
1015
1016 self.__finishSearch()
1017
1018 def __finishSearch(self):
1019 """
1020 Private slot performing the search finishing actions.
1021 """
1022 self.__updateSearchActionButtons()
1023 self.__updateSearchButton()
1024
1025 self.searchEditName.setFocus(Qt.FocusReason.OtherFocusReason)
1026
1027 @pyqtSlot()
1028 def on_installButton_clicked(self):
1029 """
1030 Private slot to handle pressing the Install button..
1031 """
1032 packages = [
1033 itm.text(0).strip()
1034 for itm in self.searchResultList.selectedItems()
1035 ]
1036 self.executeInstallPackages(packages)
1037
1038 @pyqtSlot()
1039 def on_installUserSiteButton_clicked(self):
1040 """
1041 Private slot to handle pressing the Install to User-Site button..
1042 """
1043 packages = [
1044 itm.text(0).strip()
1045 for itm in self.searchResultList.selectedItems()
1046 ]
1047 self.executeInstallPackages(packages, userSite=True)
1048
1049 def executeInstallPackages(self, packages, userSite=False):
1050 """
1051 Public method to install the given list of packages.
1052
1053 @param packages list of package names to be installed
1054 @type list of str
1055 @param userSite flag indicating to install to the user directory
1056 @type bool
1057 """
1058 venvName = self.environmentsComboBox.currentText()
1059 if venvName and packages:
1060 self.__pip.installPackages(packages, venvName=venvName,
1061 userSite=userSite)
1062 self.on_refreshButton_clicked()
1063
1064 @pyqtSlot()
1065 def on_showDetailsButton_clicked(self):
1066 """
1067 Private slot to handle pressing the Show Details button.
1068 """
1069 self.__showSearchedDetails()
1070
1071 @pyqtSlot(QTreeWidgetItem, int)
1072 def on_searchResultList_itemActivated(self, item, column):
1073 """
1074 Private slot reacting on an search result item activation.
1075
1076 @param item reference to the activated item
1077 @type QTreeWidgetItem
1078 @param column activated column
1079 @type int
1080 """
1081 self.__showSearchedDetails(item)
1082
1083 def __showSearchedDetails(self, item=None):
1084 """
1085 Private slot to show details about the selected search result package.
1086
1087 @param item reference to the search result item to show details for
1088 @type QTreeWidgetItem
1089 """
1090 self.showDetailsButton.setEnabled(False)
1091
1092 if not item:
1093 item = self.searchResultList.selectedItems()[0]
1094
1095 packageVersion = item.data(0, self.SearchVersionRole)
1096 packageName = item.text(0)
1097
1098 self.__showPackageDetails(packageName, packageVersion,
1099 installable=True)
1100
1101 def __showPackageDetails(self, packageName, packageVersion,
1102 upgradable=False, installable=False):
1103 """
1104 Private method to populate the package details dialog.
1105
1106 @param packageName name of the package to show details for
1107 @type str
1108 @param packageVersion version of the package
1109 @type str
1110 @param upgradable flag indicating that the package may be upgraded
1111 (defaults to False)
1112 @type bool (optional)
1113 @param installable flag indicating that the package may be installed
1114 (defaults to False)
1115 @type bool (optional)
1116 """
1117 with EricOverrideCursor():
1118 packageData = self.__pip.getPackageDetails(
1119 packageName, packageVersion)
1120
1121 if packageData:
1122 from .PipPackageDetailsDialog import PipPackageDetailsDialog
1123
1124 self.showDetailsButton.setEnabled(True)
1125
1126 if installable:
1127 buttonsMode = PipPackageDetailsDialog.ButtonInstall
1128 elif upgradable:
1129 buttonsMode = (
1130 PipPackageDetailsDialog.ButtonRemove |
1131 PipPackageDetailsDialog.ButtonUpgrade
1132 )
1133 else:
1134 buttonsMode = PipPackageDetailsDialog.ButtonRemove
1135
1136 if self.__packageDetailsDialog is not None:
1137 self.__packageDetailsDialog.close()
1138
1139 self.__packageDetailsDialog = (
1140 PipPackageDetailsDialog(packageData, buttonsMode=buttonsMode,
1141 parent=self)
1142 )
1143 self.__packageDetailsDialog.show()
1144 else:
1145 EricMessageBox.warning(
1146 self,
1147 self.tr("Search PyPI"),
1148 self.tr("""<p>No package details info for <b>{0}</b>"""
1149 """ available.</p>""").format(packageName))
1150
1151 #######################################################################
1152 ## Menu related methods below
1153 #######################################################################
1154
1155 def __initPipMenu(self):
1156 """
1157 Private method to create the super menu and attach it to the super
1158 menu button.
1159 """
1160 self.__pipMenu = QMenu()
1161 self.__installPipAct = self.__pipMenu.addAction(
1162 self.tr("Install Pip"),
1163 self.__installPip)
1164 self.__installPipUserAct = self.__pipMenu.addAction(
1165 self.tr("Install Pip to User-Site"),
1166 self.__installPipUser)
1167 self.__repairPipAct = self.__pipMenu.addAction(
1168 self.tr("Repair Pip"),
1169 self.__repairPip)
1170 self.__pipMenu.addSeparator()
1171 self.__installPackagesAct = self.__pipMenu.addAction(
1172 self.tr("Install Packages"),
1173 self.__installPackages)
1174 self.__installLocalPackageAct = self.__pipMenu.addAction(
1175 self.tr("Install Local Package"),
1176 self.__installLocalPackage)
1177 self.__pipMenu.addSeparator()
1178 self.__installRequirementsAct = self.__pipMenu.addAction(
1179 self.tr("Install Requirements"),
1180 self.__installRequirements)
1181 self.__reinstallPackagesAct = self.__pipMenu.addAction(
1182 self.tr("Re-Install Selected Packages"),
1183 self.__reinstallPackages)
1184 self.__uninstallRequirementsAct = self.__pipMenu.addAction(
1185 self.tr("Uninstall Requirements"),
1186 self.__uninstallRequirements)
1187 self.__generateRequirementsAct = self.__pipMenu.addAction(
1188 self.tr("Generate Requirements..."),
1189 self.__generateRequirements)
1190 self.__pipMenu.addSeparator()
1191 self.__showLicensesDialogAct = self.__pipMenu.addAction(
1192 self.tr("Show Licenses..."),
1193 self.__showLicensesDialog)
1194 self.__pipMenu.addSeparator()
1195 self.__checkVulnerabilityAct = self.__pipMenu.addAction(
1196 self.tr("Check Vulnerabilities"),
1197 self.__updateVulnerabilityData)
1198 # updateVulnerabilityDbAct
1199 self.__pipMenu.addAction(
1200 self.tr("Update Vulnerability Database"),
1201 self.__updateVulnerabilityDbCache)
1202 self.__pipMenu.addSeparator()
1203 self.__cyclonedxAct = self.__pipMenu.addAction(
1204 self.tr("Create SBOM file"),
1205 self.__createSBOMFile)
1206 self.__pipMenu.addSeparator()
1207 self.__cacheInfoAct = self.__pipMenu.addAction(
1208 self.tr("Show Cache Info..."),
1209 self.__showCacheInfo)
1210 self.__cacheShowListAct = self.__pipMenu.addAction(
1211 self.tr("Show Cached Files..."),
1212 self.__showCacheList)
1213 self.__cacheRemoveAct = self.__pipMenu.addAction(
1214 self.tr("Remove Cached Files..."),
1215 self.__removeCachedFiles)
1216 self.__cachePurgeAct = self.__pipMenu.addAction(
1217 self.tr("Purge Cache..."),
1218 self.__purgeCache)
1219 self.__pipMenu.addSeparator()
1220 # editUserConfigAct
1221 self.__pipMenu.addAction(
1222 self.tr("Edit User Configuration..."),
1223 self.__editUserConfiguration)
1224 self.__editVirtualenvConfigAct = self.__pipMenu.addAction(
1225 self.tr("Edit Environment Configuration..."),
1226 self.__editVirtualenvConfiguration)
1227 self.__pipMenu.addSeparator()
1228 # pipConfigAct
1229 self.__pipMenu.addAction(
1230 self.tr("Configure..."),
1231 self.__pipConfigure)
1232
1233 self.__pipMenu.aboutToShow.connect(self.__aboutToShowPipMenu)
1234
1235 self.pipMenuButton.setMenu(self.__pipMenu)
1236
1237 def __aboutToShowPipMenu(self):
1238 """
1239 Private slot to set the action enabled status.
1240 """
1241 enable = bool(self.environmentsComboBox.currentText())
1242 enablePip = self.__isPipAvailable()
1243 enablePipCache = self.__availablePipVersion() >= (20, 1, 0)
1244
1245 self.__installPipAct.setEnabled(not enablePip)
1246 self.__installPipUserAct.setEnabled(not enablePip)
1247 self.__repairPipAct.setEnabled(enablePip)
1248
1249 self.__installPackagesAct.setEnabled(enablePip)
1250 self.__installLocalPackageAct.setEnabled(enablePip)
1251 self.__reinstallPackagesAct.setEnabled(enablePip)
1252
1253 self.__installRequirementsAct.setEnabled(enablePip)
1254 self.__uninstallRequirementsAct.setEnabled(enablePip)
1255 self.__generateRequirementsAct.setEnabled(enablePip)
1256
1257 self.__cacheInfoAct.setEnabled(enablePipCache)
1258 self.__cacheShowListAct.setEnabled(enablePipCache)
1259 self.__cacheRemoveAct.setEnabled(enablePipCache)
1260 self.__cachePurgeAct.setEnabled(enablePipCache)
1261
1262 self.__editVirtualenvConfigAct.setEnabled(enable)
1263
1264 self.__checkVulnerabilityAct.setEnabled(
1265 enable & self.vulnerabilityCheckBox.isEnabled())
1266
1267 self.__cyclonedxAct.setEnabled(enable)
1268
1269 self.__showLicensesDialogAct.setEnabled(enable)
1270
1271 @pyqtSlot()
1272 def __installPip(self):
1273 """
1274 Private slot to install pip into the selected environment.
1275 """
1276 venvName = self.environmentsComboBox.currentText()
1277 if venvName:
1278 self.__pip.installPip(venvName)
1279 self.on_refreshButton_clicked()
1280
1281 @pyqtSlot()
1282 def __installPipUser(self):
1283 """
1284 Private slot to install pip into the user site for the selected
1285 environment.
1286 """
1287 venvName = self.environmentsComboBox.currentText()
1288 if venvName:
1289 self.__pip.installPip(venvName, userSite=True)
1290 self.on_refreshButton_clicked()
1291
1292 @pyqtSlot()
1293 def __repairPip(self):
1294 """
1295 Private slot to repair the pip installation of the selected
1296 environment.
1297 """
1298 venvName = self.environmentsComboBox.currentText()
1299 if venvName:
1300 self.__pip.repairPip(venvName)
1301 self.on_refreshButton_clicked()
1302
1303 @pyqtSlot()
1304 def __installPackages(self):
1305 """
1306 Private slot to install packages to be given by the user.
1307 """
1308 venvName = self.environmentsComboBox.currentText()
1309 if venvName:
1310 from .PipPackagesInputDialog import PipPackagesInputDialog
1311 dlg = PipPackagesInputDialog(self, self.tr("Install Packages"))
1312 if dlg.exec() == QDialog.DialogCode.Accepted:
1313 packages, user = dlg.getData()
1314 self.executeInstallPackages(packages, userSite=user)
1315
1316 @pyqtSlot()
1317 def __installLocalPackage(self):
1318 """
1319 Private slot to install a package available on local storage.
1320 """
1321 venvName = self.environmentsComboBox.currentText()
1322 if venvName:
1323 from .PipFileSelectionDialog import PipFileSelectionDialog
1324 dlg = PipFileSelectionDialog(self, "package")
1325 if dlg.exec() == QDialog.DialogCode.Accepted:
1326 package, user = dlg.getData()
1327 if package and os.path.exists(package):
1328 self.executeInstallPackages([package], userSite=user)
1329
1330 @pyqtSlot()
1331 def __reinstallPackages(self):
1332 """
1333 Private slot to force a re-installation of the selected packages.
1334 """
1335 packages = [itm.text(PipPackagesWidget.PackageColumn)
1336 for itm in self.packagesList.selectedItems()]
1337 venvName = self.environmentsComboBox.currentText()
1338 if venvName and packages:
1339 self.__pip.installPackages(packages, venvName=venvName,
1340 forceReinstall=True)
1341 self.on_refreshButton_clicked()
1342
1343 @pyqtSlot()
1344 def __installRequirements(self):
1345 """
1346 Private slot to install packages as given in a requirements file.
1347 """
1348 venvName = self.environmentsComboBox.currentText()
1349 if venvName:
1350 self.__pip.installRequirements(venvName)
1351 self.on_refreshButton_clicked()
1352
1353 @pyqtSlot()
1354 def __uninstallRequirements(self):
1355 """
1356 Private slot to uninstall packages as given in a requirements file.
1357 """
1358 venvName = self.environmentsComboBox.currentText()
1359 if venvName:
1360 self.__pip.uninstallRequirements(venvName)
1361 self.on_refreshButton_clicked()
1362
1363 @pyqtSlot()
1364 def __generateRequirements(self):
1365 """
1366 Private slot to generate the contents for a requirements file.
1367 """
1368 venvName = self.environmentsComboBox.currentText()
1369 if venvName:
1370 from .PipFreezeDialog import PipFreezeDialog
1371 self.__freezeDialog = PipFreezeDialog(self.__pip, self)
1372 self.__freezeDialog.show()
1373 self.__freezeDialog.start(venvName)
1374
1375 @pyqtSlot()
1376 def __editUserConfiguration(self):
1377 """
1378 Private slot to edit the user configuration.
1379 """
1380 self.__editConfiguration()
1381
1382 @pyqtSlot()
1383 def __editVirtualenvConfiguration(self):
1384 """
1385 Private slot to edit the configuration of the selected environment.
1386 """
1387 venvName = self.environmentsComboBox.currentText()
1388 if venvName:
1389 self.__editConfiguration(venvName=venvName)
1390
1391 def __editConfiguration(self, venvName=""):
1392 """
1393 Private method to edit a configuration.
1394
1395 @param venvName name of the environment to act upon
1396 @type str
1397 """
1398 from QScintilla.MiniEditor import MiniEditor
1399 if venvName:
1400 cfgFile = self.__pip.getVirtualenvConfig(venvName)
1401 if not cfgFile:
1402 return
1403 else:
1404 cfgFile = self.__pip.getUserConfig()
1405 cfgDir = os.path.dirname(cfgFile)
1406 if not cfgDir:
1407 EricMessageBox.critical(
1408 None,
1409 self.tr("Edit Configuration"),
1410 self.tr("""No valid configuration path determined."""
1411 """ Aborting"""))
1412 return
1413
1414 try:
1415 if not os.path.isdir(cfgDir):
1416 os.makedirs(cfgDir)
1417 except OSError:
1418 EricMessageBox.critical(
1419 None,
1420 self.tr("Edit Configuration"),
1421 self.tr("""No valid configuration path determined."""
1422 """ Aborting"""))
1423 return
1424
1425 if not os.path.exists(cfgFile):
1426 with contextlib.suppress(OSError), open(cfgFile, "w") as f:
1427 f.write("[global]\n")
1428
1429 # check, if the destination is writeable
1430 if not os.access(cfgFile, os.W_OK):
1431 EricMessageBox.critical(
1432 None,
1433 self.tr("Edit Configuration"),
1434 self.tr("""No valid configuration path determined."""
1435 """ Aborting"""))
1436 return
1437
1438 self.__editor = MiniEditor(cfgFile, "Properties")
1439 self.__editor.show()
1440
1441 def __pipConfigure(self):
1442 """
1443 Private slot to open the configuration page.
1444 """
1445 ericApp().getObject("UserInterface").showPreferences("pipPage")
1446
1447 @pyqtSlot()
1448 def __showCacheInfo(self):
1449 """
1450 Private slot to show information about the cache.
1451 """
1452 venvName = self.environmentsComboBox.currentText()
1453 if venvName:
1454 self.__pip.showCacheInfo(venvName)
1455
1456 @pyqtSlot()
1457 def __showCacheList(self):
1458 """
1459 Private slot to show a list of cached files.
1460 """
1461 venvName = self.environmentsComboBox.currentText()
1462 if venvName:
1463 self.__pip.cacheList(venvName)
1464
1465 @pyqtSlot()
1466 def __removeCachedFiles(self):
1467 """
1468 Private slot to remove files from the pip cache.
1469 """
1470 venvName = self.environmentsComboBox.currentText()
1471 if venvName:
1472 self.__pip.cacheRemove(venvName)
1473
1474 @pyqtSlot()
1475 def __purgeCache(self):
1476 """
1477 Private slot to empty the pip cache.
1478 """
1479 venvName = self.environmentsComboBox.currentText()
1480 if venvName:
1481 self.__pip.cachePurge(venvName)
1482
1483 ##################################################################
1484 ## Interface to the vulnerability checks below
1485 ##################################################################
1486
1487 @pyqtSlot(bool)
1488 def on_vulnerabilityCheckBox_clicked(self, checked):
1489 """
1490 Private slot handling a change of the automatic vulnerability checks.
1491
1492 @param checked flag indicating the state of the check box
1493 @type bool
1494 """
1495 if checked:
1496 self.__updateVulnerabilityData(clearFirst=True)
1497
1498 self.packagesList.header().setSectionHidden(
1499 PipPackagesWidget.VulnerabilityColumn, not checked)
1500
1501 @pyqtSlot()
1502 def __clearVulnerabilityInfo(self):
1503 """
1504 Private slot to clear the vulnerability info.
1505 """
1506 for row in range(self.packagesList.topLevelItemCount()):
1507 itm = self.packagesList.topLevelItem(row)
1508 itm.setText(PipPackagesWidget.VulnerabilityColumn, "")
1509 itm.setToolTip(PipPackagesWidget.VulnerabilityColumn, "")
1510 itm.setIcon(PipPackagesWidget.VulnerabilityColumn, QIcon())
1511 itm.setData(PipPackagesWidget.VulnerabilityColumn,
1512 PipPackagesWidget.VulnerabilityRole,
1513 None)
1514
1515 @pyqtSlot()
1516 def __updateVulnerabilityData(self, clearFirst=True):
1517 """
1518 Private slot to update the shown vulnerability info.
1519
1520 @param clearFirst flag indicating to clear the vulnerability info first
1521 (defaults to True)
1522 @type bool (optional)
1523 """
1524 if clearFirst:
1525 self.__clearVulnerabilityInfo()
1526
1527 packages = []
1528 for row in range(self.packagesList.topLevelItemCount()):
1529 itm = self.packagesList.topLevelItem(row)
1530 packages.append(Package(
1531 name=itm.text(PipPackagesWidget.PackageColumn),
1532 version=itm.text(PipPackagesWidget.InstalledVersionColumn)
1533 ))
1534
1535 error, vulnerabilities = (
1536 self.__pip.getVulnerabilityChecker().check(packages)
1537 )
1538 if error == VulnerabilityCheckError.OK:
1539 for package in vulnerabilities:
1540 items = self.packagesList.findItems(
1541 package,
1542 Qt.MatchFlag.MatchExactly |
1543 Qt.MatchFlag.MatchCaseSensitive
1544 )
1545 if items:
1546 itm = items[0]
1547 itm.setData(
1548 PipPackagesWidget.VulnerabilityColumn,
1549 PipPackagesWidget.VulnerabilityRole,
1550 vulnerabilities[package]
1551 )
1552 affected = {v.spec for v in vulnerabilities[package]}
1553 itm.setText(
1554 PipPackagesWidget.VulnerabilityColumn,
1555 ', '.join(affected)
1556 )
1557 itm.setIcon(
1558 PipPackagesWidget.VulnerabilityColumn,
1559 UI.PixmapCache.getIcon("securityLow")
1560 )
1561
1562 elif error in (VulnerabilityCheckError.FullDbUnavailable,
1563 VulnerabilityCheckError.SummaryDbUnavailable):
1564 self.vulnerabilityCheckBox.setChecked(False)
1565 self.vulnerabilityCheckBox.setEnabled(False)
1566 self.packagesList.setColumnHidden(
1567 PipPackagesWidget.VulnerabilityColumn, True)
1568
1569 @pyqtSlot()
1570 def __updateVulnerabilityDbCache(self):
1571 """
1572 Private slot to initiate an update of the local cache of the
1573 vulnerability database.
1574 """
1575 with EricOverrideCursor():
1576 self.__pip.getVulnerabilityChecker().updateVulnerabilityDb()
1577
1578 def __showVulnerabilityInformation(self, packageName, packageVersion,
1579 vulnerabilities):
1580 """
1581 Private method to show the detected vulnerability data.
1582
1583 @param packageName name of the package
1584 @type str
1585 @param packageVersion installed version number
1586 @type str
1587 @param vulnerabilities list of vulnerabilities
1588 @type list of Vulnerability
1589 """
1590 header = (
1591 self.tr("{0} {1}", "package name, package version")
1592 .format(packageName, packageVersion)
1593 )
1594 topItem = QTreeWidgetItem(self.infoWidget, [header])
1595 topItem.setFirstColumnSpanned(True)
1596 topItem.setExpanded(True)
1597 font = topItem.font(0)
1598 font.setBold(True)
1599 topItem.setFont(0, font)
1600
1601 for vulnerability in vulnerabilities:
1602 title = (
1603 vulnerability.cve
1604 if vulnerability.cve else
1605 vulnerability.vulnerabilityId
1606 )
1607 titleItem = QTreeWidgetItem(topItem, [title])
1608 titleItem.setFirstColumnSpanned(True)
1609 titleItem.setExpanded(True)
1610
1611 QTreeWidgetItem(
1612 titleItem,
1613 [self.tr("Affected Version:"), vulnerability.spec])
1614 itm = QTreeWidgetItem(
1615 titleItem,
1616 [self.tr("Advisory:"), vulnerability.advisory])
1617 itm.setToolTip(1, "<p>{0}</p>".format(
1618 vulnerability.advisory.replace("\r\n", "<br/>")
1619 ))
1620
1621 self.infoWidget.scrollToTop()
1622 self.infoWidget.resizeColumnToContents(0)
1623
1624 header = self.infoWidget.header()
1625 header.setStretchLastSection(True)
1626
1627 #######################################################################
1628 ## Dependency tree related methods below
1629 #######################################################################
1630
1631 @pyqtSlot(bool)
1632 def on_viewToggleButton_toggled(self, checked):
1633 """
1634 Private slot handling the view selection.
1635
1636 @param checked state of the toggle button
1637 @type bool
1638 """
1639 if checked:
1640 self.viewsStackWidget.setCurrentWidget(
1641 self.dependenciesPage)
1642 self.__refreshDependencyTree()
1643 else:
1644 self.viewsStackWidget.setCurrentWidget(
1645 self.packagesPage)
1646 self.__refreshPackagesList()
1647
1648 @pyqtSlot(bool)
1649 def on_requiresButton_toggled(self, checked):
1650 """
1651 Private slot handling the selection of the view type.
1652
1653 @param checked state of the radio button (unused)
1654 @type bool
1655 """
1656 self.__refreshDependencyTree()
1657
1658 @pyqtSlot()
1659 def on_localDepCheckBox_clicked(self):
1660 """
1661 Private slot handling the switching of the local mode.
1662 """
1663 self.__refreshDependencyTree()
1664
1665 @pyqtSlot()
1666 def on_userDepCheckBox_clicked(self):
1667 """
1668 Private slot handling the switching of the 'user-site' mode.
1669 """
1670 self.__refreshDependencyTree()
1671
1672 def __refreshDependencyTree(self):
1673 """
1674 Private method to refresh the dependency tree.
1675 """
1676 self.dependenciesList.clear()
1677 venvName = self.environmentsComboBox.currentText()
1678 if venvName:
1679 interpreter = self.__pip.getVirtualenvInterpreter(venvName)
1680 if interpreter:
1681 with EricOverrideCursor():
1682 dependencies = self.__pip.getDependencyTree(
1683 venvName,
1684 localPackages=self.localDepCheckBox.isChecked(),
1685 usersite=self.userDepCheckBox.isChecked(),
1686 reverse=self.requiredByButton.isChecked(),
1687 )
1688
1689 self.dependenciesList.setUpdatesEnabled(False)
1690 for dependency in dependencies:
1691 self.__addDependency(dependency, self.dependenciesList)
1692
1693 self.dependenciesList.sortItems(
1694 PipPackagesWidget.DepPackageColumn,
1695 Qt.SortOrder.AscendingOrder)
1696 for col in range(self.dependenciesList.columnCount()):
1697 self.dependenciesList.resizeColumnToContents(col)
1698 self.dependenciesList.setUpdatesEnabled(True)
1699
1700 self.__updateDepActionButtons()
1701
1702 def __addDependency(self, dependency, parent):
1703 """
1704 Private method to add a dependency branch to a given parent.
1705
1706 @param dependency dependency to be added
1707 @type dict
1708 @param parent reference to the parent item
1709 @type QTreeWidget or QTreeWidgetItem
1710 """
1711 itm = QTreeWidgetItem(parent, [
1712 dependency["package_name"],
1713 dependency["installed_version"],
1714 dependency["required_version"],
1715 ])
1716 itm.setExpanded(True)
1717
1718 if dependency["installed_version"] == "?":
1719 itm.setText(PipPackagesWidget.DepInstalledVersionColumn,
1720 self.tr("unknown"))
1721
1722 if dependency["required_version"].lower() not in ("any", "?"):
1723 spec = (
1724 "=={0}".format(dependency["required_version"])
1725 if dependency["required_version"][0] in "0123456789" else
1726 dependency["required_version"]
1727 )
1728 specifierSet = SpecifierSet(specifiers=spec)
1729 if not specifierSet.contains(dependency["installed_version"]):
1730 itm.setIcon(PipPackagesWidget.DepRequiredVersionColumn,
1731 UI.PixmapCache.getIcon("warning"))
1732
1733 elif dependency["required_version"].lower() == "any":
1734 itm.setText(PipPackagesWidget.DepRequiredVersionColumn,
1735 self.tr("any"))
1736
1737 elif dependency["required_version"] == "?":
1738 itm.setText(PipPackagesWidget.DepRequiredVersionColumn,
1739 self.tr("unknown"))
1740
1741 # recursively add sub-dependencies
1742 for dep in dependency["dependencies"]:
1743 self.__addDependency(dep, itm)
1744
1745 @pyqtSlot(QTreeWidgetItem, int)
1746 def on_dependenciesList_itemActivated(self, item, column):
1747 """
1748 Private slot reacting on a package item of the dependency tree being
1749 activated.
1750
1751 @param item reference to the activated item
1752 @type QTreeWidgetItem
1753 @param column activated column
1754 @type int
1755 """
1756 packageName = item.text(PipPackagesWidget.DepPackageColumn)
1757 packageVersion = item.text(
1758 PipPackagesWidget.DepInstalledVersionColumn)
1759
1760 self.__showPackageDetails(packageName, packageVersion)
1761
1762 @pyqtSlot()
1763 def on_dependenciesList_itemSelectionChanged(self):
1764 """
1765 Private slot reacting on a change of selected items of the dependency
1766 tree.
1767 """
1768 if len(self.dependenciesList.selectedItems()) == 0:
1769 self.dependencyInfoWidget.clear()
1770
1771 @pyqtSlot(QTreeWidgetItem, int)
1772 def on_dependenciesList_itemPressed(self, item, column):
1773 """
1774 Private slot reacting on a package item of the dependency tree being
1775 pressed.
1776
1777 @param item reference to the pressed item
1778 @type QTreeWidgetItem
1779 @param column pressed column
1780 @type int
1781 """
1782 self.dependencyInfoWidget.clear()
1783
1784 if item is not None:
1785 self.__showPackageInformation(
1786 item.text(PipPackagesWidget.DepPackageColumn),
1787 self.dependencyInfoWidget
1788 )
1789
1790 self.__updateDepActionButtons()
1791
1792 @pyqtSlot()
1793 def on_refreshDependenciesButton_clicked(self):
1794 """
1795 Private slot to refresh the dependency tree.
1796 """
1797 currentEnvironment = self.environmentsComboBox.currentText()
1798 self.environmentsComboBox.clear()
1799 self.dependenciesList.clear()
1800
1801 with EricOverrideCursor():
1802 self.__populateEnvironments()
1803
1804 index = self.environmentsComboBox.findText(
1805 currentEnvironment,
1806 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
1807 )
1808 if index != -1:
1809 self.environmentsComboBox.setCurrentIndex(index)
1810
1811 self.__updateDepActionButtons()
1812
1813 @pyqtSlot()
1814 def on_showDepPackageDetailsButton_clicked(self):
1815 """
1816 Private slot to show information for the selected package of the
1817 dependency tree.
1818 """
1819 item = self.dependenciesList.selectedItems()[0]
1820 if item:
1821 packageName = item.text(PipPackagesWidget.DepPackageColumn)
1822 packageVersion = item.text(
1823 PipPackagesWidget.DepInstalledVersionColumn)
1824
1825 self.__showPackageDetails(packageName, packageVersion)
1826
1827 def __updateDepActionButtons(self):
1828 """
1829 Private method to set the state of the dependency page action buttons.
1830 """
1831 self.showDepPackageDetailsButton.setEnabled(
1832 len(self.dependenciesList.selectedItems()) == 1 and
1833 self.__isPipAvailable()
1834 )
1835
1836 ##################################################################
1837 ## Interface to show the licenses dialog below
1838 ##################################################################
1839
1840 @pyqtSlot()
1841 def __showLicensesDialog(self):
1842 """
1843 Private slot to show a dialog with the licenses of the selected
1844 environment.
1845 """
1846 from .PipLicensesDialog import PipLicensesDialog
1847
1848 environment = self.environmentsComboBox.currentText()
1849 localPackages = (
1850 self.localDepCheckBox.isChecked()
1851 if self.viewToggleButton.isChecked() else
1852 self.localCheckBox.isChecked()
1853 )
1854 usersite = (
1855 self.userDepCheckBox.isChecked()
1856 if self.viewToggleButton.isChecked() else
1857 self.userCheckBox.isChecked()
1858 )
1859 dlg = PipLicensesDialog(
1860 self.__pip,
1861 environment,
1862 localPackages=localPackages,
1863 usersite=usersite,
1864 parent=self
1865 )
1866 dlg.exec()
1867
1868 ##################################################################
1869 ## Interface to create a SBOM file using CycloneDX
1870 ##################################################################
1871
1872 @pyqtSlot()
1873 def __createSBOMFile(self):
1874 """
1875 Private slot to create a "Software Bill Of Material" file.
1876 """
1877 import CycloneDXInterface
1878
1879 venvName = self.environmentsComboBox.currentText()
1880 if venvName == self.__pip.getProjectEnvironmentString():
1881 venvName = "<project>"
1882 CycloneDXInterface.createCycloneDXFile(venvName)

eric ide

mercurial