src/eric7/PipInterface/PipPackagesWidget.py

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

eric ide

mercurial