Conda interface: added capability to remove conda environments the conda way. conda

Sun, 03 Feb 2019 16:59:36 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 03 Feb 2019 16:59:36 +0100
branch
conda
changeset 6697
2f5c951bdf14
parent 6696
706185900558
child 6698
bc5aa4cda1ee

Conda interface: added capability to remove conda environments the conda way.

CondaInterface/Conda.py file | annotate | diff | comparison | revisions
CondaInterface/CondaExecDialog.py file | annotate | diff | comparison | revisions
CondaInterface/__init__.py file | annotate | diff | comparison | revisions
Preferences/ConfigurationPages/CondaPage.py file | annotate | diff | comparison | revisions
UI/UserInterface.py file | annotate | diff | comparison | revisions
VirtualEnv/VirtualenvConfigurationDialog.py file | annotate | diff | comparison | revisions
VirtualEnv/VirtualenvManager.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CondaInterface/Conda.py	Sun Feb 03 16:59:36 2019 +0100
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the conda GUI logic.
+"""
+
+from __future__ import unicode_literals
+try:
+    str = unicode       # __IGNORE_EXCEPTION__
+except NameError:
+    pass
+
+import json
+import os
+
+from PyQt5.QtCore import QObject, QProcess
+
+from E5Gui import E5MessageBox
+
+import Globals
+import Preferences
+
+from . import rootPrefix
+
+
+class Conda(QObject):
+    """
+    Class implementing the conda GUI logic.
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent parent
+        @type QObject
+        """
+        super(Conda, self).__init__(parent)
+        
+        self.__ui = parent
+    
+    def createCondaEnvironment(self, arguments):
+        """
+        Public method to create a conda environment.
+        
+        @param arguments list of command line arguments
+        @type list of str
+        @return tuple containing a flag indicating success, the directory of
+            the created environment (aka. prefix) and the corresponding Python
+            interpreter
+        @rtype tuple of (bool, str, str)
+        """
+        from .CondaExecDialog import CondaExecDialog
+        
+        args = ["create", "--json", "--yes"] + arguments
+        
+        dlg = CondaExecDialog("create", self.__ui)
+        dlg.start(args)
+        dlg.exec_()
+        ok, resultDict = dlg.getResult()
+        
+        if ok:
+            if "actions" in resultDict and \
+                    "PREFIX" in resultDict["actions"]:
+                prefix = resultDict["actions"]["PREFIX"]
+            elif "prefix" in resultDict:
+                prefix = resultDict["prefix"]
+            elif "dst_prefix" in resultDict:
+                prefix = resultDict["dst_prefix"]
+            else:
+                prefix = ""
+            
+            # determine Python executable
+            if prefix:
+                pathPrefixes = [
+                    prefix,
+                    rootPrefix()
+                ]
+            else:
+                pathPrefixes = [
+                    rootPrefix()
+                ]
+            for pathPrefix in pathPrefixes:
+                if Globals.isWindowsPlatform():
+                    python = os.path.join(pathPrefix, "python.exe")
+                else:
+                    python = os.path.join(pathPrefix, "bin", "python")
+                if os.path.exists(python):
+                    break
+            else:
+                python = ""
+            
+            return True, prefix, python
+        else:
+            return False, "", ""
+    
+    def removeCondaEnvironment(self, name="", prefix=""):
+        """
+        Public method to remove a conda environment.
+        
+        @param name name of the environment
+        @type str
+        @param prefix prefix of the environment
+        @type str
+        @return flag indicating success
+        @rtype bool
+        @exception RuntimeError raised to indicate an error in parameters
+        
+        Note: only one of name or prefix must be given.
+        """
+        if name and prefix:
+            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
+        
+        if not name and not prefix:
+            raise RuntimeError("One of 'name' or 'prefix' must be given.")
+        
+        args = [
+            "remove",
+            "--json",
+            "--quiet",
+            "--all",
+        ]
+        if name:
+            args.extend(["--name", name])
+        elif prefix:
+            args.extend(["--prefix", prefix])
+        
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        proc = QProcess()
+        proc.start(exe, args)
+        if not proc.waitForStarted(15000):
+            E5MessageBox.critical(
+                self.__ui,
+                self.tr("conda remove"),
+                self.tr("""The conda executable could not be started."""))
+            return False
+        else:
+            proc.waitForFinished(15000)
+            output = str(proc.readAllStandardOutput(),
+                         Preferences.getSystem("IOEncoding"),
+                         'replace').strip()
+            try:
+                jsonDict = json.loads(output)
+            except Exception:
+                E5MessageBox.critical(
+                    self.__ui,
+                    self.tr("conda remove"),
+                    self.tr("""The conda executable returned invalid data."""))
+                return False
+            
+            if "error" in jsonDict:
+                E5MessageBox.critical(
+                    self.__ui,
+                    self.tr("conda remove"),
+                    self.tr("<p>The conda executable returned an error.</p>"
+                            "<p>{0}</p>").format(jsonDict["message"]))
+                return False
+            
+            return jsonDict["success"]
--- a/CondaInterface/CondaExecDialog.py	Sun Feb 03 16:31:53 2019 +0100
+++ b/CondaInterface/CondaExecDialog.py	Sun Feb 03 16:59:36 2019 +0100
@@ -23,35 +23,29 @@
 from .Ui_CondaExecDialog import Ui_CondaExecDialog
 
 import Preferences
-from Globals import dataString
+import Globals
 
 
 class CondaExecDialog(QDialog, Ui_CondaExecDialog):
     """
     Class implementing a dialog to show the output of a conda execution.
     """
-    def __init__(self, configuration, venvManager, parent=None):
+    def __init__(self, command, parent=None):
         """
         Constructor
         
-        @param configuration dictionary containing the configuration parameters
-            as returned by the command configuration dialog
-        @type dict
-        @param venvManager reference to the virtual environment manager
-        @type VirtualenvManager
+        @param command conda command executed
+        @type str
         @param parent reference to the parent widget
         @type QWidget
         """
         super(CondaExecDialog, self).__init__(parent)
         self.setupUi(self)
         
-        self.__finishCommandMethods = {
-            "create": self.__finishCreate,
-        }
+        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
         
-        self.__venvName = configuration["logicalName"]
-        self.__venvManager = venvManager
-        self.__condaCommand = configuration["command"]
+        self.__condaCommand = command
         
         self.__process = None
         self.__condaExe = Preferences.getConda("CondaExecutable")
@@ -92,6 +86,9 @@
         self.__firstProgress = True
         self.__lastFetchFile = ""
         
+        self.__statusOk = False
+        self.__result = None
+        
         self.__process = QProcess()
         self.__process.readyReadStandardOutput.connect(self.__readStdout)
         self.__process.readyReadStandardError.connect(self.__readStderr)
@@ -116,9 +113,12 @@
         It is called when the process finished or
         the user pressed the button.
         
-        @param exitCode exit code of the process (integer)
-        @param exitStatus exit status of the process (QProcess.ExitStatus)
-        @keyparam giveUp flag indicating to not start another attempt (boolean)
+        @param exitCode exit code of the process
+        @type int
+        @param exitStatus exit status of the process
+        @type QProcess.ExitStatus
+        @param giveUp flag indicating to not start another attempt
+        @type bool
         """
         if self.__process is not None and \
            self.__process.state() != QProcess.NotRunning:
@@ -133,48 +133,41 @@
         self.progressLabel.hide()
         self.progressBar.hide()
         
+        self.__statusOk = exitCode == 0
+        
         self.__logOutput(self.tr("Operation finished.\n"))
-        if self.__json:
-            if self.__bufferedStdout:
+        if not self.__json and self.__bufferedStdout:
+            self.__logOutput(self.__bufferedStdout)
+        
+        if self.__json and self.__bufferedStdout:
                 index = self.__bufferedStdout.find("{")
                 rindex = self.__bufferedStdout.rfind("}")
                 self.__bufferedStdout = self.__bufferedStdout[index:rindex + 1]
                 try:
-                    jsonDict = json.loads(self.__bufferedStdout)
+                    self.__result = json.loads(self.__bufferedStdout)
                 except Exception as error:
+                    self.__result = {}
                     self.__logError(str(error))
                     return
                 
-                if "error" in jsonDict:
-                    self.__logError(jsonDict["error"])
-                elif "success" in jsonDict and jsonDict["success"]:
-                    self.__finishCommandMethods[self.__condaCommand](jsonDict)
-                else:
+                if "error" in self.__result:
+                    self.__logError(self.__result["error"])
+                    self.__statusOk = False
+                elif "success" in self.__result and \
+                        not self.__result["success"]:
                     self.__logError(
                         self.tr("Conda command '{0}' did not return success.")
                         .format(self.__condaCommand))
-        elif self.__bufferedStdout:
-            self.__logOutput(self.__bufferedStdout)
+                    self.__statusOk = False
     
-    def __finishCreate(self, resultDict):
-        """
-        Private method to finish the 'create' command.
-        
-        @param resultDict dictionary containing the 'create' result data
-        @type dict
+    def getResult(self):
         """
-        if "actions" in resultDict and \
-                "PREFIX" in resultDict["actions"]:
-            prefix = resultDict["actions"]["PREFIX"]
-        elif "prefix" in resultDict:
-            prefix = resultDict["prefix"]
-        elif "dst_prefix" in resultDict:
-            prefix = resultDict["dst_prefix"]
-        else:
-            prefix = ""
-        self.__venvManager.addVirtualEnv(self.__venvName,
-                                         prefix,
-                                         isConda=True)
+        Public method to the result of the command execution.
+        
+        @return tuple containing a flag indicating success and the result data.
+        @rtype tuple of (bool, dict)
+        """
+        return self.__statusOk, self.__result
     
     def __setProgressValues(self, jsonDict, progressType):
         """
@@ -200,7 +193,7 @@
                 self.progressBar.setMaximum(jsonDict["maxval"])
                 self.progressBar.setValue(jsonDict["progress"])
                 filename = jsonDict["fetch"].strip()
-                filesize = dataString(int(jsonDict["maxval"]))
+                filesize = Globals.dataString(int(jsonDict["maxval"]))
             
             self.progressLabel.setText(
                 self.tr("{0} (Size: {1})").format(filename, filesize))
--- a/CondaInterface/__init__.py	Sun Feb 03 16:31:53 2019 +0100
+++ b/CondaInterface/__init__.py	Sun Feb 03 16:59:36 2019 +0100
@@ -13,7 +13,7 @@
 except NameError:
     pass
 
-import re
+import json
 
 from PyQt5.QtCore import QCoreApplication, QProcess
 
@@ -21,23 +21,24 @@
 
 __CondaVersion = tuple()
 __CondaVersionStr = ""
+__CondaRootPrefix = ""
+
+__initialized = False
 
 
-def __determineCondaVersion():
+def __initializeCondaInterface():
     """
-    Private module function to get the conda version via the conda executable.
+    Private module function to (re-)initialize the conda interface.
     """
-    global __CondaVersionStr, __CondaVersion
+    global __CondaVersionStr, __CondaVersion, __CondaRootPrefix, __initialized
     
-    if not __CondaVersion:
+    if not __initialized:
         exe = Preferences.getConda("CondaExecutable")
         if not exe:
             exe = "conda"
         
-        versionRe = re.compile(r"""^conda.*?(\d+\.\d+\.\d+).*""")
-        
         proc = QProcess()
-        proc.start(exe, ["--version"])
+        proc.start(exe, ["info", "--json"])
         if not proc.waitForStarted(15000):
             __CondaVersionStr = QCoreApplication.translate(
                 "CondaInterface",
@@ -47,16 +48,21 @@
             output = str(proc.readAllStandardOutput(),
                          Preferences.getSystem("IOEncoding"),
                          'replace').strip()
-            match = re.match(versionRe, output)
-            if match:
-                __CondaVersionStr = match.group(1)
-                __CondaVersion = tuple(
-                    int(i) for i in __CondaVersionStr.split(".")
-                )
-            else:
+            try:
+                jsonDict = json.loads(output)
+            except Exception:
                 __CondaVersionStr = QCoreApplication.translate(
                     "CondaInterface",
-                    '<conda returned strange version info.')
+                    '<conda returned invalid data.')
+                return
+            
+            __CondaVersionStr = jsonDict["conda_version"]
+            __CondaVersion = tuple(
+                int(i) for i in __CondaVersionStr.split(".")
+            )
+            __CondaRootPrefix = jsonDict["root_prefix"]
+            
+            __initialized = True
 
 
 def condaVersion():
@@ -66,7 +72,7 @@
     @return tuple containing the conda version
     @rtype tuple of (int, int, int)
     """
-    __determineCondaVersion()
+    __initializeCondaInterface()
     return __CondaVersion
 
 
@@ -77,5 +83,25 @@
     @return conda version as a string
     @rtype str
     """
-    __determineCondaVersion()
+    __initializeCondaInterface()
     return __CondaVersionStr
+
+
+def rootPrefix():
+    """
+    Module function to get the root prefix.
+    
+    @return root prefix
+    @rtype str
+    """
+    __initializeCondaInterface()
+    return __CondaRootPrefix
+
+
+def resetInterface():
+    """
+    Module function to reset the conda interface.
+    """
+    global __initialized
+    
+    __initialized = False
--- a/Preferences/ConfigurationPages/CondaPage.py	Sun Feb 03 16:31:53 2019 +0100
+++ b/Preferences/ConfigurationPages/CondaPage.py	Sun Feb 03 16:59:36 2019 +0100
@@ -35,13 +35,19 @@
             " dialog."))
         
         # set initial values
-        self.condaExePicker.setText(Preferences.getConda("CondaExecutable"))
+        self.__condaExecutable = Preferences.getConda("CondaExecutable")
+        self.condaExePicker.setText(self.__condaExecutable)
         
     def save(self):
         """
         Public slot to save the conda configuration.
         """
-        Preferences.setConda("CondaExecutable", self.condaExePicker.text())
+        condaExecutable = self.condaExePicker.text()
+        if condaExecutable != self.__condaExecutable:
+            Preferences.setConda("CondaExecutable", condaExecutable)
+            
+            import CondaInterface
+            CondaInterface.resetInterface()
     
 
 def create(dlg):
--- a/UI/UserInterface.py	Sun Feb 03 16:31:53 2019 +0100
+++ b/UI/UserInterface.py	Sun Feb 03 16:59:36 2019 +0100
@@ -231,6 +231,11 @@
         # load the view profiles
         self.profiles = Preferences.getUI("ViewProfiles2")
         
+        # Generate the conda interface
+        from CondaInterface.Conda import Conda
+        self.condaInterface = Conda(self)
+        e5App().registerObject("Conda", self.condaInterface)
+        
         # Generate the virtual environment manager
         from VirtualEnv.VirtualenvManager import VirtualenvManager
         self.virtualenvManager = VirtualenvManager(self)
--- a/VirtualEnv/VirtualenvConfigurationDialog.py	Sun Feb 03 16:31:53 2019 +0100
+++ b/VirtualEnv/VirtualenvConfigurationDialog.py	Sun Feb 03 16:59:36 2019 +0100
@@ -403,7 +403,6 @@
         """
         args = []
         if self.condaButton.isChecked():
-            args.extend(["create", "--json", "--yes"])
             if bool(self.condaNameEdit.text()):
                 args.extend(["--name", self.condaNameEdit.text()])
             if bool(self.condaTargetDirectoryPicker.text()):
--- a/VirtualEnv/VirtualenvManager.py	Sun Feb 03 16:31:53 2019 +0100
+++ b/VirtualEnv/VirtualenvManager.py	Sun Feb 03 16:59:36 2019 +0100
@@ -19,9 +19,9 @@
 from PyQt5.QtWidgets import QDialog
 
 from E5Gui import E5MessageBox
+from E5Gui.E5Application import e5App
 
 import Preferences
-import Utilities
 
 
 class VirtualenvManager(QObject):
@@ -148,19 +148,24 @@
         dlg = VirtualenvConfigurationDialog()
         if dlg.exec_() == QDialog.Accepted:
             resultDict = dlg.getData()
-##            (pyvenv, args, name, openTarget, createLog, createScript,
-##             targetDir, interpreter) = dlg.getData()
             
             if resultDict["envType"] == "conda":
-                from CondaInterface.CondaExecDialog import CondaExecDialog
-                dia = CondaExecDialog(resultDict, self)
+                # create the conda environment
+                conda = e5App().getObject("Conda")
+                ok, prefix, interpreter = conda.createCondaEnvironment(
+                    resultDict["arguments"])
+                if ok and "--dry-run" not in resultDict["arguments"]:
+                    self.addVirtualEnv(resultDict["logicalName"],
+                                       prefix,
+                                       venvInterpreter=interpreter,
+                                       isConda=True)
             else:
                 # now do the call
                 from .VirtualenvExecDialog import VirtualenvExecDialog
                 dia = VirtualenvExecDialog(resultDict, self)
-            dia.show()
-            dia.start(resultDict["arguments"])
-            dia.exec_()
+                dia.show()
+                dia.start(resultDict["arguments"])
+                dia.exec_()
     
     def addVirtualEnv(self, venvName, venvDirectory, venvInterpreter="",
                       venvVariant=3, isGlobal=False, isConda=False,
@@ -208,9 +213,6 @@
             dlg = VirtualenvInterpreterSelectionDialog(venvName, venvDirectory)
             if dlg.exec_() == QDialog.Accepted:
                 venvInterpreter, venvVariant = dlg.getData()
-                if not Utilities.startswithPath(venvInterpreter,
-                                                venvDirectory):
-                    isGlobal = True
         
         if venvInterpreter:
             self.__virtualEnvironments[venvName] = {
@@ -337,9 +339,17 @@
             if dlg.exec_() == QDialog.Accepted:
                 for venvName in venvNames:
                     if self.__isEnvironmentDeleteable(venvName):
-                        shutil.rmtree(
-                            self.__virtualEnvironments[venvName]["path"], True)
-                        del self.__virtualEnvironments[venvName]
+                        if self.isCondaEnvironment(venvName):
+                            conda = e5App().getObject("Conda")
+                            path = self.__virtualEnvironments[venvName]["path"]
+                            res = conda.removeCondaEnvironment(prefix=path)
+                            if res:
+                                del self.__virtualEnvironments[venvName]
+                        else:
+                            shutil.rmtree(
+                                self.__virtualEnvironments[venvName]["path"],
+                                True)
+                            del self.__virtualEnvironments[venvName]
                 
                 self.__saveSettings()
                 
--- a/eric6.e4p	Sun Feb 03 16:31:53 2019 +0100
+++ b/eric6.e4p	Sun Feb 03 16:59:36 2019 +0100
@@ -16,6 +16,7 @@
   <TranslationPattern>i18n/eric6_%language%.ts</TranslationPattern>
   <Eol index="1"/>
   <Sources>
+    <Source>CondaInterface/Conda.py</Source>
     <Source>CondaInterface/CondaExecDialog.py</Source>
     <Source>CondaInterface/__init__.py</Source>
     <Source>Cooperation/ChatWidget.py</Source>
@@ -2246,14 +2247,14 @@
   </Resources>
   <Others>
     <Other>.hgignore</Other>
-    <Other>APIs/Python/zope-2.10.7.api</Other>
-    <Other>APIs/Python/zope-2.11.2.api</Other>
-    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/Python3/PyQt4.bas</Other>
     <Other>APIs/Python3/PyQt5.bas</Other>
     <Other>APIs/Python3/QScintilla2.bas</Other>
     <Other>APIs/Python3/eric6.api</Other>
     <Other>APIs/Python3/eric6.bas</Other>
+    <Other>APIs/Python/zope-2.10.7.api</Other>
+    <Other>APIs/Python/zope-2.11.2.api</Other>
+    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/QSS/qss.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.bas</Other>

eric ide

mercurial