CondaInterface/CondaPackagesWidget.py

branch
maintenance
changeset 6826
c6dda2cbe081
parent 6796
eebd0a5f10f4
equal deleted inserted replaced
6764:d14ddbfbbd36 6826:c6dda2cbe081
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the conda packages management widget.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSlot, Qt
15 from PyQt5.QtGui import QCursor
16 from PyQt5.QtWidgets import QWidget, QToolButton, QMenu, QTreeWidgetItem, \
17 QApplication, QLineEdit, QDialog
18
19 from E5Gui import E5FileDialog, E5MessageBox, E5TextInputDialog
20 from E5Gui.E5Application import e5App
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.UserRole + 1
35 PackageBuildRole = Qt.UserRole + 2
36
37 # Role definitions of search results list
38 PackageDetailedDataRole = Qt.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(CondaPackagesWidget, self).__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 "navigation_supermenu_button")
71 self.condaMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
72 self.condaMenuButton.setToolTip(self.tr("Conda Menu"))
73 self.condaMenuButton.setPopupMode(QToolButton.InstantPopup)
74 self.condaMenuButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
75 self.condaMenuButton.setFocusPolicy(Qt.NoFocus)
76 self.condaMenuButton.setAutoRaise(True)
77 self.condaMenuButton.setShowMenuInside(True)
78
79 self.refreshButton.setIcon(UI.PixmapCache.getIcon("reload"))
80 self.upgradeButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
81 self.upgradeAllButton.setIcon(UI.PixmapCache.getIcon("2uparrow"))
82 self.uninstallButton.setIcon(UI.PixmapCache.getIcon("minus"))
83 self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find"))
84 self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext"))
85 self.installButton.setIcon(UI.PixmapCache.getIcon("plus"))
86 self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info"))
87
88 if CondaInterface.condaVersion() >= (4, 4, 0):
89 self.searchOptionsWidget.hide()
90 else:
91 self.platformComboBox.addItems(sorted([
92 "", "win-32", "win-64", "osx-64", "linux-32", "linux-64",
93 ]))
94
95 self.__initCondaMenu()
96 self.__populateEnvironments()
97 self.__updateActionButtons()
98
99 self.searchWidget.hide()
100
101 self.__conda.condaEnvironmentCreated.connect(
102 self.on_refreshButton_clicked)
103 self.__conda.condaEnvironmentRemoved.connect(
104 self.on_refreshButton_clicked)
105
106 def __populateEnvironments(self):
107 """
108 Private method to get a list of environments and populate the selector.
109 """
110 environments = [("", "")] + sorted(
111 self.__conda.getCondaEnvironmentsList())
112 for environment in environments:
113 self.environmentsComboBox.addItem(environment[0], environment[1])
114
115 def __initCondaMenu(self):
116 """
117 Private method to create the super menu and attach it to the super
118 menu button.
119 """
120 self.__condaMenu = QMenu(self)
121 self.__envActs = []
122
123 self.__cleanMenu = QMenu(self.tr("Clean"), self)
124 self.__cleanMenu.addAction(
125 self.tr("All"), lambda: self.__conda.cleanConda("all"))
126 self.__cleanMenu.addAction(
127 self.tr("Cache"), lambda: self.__conda.cleanConda("index-cache"))
128 self.__cleanMenu.addAction(
129 self.tr("Lock Files"),
130 lambda: self.__conda.cleanConda("lock"))
131 self.__cleanMenu.addAction(
132 self.tr("Packages"), lambda: self.__conda.cleanConda("packages"))
133 self.__cleanMenu.addAction(
134 self.tr("Tarballs"), lambda: self.__conda.cleanConda("tarballs"))
135
136 self.__condaMenu.addAction(
137 self.tr("About Conda..."), self.__aboutConda)
138 self.__condaMenu.addSeparator()
139 self.__condaMenu.addAction(
140 self.tr("Update Conda"), self.__conda.updateConda)
141 self.__condaMenu.addSeparator()
142 self.__envActs.append(self.__condaMenu.addAction(
143 self.tr("Install Packages"), self.__installPackages))
144 self.__envActs.append(self.__condaMenu.addAction(
145 self.tr("Install Requirements"), self.__installRequirements))
146 self.__condaMenu.addSeparator()
147 self.__envActs.append(self.__condaMenu.addAction(
148 self.tr("Generate Requirements"), self.__generateRequirements))
149 self.__condaMenu.addSeparator()
150 self.__condaMenu.addAction(
151 self.tr("Create Environment from Requirements"),
152 self.__createEnvironment)
153 self.__envActs.append(self.__condaMenu.addAction(
154 self.tr("Clone Environment"), self.__cloneEnvironment))
155 self.__deleteEnvAct = self.__condaMenu.addAction(
156 self.tr("Delete Environment"), self.__deleteEnvironment)
157 self.__condaMenu.addSeparator()
158 self.__condaMenu.addMenu(self.__cleanMenu)
159 self.__condaMenu.addSeparator()
160 self.__condaMenu.addAction(
161 self.tr("Edit User Configuration..."),
162 self.__editUserConfiguration)
163 self.__condaMenu.addSeparator()
164 self.__condaMenu.addAction(
165 self.tr("Configure..."), self.__condaConfigure)
166
167 self.condaMenuButton.setMenu(self.__condaMenu)
168
169 self.__condaMenu.aboutToShow.connect(self.__aboutToShowCondaMenu)
170
171 def __selectedUpdateableItems(self):
172 """
173 Private method to get a list of selected items that can be updated.
174
175 @return list of selected items that can be updated
176 @rtype list of QTreeWidgetItem
177 """
178 return [
179 itm for itm in self.packagesList.selectedItems()
180 if bool(itm.text(2))
181 ]
182
183 def __allUpdateableItems(self):
184 """
185 Private method to get a list of all items that can be updated.
186
187 @return list of all items that can be updated
188 @rtype list of QTreeWidgetItem
189 """
190 updateableItems = []
191 for index in range(self.packagesList.topLevelItemCount()):
192 itm = self.packagesList.topLevelItem(index)
193 if itm.text(2):
194 updateableItems.append(itm)
195
196 return updateableItems
197
198 def __updateActionButtons(self):
199 """
200 Private method to set the state of the action buttons.
201 """
202 self.upgradeButton.setEnabled(
203 bool(self.__selectedUpdateableItems()))
204 self.uninstallButton.setEnabled(
205 bool(self.packagesList.selectedItems()))
206 self.upgradeAllButton.setEnabled(
207 bool(self.__allUpdateableItems()))
208
209 @pyqtSlot(int)
210 def on_environmentsComboBox_currentIndexChanged(self, index):
211 """
212 Private slot handling the selection of a conda environment.
213
214 @param index index of the selected conda environment
215 @type int
216 """
217 self.packagesList.clear()
218 prefix = self.environmentsComboBox.itemData(index)
219 if prefix:
220 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
221 self.statusLabel.show()
222 self.statusLabel.setText(self.tr("Getting installed packages..."))
223 QApplication.processEvents()
224
225 # 1. populate with installed packages
226 self.packagesList.setUpdatesEnabled(False)
227 installedPackages = \
228 self.__conda.getInstalledPackages(prefix=prefix)
229 for package, version, build in installedPackages:
230 itm = QTreeWidgetItem(self.packagesList, [package, version])
231 itm.setData(1, self.PackageVersionRole, version)
232 itm.setData(1, self.PackageBuildRole, build)
233 self.packagesList.setUpdatesEnabled(True)
234 self.statusLabel.setText(self.tr("Getting outdated packages..."))
235 QApplication.processEvents()
236
237 # 2. update with update information
238 self.packagesList.setUpdatesEnabled(False)
239 updateablePackages = \
240 self.__conda.getUpdateablePackages(prefix=prefix)
241 for package, version, build in updateablePackages:
242 items = self.packagesList.findItems(
243 package, Qt.MatchExactly | Qt.MatchCaseSensitive)
244 if items:
245 itm = items[0]
246 itm.setText(2, version)
247 itm.setData(2, self.PackageVersionRole, version)
248 itm.setData(2, self.PackageBuildRole, build)
249 if itm.data(1, self.PackageVersionRole) == version:
250 # build must be different, show in version display
251 itm.setText(1, self.tr("{0} (Build: {1})").format(
252 itm.data(1, self.PackageVersionRole),
253 itm.data(1, self.PackageBuildRole),
254 ))
255 itm.setText(2, self.tr("{0} (Build: {1})").format(
256 itm.data(2, self.PackageVersionRole),
257 itm.data(2, self.PackageBuildRole),
258 ))
259
260 self.packagesList.sortItems(0, Qt.AscendingOrder)
261 for col in range(self.packagesList.columnCount()):
262 self.packagesList.resizeColumnToContents(col)
263 self.packagesList.setUpdatesEnabled(True)
264 QApplication.restoreOverrideCursor()
265 self.statusLabel.hide()
266
267 self.__updateActionButtons()
268 self.__updateSearchActionButtons()
269
270 @pyqtSlot()
271 def on_packagesList_itemSelectionChanged(self):
272 """
273 Private slot to handle the selection of some items..
274 """
275 self.__updateActionButtons()
276
277 @pyqtSlot()
278 def on_refreshButton_clicked(self):
279 """
280 Private slot to refresh the display.
281 """
282 currentEnvironment = self.environmentsComboBox.currentText()
283 self.environmentsComboBox.clear()
284 self.packagesList.clear()
285
286 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
287 QApplication.processEvents()
288
289 self.__populateEnvironments()
290
291 index = self.environmentsComboBox.findText(
292 currentEnvironment, Qt.MatchExactly | Qt.MatchCaseSensitive)
293 if index != -1:
294 self.environmentsComboBox.setCurrentIndex(index)
295
296 QApplication.restoreOverrideCursor()
297 self.__updateActionButtons()
298
299 @pyqtSlot()
300 def on_upgradeButton_clicked(self):
301 """
302 Private slot to upgrade selected packages of the selected environment.
303 """
304 packages = [itm.text(0) for itm in self.__selectedUpdateableItems()]
305 if packages:
306 prefix = self.environmentsComboBox.itemData(
307 self.environmentsComboBox.currentIndex())
308 ok = self.__conda.updatePackages(packages, prefix=prefix)
309 if ok:
310 self.on_refreshButton_clicked()
311
312 @pyqtSlot()
313 def on_upgradeAllButton_clicked(self):
314 """
315 Private slot to upgrade all packages of the selected environment.
316 """
317 prefix = self.environmentsComboBox.itemData(
318 self.environmentsComboBox.currentIndex())
319 ok = self.__conda.updateAllPackages(prefix=prefix)
320 if ok:
321 self.on_refreshButton_clicked()
322
323 @pyqtSlot()
324 def on_uninstallButton_clicked(self):
325 """
326 Private slot to remove selected packages of the selected environment.
327 """
328 packages = [itm.text(0) for itm in self.packagesList.selectedItems()]
329 if packages:
330 prefix = self.environmentsComboBox.itemData(
331 self.environmentsComboBox.currentIndex())
332 ok = self.__conda.uninstallPackages(packages, prefix=prefix)
333 if ok:
334 self.on_refreshButton_clicked()
335
336 #######################################################################
337 ## Search widget related methods below
338 #######################################################################
339
340 def __updateSearchActionButtons(self):
341 """
342 Private method to update the action button states of the search widget.
343 """
344 enable = len(self.searchResultList.selectedItems()) == 1
345 self.installButton.setEnabled(
346 enable and self.environmentsComboBox.currentIndex() > 0)
347 self.showDetailsButton.setEnabled(
348 enable and bool(self.searchResultList.selectedItems()[0].parent()))
349
350 def __doSearch(self):
351 """
352 Private method to search for packages.
353 """
354 self.searchResultList.clear()
355 pattern = self.searchEdit.text()
356 if pattern:
357 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
358 QApplication.processEvents()
359
360 if CondaInterface.condaVersion() >= (4, 4, 0):
361 prefix = ""
362 else:
363 prefix = self.environmentsComboBox.itemData(
364 self.environmentsComboBox.currentIndex())
365 ok, result = self.__conda.searchPackages(
366 pattern,
367 fullNameOnly=self.fullNameButton.isChecked(),
368 packageSpec=self.packageSpecButton.isChecked(),
369 platform=self.platformComboBox.currentText(),
370 prefix=prefix,
371 )
372
373 if result:
374 if ok:
375 self.searchResultList.setUpdatesEnabled(False)
376 for package in result:
377 itm = QTreeWidgetItem(self.searchResultList, [package])
378 itm.setExpanded(False)
379 for detail in result[package]:
380 version = detail["version"]
381 build = detail["build"]
382 if "subdir" in detail:
383 platform = detail["subdir"]
384 elif "platform" in detail:
385 platform = detail["platform"]
386 else:
387 platform = ""
388 citm = QTreeWidgetItem(
389 itm, ["", version, build, platform])
390 citm.setData(0, self.PackageDetailedDataRole,
391 detail)
392
393 self.searchResultList.sortItems(0, Qt.AscendingOrder)
394 self.searchResultList.resizeColumnToContents(0)
395 self.searchResultList.setUpdatesEnabled(True)
396 else:
397 QApplication.restoreOverrideCursor()
398 try:
399 message = result["message"]
400 except KeyError:
401 message = result["error"]
402 E5MessageBox.warning(
403 self,
404 self.tr("Conda Search Package Error"),
405 message)
406 QApplication.restoreOverrideCursor()
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.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.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.Accepted:
626 virtEnvName, envName, _ = dlg.getData()
627 args = [
628 "--name",
629 envName.strip(),
630 "--clone",
631 prefix,
632 ]
633 ok, prefix, interpreter = \
634 self.__conda.createCondaEnvironment(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.Accepted:
650 virtEnvName, envName, requirements = dlg.getData()
651 args = [
652 "--name",
653 envName.strip(),
654 "--file",
655 requirements,
656 ]
657 ok, prefix, interpreter = \
658 self.__conda.createCondaEnvironment(args)
659 if ok:
660 e5App().getObject("VirtualEnvManager").addVirtualEnv(
661 virtEnvName, prefix, interpreter, isConda=True)
662
663 @pyqtSlot()
664 def __deleteEnvironment(self):
665 """
666 Private slot to delete a conda environment.
667 """
668 envName = self.environmentsComboBox.currentText()
669 ok = E5MessageBox.yesNo(
670 self,
671 self.tr("Delete Environment"),
672 self.tr("""<p>Shall the environment <b>{0}</b> really be"""
673 """ deleted?</p>""").format(envName)
674 )
675 if ok:
676 self.__conda.removeCondaEnvironment(name=envName)
677
678 @pyqtSlot()
679 def __editUserConfiguration(self):
680 """
681 Private slot to edit the user configuration.
682 """
683 from QScintilla.MiniEditor import MiniEditor
684
685 cfgFile = CondaInterface.userConfiguration()
686 if not cfgFile:
687 return
688
689 if not os.path.exists(cfgFile):
690 self.__conda.writeDefaultConfiguration()
691
692 # check, if the destination is writeable
693 if not os.access(cfgFile, os.W_OK):
694 E5MessageBox.critical(
695 None,
696 self.tr("Edit Configuration"),
697 self.tr("""The configuration file "{0}" does not exist"""
698 """ or is not writable."""))
699 return
700
701 self.__editor = MiniEditor(cfgFile, "YAML")
702 self.__editor.show()
703
704 @pyqtSlot()
705 def __condaConfigure(self):
706 """
707 Private slot to open the configuration page.
708 """
709 e5App().getObject("UserInterface").showPreferences("condaPage")
710
711 @pyqtSlot()
712 def on_recheckButton_clicked(self):
713 """
714 Private slot to re-check the availability of conda and adjust the
715 interface if it became available.
716 """
717 if CondaInterface.isCondaAvailable():
718 self.__initCondaInterface()
719
720 self.notAvailableWidget.hide()
721 self.baseWidget.show()

eric ide

mercurial