CondaInterface/Conda.py

branch
maintenance
changeset 6826
c6dda2cbe081
parent 6728
ba077788a882
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CondaInterface/Conda.py	Sat Mar 02 11:15:24 2019 +0100
@@ -0,0 +1,734 @@
+# -*- 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 pyqtSignal, QObject, QProcess, QCoreApplication
+from PyQt5.QtWidgets import QDialog
+
+from E5Gui import E5MessageBox
+
+import Globals
+import Preferences
+
+from . import rootPrefix, condaVersion
+from .CondaExecDialog import CondaExecDialog
+
+
+class Conda(QObject):
+    """
+    Class implementing the conda GUI logic.
+    
+    @signal condaEnvironmentCreated() emitted to indicate the creation of
+        a new environment
+    @signal condaEnvironmentRemoved() emitted to indicate the removal of
+        an environment
+    """
+    condaEnvironmentCreated = pyqtSignal()
+    condaEnvironmentRemoved = pyqtSignal()
+    
+    RootName = QCoreApplication.translate("Conda", "<root>")
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent parent
+        @type QObject
+        """
+        super(Conda, self).__init__(parent)
+        
+        self.__ui = parent
+    
+    #######################################################################
+    ## environment related methods below
+    #######################################################################
+    
+    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)
+        """
+        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 = ""
+            
+            self.condaEnvironmentCreated.emit()
+            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
+            
+            if jsonDict["success"]:
+                self.condaEnvironmentRemoved.emit()
+            
+            return jsonDict["success"]
+        
+        return False
+    
+    def getCondaEnvironmentsList(self):
+        """
+        Public method to get a list of all Conda environments.
+        
+        @return list of tuples containing the environment name and prefix
+        @rtype list of tuples of (str, str)
+        """
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        environmentsList = []
+        
+        proc = QProcess()
+        proc.start(exe, ["info", "--json"])
+        if proc.waitForStarted(15000):
+            if proc.waitForFinished(15000):
+                output = str(proc.readAllStandardOutput(),
+                             Preferences.getSystem("IOEncoding"),
+                             'replace').strip()
+                try:
+                    jsonDict = json.loads(output)
+                except Exception:
+                    jsonDict = {}
+                
+                if "envs" in jsonDict:
+                    for prefix in jsonDict["envs"][:]:
+                        if prefix == jsonDict["root_prefix"]:
+                            if not jsonDict["root_writable"]:
+                                # root prefix is listed but not writable
+                                continue
+                            name = self.RootName
+                        else:
+                            name = os.path.basename(prefix)
+                        
+                        environmentsList.append((name, prefix))
+        
+        return environmentsList
+    
+    #######################################################################
+    ## package related methods below
+    #######################################################################
+    
+    def getInstalledPackages(self, name="", prefix=""):
+        """
+        Public method to get a list of installed packages of a conda
+        environment.
+        
+        @param name name of the environment
+        @type str
+        @param prefix prefix of the environment
+        @type str
+        @return list of installed packages. Each entry is a tuple containing
+            the package name, version and build.
+        @rtype list of tuples of (str, str, str)
+        @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 = [
+            "list",
+            "--json",
+        ]
+        if name:
+            args.extend(["--name", name])
+        elif prefix:
+            args.extend(["--prefix", prefix])
+        
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        packages = []
+        
+        proc = QProcess()
+        proc.start(exe, args)
+        if proc.waitForStarted(15000):
+            if proc.waitForFinished(30000):
+                output = str(proc.readAllStandardOutput(),
+                             Preferences.getSystem("IOEncoding"),
+                             'replace').strip()
+                try:
+                    jsonList = json.loads(output)
+                except Exception:
+                    jsonList = []
+                
+                for package in jsonList:
+                    if isinstance(package, dict):
+                        packages.append((
+                            package["name"],
+                            package["version"],
+                            package["build_string"]
+                        ))
+                    else:
+                        packages.append(tuple(package.rsplit("-", 2)))
+        
+        return packages
+    
+    def getUpdateablePackages(self, name="", prefix=""):
+        """
+        Public method to get a list of updateable packages of a conda
+        environment.
+        
+        @param name name of the environment
+        @type str
+        @param prefix prefix of the environment
+        @type str
+        @return list of installed packages. Each entry is a tuple containing
+            the package name, version and build.
+        @rtype list of tuples of (str, str, str)
+        @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 = [
+            "update",
+            "--json",
+            "--quiet",
+            "--all",
+            "--dry-run",
+        ]
+        if name:
+            args.extend(["--name", name])
+        elif prefix:
+            args.extend(["--prefix", prefix])
+        
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        packages = []
+        
+        proc = QProcess()
+        proc.start(exe, args)
+        if proc.waitForStarted(15000):
+            if proc.waitForFinished(30000):
+                output = str(proc.readAllStandardOutput(),
+                             Preferences.getSystem("IOEncoding"),
+                             'replace').strip()
+                try:
+                    jsonDict = json.loads(output)
+                except Exception:
+                    jsonDict = {}
+                
+                if "actions" in jsonDict and "LINK" in jsonDict["actions"]:
+                    for linkEntry in jsonDict["actions"]["LINK"]:
+                        if isinstance(linkEntry, dict):
+                            packages.append((
+                                linkEntry["name"],
+                                linkEntry["version"],
+                                linkEntry["build_string"]
+                            ))
+                        else:
+                            package = linkEntry.split()[0]
+                            packages.append(tuple(package.rsplit("-", 2)))
+        
+        return packages
+    
+    def updatePackages(self, packages, name="", prefix=""):
+        """
+        Public method to update packages of a conda environment.
+        
+        @param packages list of package names to be updated
+        @type list of str
+        @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.")
+        
+        if packages:
+            args = [
+                "update",
+                "--json",
+                "--yes",
+            ]
+            if name:
+                args.extend(["--name", name])
+            elif prefix:
+                args.extend(["--prefix", prefix])
+            args.extend(packages)
+            
+            dlg = CondaExecDialog("update", self.__ui)
+            dlg.start(args)
+            dlg.exec_()
+            ok, _ = dlg.getResult()
+        else:
+            ok = False
+        
+        return ok
+    
+    def updateAllPackages(self, name="", prefix=""):
+        """
+        Public method to update all packages of 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 = [
+            "update",
+            "--json",
+            "--yes",
+            "--all"
+        ]
+        if name:
+            args.extend(["--name", name])
+        elif prefix:
+            args.extend(["--prefix", prefix])
+        
+        dlg = CondaExecDialog("update", self.__ui)
+        dlg.start(args)
+        dlg.exec_()
+        ok, _ = dlg.getResult()
+        
+        return ok
+    
+    def installPackages(self, packages, name="", prefix=""):
+        """
+        Public method to install packages into a conda environment.
+        
+        @param packages list of package names to be installed
+        @type list of str
+        @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.")
+        
+        if packages:
+            args = [
+                "install",
+                "--json",
+                "--yes",
+            ]
+            if name:
+                args.extend(["--name", name])
+            elif prefix:
+                args.extend(["--prefix", prefix])
+            args.extend(packages)
+            
+            dlg = CondaExecDialog("install", self.__ui)
+            dlg.start(args)
+            dlg.exec_()
+            ok, _ = dlg.getResult()
+        else:
+            ok = False
+        
+        return ok
+    
+    def uninstallPackages(self, packages, name="", prefix=""):
+        """
+        Public method to uninstall packages of a conda environment (including
+        all no longer needed dependencies).
+        
+        @param packages list of package names to be uninstalled
+        @type list of str
+        @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.")
+        
+        if packages:
+            from UI.DeleteFilesConfirmationDialog import \
+                DeleteFilesConfirmationDialog
+            dlg = DeleteFilesConfirmationDialog(
+                self.parent(),
+                self.tr("Uninstall Packages"),
+                self.tr(
+                    "Do you really want to uninstall these packages and"
+                    " their dependencies?"),
+                packages)
+            if dlg.exec_() == QDialog.Accepted:
+                args = [
+                    "remove",
+                    "--json",
+                    "--yes",
+                ]
+                if condaVersion() >= (4, 4, 0):
+                    args.append("--prune",)
+                if name:
+                    args.extend(["--name", name])
+                elif prefix:
+                    args.extend(["--prefix", prefix])
+                args.extend(packages)
+                
+                dlg = CondaExecDialog("remove", self.__ui)
+                dlg.start(args)
+                dlg.exec_()
+                ok, _ = dlg.getResult()
+            else:
+                ok = False
+        else:
+            ok = False
+        
+        return ok
+    
+    def searchPackages(self, pattern, fullNameOnly=False, packageSpec=False,
+                       platform="", name="", prefix=""):
+        """
+        Public method to search for a package pattern of a conda environment.
+        
+        @param pattern package search pattern
+        @type str
+        @param fullNameOnly flag indicating to search for full names only
+        @type bool
+        @param packageSpec flag indicating to search a package specification
+        @type bool
+        @param platform type of platform to be searched for
+        @type str
+        @param name name of the environment
+        @type str
+        @param prefix prefix of the environment
+        @type str
+        @return flag indicating success and a dictionary with package name as
+            key and list of dictionaries containing detailed data for the found
+            packages as values
+        @rtype tuple of (bool, dict of list of dict)
+        @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.")
+        
+        args = [
+            "search",
+            "--json",
+        ]
+        if fullNameOnly:
+            args.append("--full-name")
+        if packageSpec:
+            args.append("--spec")
+        if platform:
+            args.extend(["--platform", platform])
+        if name:
+            args.extend(["--name", name])
+        elif prefix:
+            args.extend(["--prefix", prefix])
+        args.append(pattern)
+        
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        packages = {}
+        ok = False
+        
+        proc = QProcess()
+        proc.start(exe, args)
+        if proc.waitForStarted(15000):
+            if proc.waitForFinished(30000):
+                output = str(proc.readAllStandardOutput(),
+                             Preferences.getSystem("IOEncoding"),
+                             'replace').strip()
+                try:
+                    packages = json.loads(output)
+                    ok = "error" not in packages
+                except Exception:
+                    # return values for errors is already set
+                    pass
+        
+        return ok, packages
+    
+    #######################################################################
+    ## special methods below
+    #######################################################################
+    
+    def updateConda(self):
+        """
+        Public method to update conda itself.
+        
+        @return flag indicating success
+        @rtype bool
+        """
+        args = [
+            "update",
+            "--json",
+            "--yes",
+            "conda"
+        ]
+        
+        dlg = CondaExecDialog("update", self.__ui)
+        dlg.start(args)
+        dlg.exec_()
+        ok, _ = dlg.getResult()
+        
+        return ok
+    
+    def writeDefaultConfiguration(self):
+        """
+        Public method to create a conda configuration with default values.
+        """
+        args = [
+            "config",
+            "--write-default",
+            "--quiet"
+        ]
+        
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        proc = QProcess()
+        proc.start(exe, args)
+        proc.waitForStarted(15000)
+        proc.waitForFinished(30000)
+    
+    def getCondaInformation(self):
+        """
+        Public method to get a dictionary containing information about conda.
+        
+        @return dictionary containing information about conda
+        @rtype dict
+        """
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        infoDict = {}
+        
+        proc = QProcess()
+        proc.start(exe, ["info", "--json"])
+        if proc.waitForStarted(15000):
+            if proc.waitForFinished(30000):
+                output = str(proc.readAllStandardOutput(),
+                             Preferences.getSystem("IOEncoding"),
+                             'replace').strip()
+                try:
+                    infoDict = json.loads(output)
+                except Exception:
+                    infoDict = {}
+        
+        return infoDict
+    
+    def runProcess(self, args):
+        """
+        Public method to execute the conda with the given arguments.
+        
+        The conda executable is called with the given arguments and
+        waited for its end.
+        
+        @param args list of command line arguments
+        @type list of str
+        @return tuple containing a flag indicating success and the output
+            of the process
+        @rtype tuple of (bool, str)
+        """
+        exe = Preferences.getConda("CondaExecutable")
+        if not exe:
+            exe = "conda"
+        
+        process = QProcess()
+        process.start(exe, args)
+        procStarted = process.waitForStarted(15000)
+        if procStarted:
+            finished = process.waitForFinished(30000)
+            if finished:
+                if process.exitCode() == 0:
+                    output = str(process.readAllStandardOutput(),
+                                 Preferences.getSystem("IOEncoding"),
+                                 'replace').strip()
+                    return True, output
+                else:
+                    return (False,
+                            self.tr("conda exited with an error ({0}).")
+                            .format(process.exitCode()))
+            else:
+                process.terminate()
+                process.waitForFinished(2000)
+                process.kill()
+                process.waitForFinished(3000)
+                return False, self.tr("conda did not finish within"
+                                      " 30 seconds.")
+        
+        return False, self.tr("conda could not be started.")
+    
+    def cleanConda(self, cleanAction):
+        """
+        Public method to update conda itself.
+        
+        @param cleanAction cleaning action to be performed (must be one of
+            the command line parameters without '--')
+        @type str
+        """
+        args = [
+            "clean",
+            "--yes",
+            "--{0}".format(cleanAction),
+        ]
+        
+        dlg = CondaExecDialog("clean", self.__ui)
+        dlg.start(args)
+        dlg.exec_()

eric ide

mercurial