eric7/CondaInterface/CondaPackagesWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8257
28146736bbfc
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the conda packages management widget.
8 """
9
10 import os
11
12 from PyQt5.QtCore import pyqtSlot, Qt
13 from PyQt5.QtWidgets import (
14 QWidget, QToolButton, QMenu, QTreeWidgetItem, QApplication, QLineEdit,
15 QDialog
16 )
17
18 from E5Gui import E5FileDialog, E5MessageBox, E5TextInputDialog
19 from E5Gui.E5Application import e5App
20 from E5Gui.E5OverrideCursor import E5OverrideCursor
21
22 from .Ui_CondaPackagesWidget import Ui_CondaPackagesWidget
23
24 import UI.PixmapCache
25
26 import CondaInterface
27
28
29 class CondaPackagesWidget(QWidget, Ui_CondaPackagesWidget):
30 """
31 Class implementing the conda packages management widget.
32 """
33 # Role definition of packages list
34 PackageVersionRole = Qt.ItemDataRole.UserRole + 1
35 PackageBuildRole = Qt.ItemDataRole.UserRole + 2
36
37 # Role definitions of search results list
38 PackageDetailedDataRole = Qt.ItemDataRole.UserRole + 1
39
40 def __init__(self, conda, parent=None):
41 """
42 Constructor
43
44 @param conda reference to the conda interface
45 @type Conda
46 @param parent reference to the parent widget
47 @type QWidget
48 """
49 super().__init__(parent)
50 self.setupUi(self)
51
52 self.__conda = conda
53
54 if not CondaInterface.isCondaAvailable():
55 self.baseWidget.hide()
56 self.searchWidget.hide()
57
58 else:
59 self.notAvailableWidget.hide()
60
61 self.__initCondaInterface()
62
63 def __initCondaInterface(self):
64 """
65 Private method to initialize the conda interface elements.
66 """
67 self.statusLabel.hide()
68
69 self.condaMenuButton.setObjectName(
70 "conda_supermenu_button")
71 self.condaMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
72 self.condaMenuButton.setToolTip(self.tr("Conda Menu"))
73 self.condaMenuButton.setPopupMode(
74 QToolButton.ToolButtonPopupMode.InstantPopup)
75 self.condaMenuButton.setToolButtonStyle(
76 Qt.ToolButtonStyle.ToolButtonIconOnly)
77 self.condaMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
78 self.condaMenuButton.setAutoRaise(True)
79 self.condaMenuButton.setShowMenuInside(True)
80
81 self.refreshButton.setIcon(UI.PixmapCache.getIcon("reload"))
82 self.upgradeButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
83 self.upgradeAllButton.setIcon(UI.PixmapCache.getIcon("2uparrow"))
84 self.uninstallButton.setIcon(UI.PixmapCache.getIcon("minus"))
85 self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find"))
86 self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext"))
87 self.installButton.setIcon(UI.PixmapCache.getIcon("plus"))
88 self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info"))
89
90 if CondaInterface.condaVersion() >= (4, 4, 0):
91 self.searchOptionsWidget.hide()
92 else:
93 self.platformComboBox.addItems(sorted([
94 "", "win-32", "win-64", "osx-64", "linux-32", "linux-64",
95 ]))
96
97 self.__initCondaMenu()
98 self.__populateEnvironments()
99 self.__updateActionButtons()
100
101 self.searchWidget.hide()
102
103 self.__conda.condaEnvironmentCreated.connect(
104 self.on_refreshButton_clicked)
105 self.__conda.condaEnvironmentRemoved.connect(
106 self.on_refreshButton_clicked)
107
108 def __populateEnvironments(self):
109 """
110 Private method to get a list of environments and populate the selector.
111 """
112 environments = [("", "")] + sorted(
113 self.__conda.getCondaEnvironmentsList())
114 for environment in environments:
115 self.environmentsComboBox.addItem(environment[0], environment[1])
116
117 def __initCondaMenu(self):
118 """
119 Private method to create the super menu and attach it to the super
120 menu button.
121 """
122 self.__condaMenu = QMenu(self)
123 self.__envActs = []
124
125 self.__cleanMenu = QMenu(self.tr("Clean"), self)
126 self.__cleanMenu.addAction(
127 self.tr("All"), lambda: self.__conda.cleanConda("all"))
128 self.__cleanMenu.addAction(
129 self.tr("Cache"), lambda: self.__conda.cleanConda("index-cache"))
130 self.__cleanMenu.addAction(
131 self.tr("Lock Files"),
132 lambda: self.__conda.cleanConda("lock"))
133 self.__cleanMenu.addAction(
134 self.tr("Packages"), lambda: self.__conda.cleanConda("packages"))
135 self.__cleanMenu.addAction(
136 self.tr("Tarballs"), lambda: self.__conda.cleanConda("tarballs"))
137
138 self.__condaMenu.addAction(
139 self.tr("About Conda..."), self.__aboutConda)
140 self.__condaMenu.addSeparator()
141 self.__condaMenu.addAction(
142 self.tr("Update Conda"), self.__conda.updateConda)
143 self.__condaMenu.addSeparator()
144 self.__envActs.append(self.__condaMenu.addAction(
145 self.tr("Install Packages"), self.__installPackages))
146 self.__envActs.append(self.__condaMenu.addAction(
147 self.tr("Install Requirements"), self.__installRequirements))
148 self.__condaMenu.addSeparator()
149 self.__envActs.append(self.__condaMenu.addAction(
150 self.tr("Generate Requirements"), self.__generateRequirements))
151 self.__condaMenu.addSeparator()
152 self.__condaMenu.addAction(
153 self.tr("Create Environment from Requirements"),
154 self.__createEnvironment)
155 self.__envActs.append(self.__condaMenu.addAction(
156 self.tr("Clone Environment"), self.__cloneEnvironment))
157 self.__deleteEnvAct = self.__condaMenu.addAction(
158 self.tr("Delete Environment"), self.__deleteEnvironment)
159 self.__condaMenu.addSeparator()
160 self.__condaMenu.addMenu(self.__cleanMenu)
161 self.__condaMenu.addSeparator()
162 self.__condaMenu.addAction(
163 self.tr("Edit User Configuration..."),
164 self.__editUserConfiguration)
165 self.__condaMenu.addSeparator()
166 self.__condaMenu.addAction(
167 self.tr("Configure..."), self.__condaConfigure)
168
169 self.condaMenuButton.setMenu(self.__condaMenu)
170
171 self.__condaMenu.aboutToShow.connect(self.__aboutToShowCondaMenu)
172
173 def __selectedUpdateableItems(self):
174 """
175 Private method to get a list of selected items that can be updated.
176
177 @return list of selected items that can be updated
178 @rtype list of QTreeWidgetItem
179 """
180 return [
181 itm for itm in self.packagesList.selectedItems()
182 if bool(itm.text(2))
183 ]
184
185 def __allUpdateableItems(self):
186 """
187 Private method to get a list of all items that can be updated.
188
189 @return list of all items that can be updated
190 @rtype list of QTreeWidgetItem
191 """
192 updateableItems = []
193 for index in range(self.packagesList.topLevelItemCount()):
194 itm = self.packagesList.topLevelItem(index)
195 if itm.text(2):
196 updateableItems.append(itm)
197
198 return updateableItems
199
200 def __updateActionButtons(self):
201 """
202 Private method to set the state of the action buttons.
203 """
204 self.upgradeButton.setEnabled(
205 bool(self.__selectedUpdateableItems()))
206 self.uninstallButton.setEnabled(
207 bool(self.packagesList.selectedItems()))
208 self.upgradeAllButton.setEnabled(
209 bool(self.__allUpdateableItems()))
210
211 @pyqtSlot(int)
212 def on_environmentsComboBox_currentIndexChanged(self, index):
213 """
214 Private slot handling the selection of a conda environment.
215
216 @param index index of the selected conda environment
217 @type int
218 """
219 self.packagesList.clear()
220 prefix = self.environmentsComboBox.itemData(index)
221 if prefix:
222 self.statusLabel.show()
223 self.statusLabel.setText(self.tr("Getting installed packages..."))
224
225 with E5OverrideCursor():
226 # 1. populate with installed packages
227 self.packagesList.setUpdatesEnabled(False)
228 installedPackages = self.__conda.getInstalledPackages(
229 prefix=prefix)
230 for package, version, build in installedPackages:
231 itm = QTreeWidgetItem(self.packagesList,
232 [package, version])
233 itm.setData(1, self.PackageVersionRole, version)
234 itm.setData(1, self.PackageBuildRole, build)
235 self.packagesList.setUpdatesEnabled(True)
236 self.statusLabel.setText(
237 self.tr("Getting outdated packages..."))
238 QApplication.processEvents()
239
240 # 2. update with update information
241 self.packagesList.setUpdatesEnabled(False)
242 updateablePackages = self.__conda.getUpdateablePackages(
243 prefix=prefix)
244 for package, version, build in updateablePackages:
245 items = self.packagesList.findItems(
246 package,
247 Qt.MatchFlag.MatchExactly |
248 Qt.MatchFlag.MatchCaseSensitive)
249 if items:
250 itm = items[0]
251 itm.setText(2, version)
252 itm.setData(2, self.PackageVersionRole, version)
253 itm.setData(2, self.PackageBuildRole, build)
254 if itm.data(1, self.PackageVersionRole) == version:
255 # build must be different, show in version display
256 itm.setText(1, self.tr("{0} (Build: {1})").format(
257 itm.data(1, self.PackageVersionRole),
258 itm.data(1, self.PackageBuildRole),
259 ))
260 itm.setText(2, self.tr("{0} (Build: {1})").format(
261 itm.data(2, self.PackageVersionRole),
262 itm.data(2, self.PackageBuildRole),
263 ))
264
265 self.packagesList.sortItems(0, Qt.SortOrder.AscendingOrder)
266 for col in range(self.packagesList.columnCount()):
267 self.packagesList.resizeColumnToContents(col)
268 self.packagesList.setUpdatesEnabled(True)
269 self.statusLabel.hide()
270
271 self.__updateActionButtons()
272 self.__updateSearchActionButtons()
273
274 @pyqtSlot()
275 def on_packagesList_itemSelectionChanged(self):
276 """
277 Private slot to handle the selection of some items..
278 """
279 self.__updateActionButtons()
280
281 @pyqtSlot()
282 def on_refreshButton_clicked(self):
283 """
284 Private slot to refresh the display.
285 """
286 currentEnvironment = self.environmentsComboBox.currentText()
287 self.environmentsComboBox.clear()
288 self.packagesList.clear()
289
290 with E5OverrideCursor():
291 self.__populateEnvironments()
292
293 index = self.environmentsComboBox.findText(
294 currentEnvironment,
295 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive)
296 if index != -1:
297 self.environmentsComboBox.setCurrentIndex(index)
298
299 self.__updateActionButtons()
300
301 @pyqtSlot()
302 def on_upgradeButton_clicked(self):
303 """
304 Private slot to upgrade selected packages of the selected environment.
305 """
306 packages = [itm.text(0) for itm in self.__selectedUpdateableItems()]
307 if packages:
308 prefix = self.environmentsComboBox.itemData(
309 self.environmentsComboBox.currentIndex())
310 ok = self.__conda.updatePackages(packages, prefix=prefix)
311 if ok:
312 self.on_refreshButton_clicked()
313
314 @pyqtSlot()
315 def on_upgradeAllButton_clicked(self):
316 """
317 Private slot to upgrade all packages of the selected environment.
318 """
319 prefix = self.environmentsComboBox.itemData(
320 self.environmentsComboBox.currentIndex())
321 ok = self.__conda.updateAllPackages(prefix=prefix)
322 if ok:
323 self.on_refreshButton_clicked()
324
325 @pyqtSlot()
326 def on_uninstallButton_clicked(self):
327 """
328 Private slot to remove selected packages of the selected environment.
329 """
330 packages = [itm.text(0) for itm in self.packagesList.selectedItems()]
331 if packages:
332 prefix = self.environmentsComboBox.itemData(
333 self.environmentsComboBox.currentIndex())
334 ok = self.__conda.uninstallPackages(packages, prefix=prefix)
335 if ok:
336 self.on_refreshButton_clicked()
337
338 #######################################################################
339 ## Search widget related methods below
340 #######################################################################
341
342 def __updateSearchActionButtons(self):
343 """
344 Private method to update the action button states of the search widget.
345 """
346 enable = len(self.searchResultList.selectedItems()) == 1
347 self.installButton.setEnabled(
348 enable and self.environmentsComboBox.currentIndex() > 0)
349 self.showDetailsButton.setEnabled(
350 enable and bool(self.searchResultList.selectedItems()[0].parent()))
351
352 def __doSearch(self):
353 """
354 Private method to search for packages.
355 """
356 self.searchResultList.clear()
357 pattern = self.searchEdit.text()
358 if pattern:
359 with E5OverrideCursor():
360 prefix = (
361 ""
362 if CondaInterface.condaVersion() >= (4, 4, 0) else
363 self.environmentsComboBox.itemData(
364 self.environmentsComboBox.currentIndex())
365 )
366 ok, result = self.__conda.searchPackages(
367 pattern,
368 fullNameOnly=self.fullNameButton.isChecked(),
369 packageSpec=self.packageSpecButton.isChecked(),
370 platform=self.platformComboBox.currentText(),
371 prefix=prefix,
372 )
373
374 if ok and result:
375 self.searchResultList.setUpdatesEnabled(False)
376 for package in result:
377 itm = QTreeWidgetItem(self.searchResultList,
378 [package])
379 itm.setExpanded(False)
380 for detail in result[package]:
381 version = detail["version"]
382 build = detail["build"]
383 if "subdir" in detail:
384 platform = detail["subdir"]
385 elif "platform" in detail:
386 platform = detail["platform"]
387 else:
388 platform = ""
389 citm = QTreeWidgetItem(
390 itm, ["", version, build, platform])
391 citm.setData(0, self.PackageDetailedDataRole,
392 detail)
393
394 self.searchResultList.sortItems(
395 0, Qt.SortOrder.AscendingOrder)
396 self.searchResultList.resizeColumnToContents(0)
397 self.searchResultList.setUpdatesEnabled(True)
398 if not ok:
399 try:
400 message = result["message"]
401 except KeyError:
402 message = result["error"]
403 E5MessageBox.warning(
404 self,
405 self.tr("Conda Search Package Error"),
406 message)
407
408 def __showDetails(self, item):
409 """
410 Private method to show a dialog with details about a package item.
411
412 @param item reference to the package item
413 @type QTreeWidgetItem
414 """
415 details = item.data(0, self.PackageDetailedDataRole)
416 if details:
417 from .CondaPackageDetailsWidget import CondaPackageDetailsDialog
418 dlg = CondaPackageDetailsDialog(details, self)
419 dlg.exec()
420
421 @pyqtSlot(str)
422 def on_searchEdit_textChanged(self, txt):
423 """
424 Private slot handling changes of the entered search specification.
425
426 @param txt current search entry
427 @type str
428 """
429 self.searchButton.setEnabled(bool(txt))
430
431 @pyqtSlot()
432 def on_searchEdit_returnPressed(self):
433 """
434 Private slot handling the user pressing the Return button in the
435 search edit.
436 """
437 self.__doSearch()
438
439 @pyqtSlot()
440 def on_searchButton_clicked(self):
441 """
442 Private slot handling the press of the search button.
443 """
444 self.__doSearch()
445
446 @pyqtSlot()
447 def on_installButton_clicked(self):
448 """
449 Private slot to install a selected package.
450 """
451 if len(self.searchResultList.selectedItems()) == 1:
452 item = self.searchResultList.selectedItems()[0]
453 if item.parent() is None:
454 # it is just the package item
455 package = item.text(0)
456 else:
457 # item with version and build
458 package = "{0}={1}={2}".format(
459 item.parent().text(0),
460 item.text(1),
461 item.text(2),
462 )
463
464 prefix = self.environmentsComboBox.itemData(
465 self.environmentsComboBox.currentIndex())
466 ok = self.__conda.installPackages([package], prefix=prefix)
467 if ok:
468 self.on_refreshButton_clicked()
469
470 @pyqtSlot()
471 def on_showDetailsButton_clicked(self):
472 """
473 Private slot handling the 'Show Details' button.
474 """
475 item = self.searchResultList.selectedItems()[0]
476 self.__showDetails(item)
477
478 @pyqtSlot()
479 def on_searchResultList_itemSelectionChanged(self):
480 """
481 Private slot handling a change of selected search results.
482 """
483 self.__updateSearchActionButtons()
484
485 @pyqtSlot(QTreeWidgetItem)
486 def on_searchResultList_itemExpanded(self, item):
487 """
488 Private slot handling the expansion of an item.
489
490 @param item reference to the expanded item
491 @type QTreeWidgetItem
492 """
493 for col in range(1, self.searchResultList.columnCount()):
494 self.searchResultList.resizeColumnToContents(col)
495
496 @pyqtSlot(QTreeWidgetItem, int)
497 def on_searchResultList_itemDoubleClicked(self, item, column):
498 """
499 Private slot handling a double click of an item.
500
501 @param item reference to the item that was double clicked
502 @type QTreeWidgetItem
503 @param column column of the double click
504 @type int
505 """
506 if item.parent() is not None:
507 self.__showDetails(item)
508
509 @pyqtSlot(bool)
510 def on_searchToggleButton_toggled(self, checked):
511 """
512 Private slot to togle the search widget.
513
514 @param checked state of the search widget button
515 @type bool
516 """
517 self.searchWidget.setVisible(checked)
518
519 if checked:
520 self.searchEdit.setFocus(Qt.FocusReason.OtherFocusReason)
521 self.searchEdit.selectAll()
522
523 self.__updateSearchActionButtons()
524
525 #######################################################################
526 ## Menu related methods below
527 #######################################################################
528
529 @pyqtSlot()
530 def __aboutToShowCondaMenu(self):
531 """
532 Private slot to handle the conda menu about to be shown.
533 """
534 selectedEnvironment = self.environmentsComboBox.currentText()
535 enable = selectedEnvironment not in [""]
536 for act in self.__envActs:
537 act.setEnabled(enable)
538
539 self.__deleteEnvAct.setEnabled(
540 selectedEnvironment not in ["", self.__conda.RootName])
541
542 @pyqtSlot()
543 def __aboutConda(self):
544 """
545 Private slot to show some information about the conda installation.
546 """
547 infoDict = self.__conda.getCondaInformation()
548
549 from .CondaInfoDialog import CondaInfoDialog
550 dlg = CondaInfoDialog(infoDict, self)
551 dlg.exec()
552
553 @pyqtSlot()
554 def __installPackages(self):
555 """
556 Private slot to install packages.
557 """
558 prefix = self.environmentsComboBox.itemData(
559 self.environmentsComboBox.currentIndex())
560 if prefix:
561 ok, packageSpecs = E5TextInputDialog.getText(
562 self,
563 self.tr("Install Packages"),
564 self.tr("Package Specifications (separated by whitespace):"),
565 QLineEdit.EchoMode.Normal,
566 minimumWidth=600)
567 if ok and packageSpecs.strip():
568 packages = [p.strip() for p in packageSpecs.split()]
569 ok = self.__conda.installPackages(packages, prefix=prefix)
570 if ok:
571 self.on_refreshButton_clicked()
572
573 @pyqtSlot()
574 def __installRequirements(self):
575 """
576 Private slot to install packages from requirements files.
577 """
578 prefix = self.environmentsComboBox.itemData(
579 self.environmentsComboBox.currentIndex())
580 if prefix:
581 requirements = E5FileDialog.getOpenFileNames(
582 self,
583 self.tr("Install Packages"),
584 "",
585 self.tr("Text Files (*.txt);;All Files (*)"))
586 if requirements:
587 args = []
588 for requirement in requirements:
589 args.extend(["--file", requirement])
590 ok = self.__conda.installPackages(args, prefix=prefix)
591 if ok:
592 self.on_refreshButton_clicked()
593
594 @pyqtSlot()
595 def __generateRequirements(self):
596 """
597 Private slot to generate a requirements file.
598 """
599 prefix = self.environmentsComboBox.itemData(
600 self.environmentsComboBox.currentIndex())
601 if prefix:
602 env = self.environmentsComboBox.currentText()
603
604 from .CondaExportDialog import CondaExportDialog
605
606 self.__requirementsDialog = CondaExportDialog(
607 self.__conda, env, prefix)
608 self.__requirementsDialog.show()
609 QApplication.processEvents()
610 self.__requirementsDialog.start()
611
612 @pyqtSlot()
613 def __cloneEnvironment(self):
614 """
615 Private slot to clone a conda environment.
616 """
617 from .CondaNewEnvironmentDataDialog import (
618 CondaNewEnvironmentDataDialog)
619
620 prefix = self.environmentsComboBox.itemData(
621 self.environmentsComboBox.currentIndex())
622 if prefix:
623 dlg = CondaNewEnvironmentDataDialog(self.tr("Clone Environment"),
624 False, self)
625 if dlg.exec() == QDialog.DialogCode.Accepted:
626 virtEnvName, envName, _ = dlg.getData()
627 args = [
628 "--name",
629 envName.strip(),
630 "--clone",
631 prefix,
632 ]
633 ok, prefix, interpreter = self.__conda.createCondaEnvironment(
634 args)
635 if ok:
636 e5App().getObject("VirtualEnvManager").addVirtualEnv(
637 virtEnvName, prefix, interpreter, isConda=True)
638
639 @pyqtSlot()
640 def __createEnvironment(self):
641 """
642 Private slot to create a conda environment from a requirements file.
643 """
644 from .CondaNewEnvironmentDataDialog import (
645 CondaNewEnvironmentDataDialog)
646
647 dlg = CondaNewEnvironmentDataDialog(self.tr("Create Environment"),
648 True, self)
649 if dlg.exec() == QDialog.DialogCode.Accepted:
650 virtEnvName, envName, requirements = dlg.getData()
651 args = [
652 "--name",
653 envName.strip(),
654 "--file",
655 requirements,
656 ]
657 ok, prefix, interpreter = self.__conda.createCondaEnvironment(args)
658 if ok:
659 e5App().getObject("VirtualEnvManager").addVirtualEnv(
660 virtEnvName, prefix, interpreter, isConda=True)
661
662 @pyqtSlot()
663 def __deleteEnvironment(self):
664 """
665 Private slot to delete a conda environment.
666 """
667 envName = self.environmentsComboBox.currentText()
668 ok = E5MessageBox.yesNo(
669 self,
670 self.tr("Delete Environment"),
671 self.tr("""<p>Shall the environment <b>{0}</b> really be"""
672 """ deleted?</p>""").format(envName)
673 )
674 if ok:
675 self.__conda.removeCondaEnvironment(name=envName)
676
677 @pyqtSlot()
678 def __editUserConfiguration(self):
679 """
680 Private slot to edit the user configuration.
681 """
682 from QScintilla.MiniEditor import MiniEditor
683
684 cfgFile = CondaInterface.userConfiguration()
685 if not cfgFile:
686 return
687
688 if not os.path.exists(cfgFile):
689 self.__conda.writeDefaultConfiguration()
690
691 # check, if the destination is writeable
692 if not os.access(cfgFile, os.W_OK):
693 E5MessageBox.critical(
694 None,
695 self.tr("Edit Configuration"),
696 self.tr("""The configuration file "{0}" does not exist"""
697 """ or is not writable.""").format(cfgFile))
698 return
699
700 self.__editor = MiniEditor(cfgFile, "YAML")
701 self.__editor.show()
702
703 @pyqtSlot()
704 def __condaConfigure(self):
705 """
706 Private slot to open the configuration page.
707 """
708 e5App().getObject("UserInterface").showPreferences("condaPage")
709
710 @pyqtSlot()
711 def on_recheckButton_clicked(self):
712 """
713 Private slot to re-check the availability of conda and adjust the
714 interface if it became available.
715 """
716 if CondaInterface.isCondaAvailable():
717 self.__initCondaInterface()
718
719 self.notAvailableWidget.hide()
720 self.baseWidget.show()

eric ide

mercurial