src/eric7/VirtualEnv/VirtualenvManager.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9144
135240382a3e
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/VirtualEnv/VirtualenvManager.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,691 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2018 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a class to manage Python virtual environments.
+"""
+
+import os
+import sys
+import shutil
+import json
+import copy
+
+from PyQt6.QtCore import pyqtSlot, pyqtSignal, QObject
+from PyQt6.QtWidgets import QDialog
+
+from EricWidgets import EricMessageBox
+from EricWidgets.EricApplication import ericApp
+
+import Globals
+import Preferences
+
+
+class VirtualenvManager(QObject):
+    """
+    Class implementing an object to manage Python virtual environments.
+    
+    @signal virtualEnvironmentAdded() emitted to indicate the addition of
+        a virtual environment
+    @signal virtualEnvironmentRemoved() emitted to indicate the removal and
+        deletion of a virtual environment
+    @signal virtualEnvironmentChanged(name) emitted to indicate a change of
+        a virtual environment
+    @signal virtualEnvironmentsListChanged() emitted to indicate a change of
+        the list of virtual environments (may be used to refresh the list)
+    """
+    DefaultKey = "<default>"
+    
+    virtualEnvironmentAdded = pyqtSignal()
+    virtualEnvironmentRemoved = pyqtSignal()
+    virtualEnvironmentChanged = pyqtSignal(str)
+    
+    virtualEnvironmentsListChanged = pyqtSignal()
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object
+        @type QWidget
+        """
+        super().__init__(parent)
+        
+        self.__ui = parent
+        
+        self.__loadSettings()
+    
+    def __loadSettings(self):
+        """
+        Private slot to load the virtual environments.
+        """
+        self.__virtualEnvironmentsBaseDir = Preferences.getSettings().value(
+            "PyVenv/VirtualEnvironmentsBaseDir", "")
+        
+        venvString = Preferences.getSettings().value(
+            "PyVenv/VirtualEnvironments", "{}")     # __IGNORE_WARNING_M613__
+        environments = json.loads(venvString)
+        
+        self.__virtualEnvironments = {}
+        # each environment entry is a dictionary:
+        #   path:           the directory of the virtual environment
+        #                   (empty for a global environment)
+        #   interpreter:    the path of the Python interpreter
+        #   variant:        Python variant (always 3)
+        #   is_global:      a flag indicating a global environment
+        #   is_conda:       a flag indicating an Anaconda environment
+        #   is_remote:      a flag indicating a remotely accessed environment
+        #   exec_path:      a string to be prefixed to the PATH environment
+        #                   setting
+        #
+        envsToDelete = []
+        for venvName in environments:
+            environment = environments[venvName]
+            if (
+                ("is_remote" in environment and environment["is_remote"]) or
+                os.access(environment["interpreter"], os.X_OK)
+            ):
+                if "is_global" not in environment:
+                    environment["is_global"] = environment["path"] == ""
+                if "is_conda" not in environment:
+                    environment["is_conda"] = False
+                if "is_remote" not in environment:
+                    environment["is_remote"] = False
+                if "exec_path" not in environment:
+                    environment["exec_path"] = ""
+                self.__virtualEnvironments[venvName] = environment
+        
+        # now remove unsupported environments
+        for venvName in envsToDelete:
+            del environments[venvName]
+        
+        # check, if the interpreter used to run eric is in the environments
+        defaultPy = Globals.getPythonExecutable()
+        found = False
+        for venvName in self.__virtualEnvironments:
+            if (defaultPy ==
+                    self.__virtualEnvironments[venvName]["interpreter"]):
+                found = True
+                break
+        if not found:
+            # add an environment entry for the default interpreter
+            self.__virtualEnvironments[VirtualenvManager.DefaultKey] = {
+                "path": "",
+                "interpreter": defaultPy,
+                "variant": 3,
+                "is_global": True,
+                "is_conda": False,
+                "is_remote": False,
+                "exec_path": "",
+            }
+        
+        self.__saveSettings()
+    
+    def __saveSettings(self):
+        """
+        Private slot to save the virtual environments.
+        """
+        Preferences.getSettings().setValue(
+            "PyVenv/VirtualEnvironmentsBaseDir",
+            self.__virtualEnvironmentsBaseDir)
+        
+        Preferences.getSettings().setValue(
+            "PyVenv/VirtualEnvironments",
+            json.dumps(self.__virtualEnvironments)
+        )
+        Preferences.syncPreferences()
+    
+    @pyqtSlot()
+    def reloadSettings(self):
+        """
+        Public slot to reload the virtual environments.
+        """
+        Preferences.syncPreferences()
+        self.__loadSettings()
+    
+    def getDefaultEnvironment(self):
+        """
+        Public method to get the default virtual environment.
+        
+        Default is an environment with the key '<default>' or the first one
+        having an interpreter matching sys.executable (i.e. the one used to
+        execute eric with)
+        
+        @return tuple containing the environment name and a dictionary
+            containing a copy of the default virtual environment
+        @rtype tuple of (str, dict)
+        """
+        if VirtualenvManager.DefaultKey in self.__virtualEnvironments:
+            return (
+                VirtualenvManager.DefaultKey,
+                copy.copy(
+                    self.__virtualEnvironments[VirtualenvManager.DefaultKey])
+            )
+        
+        else:
+            return self.environmentForInterpreter(sys.executable)
+    
+    def environmentForInterpreter(self, interpreter):
+        """
+        Public method to get the environment a given interpreter belongs to.
+        
+        @param interpreter path of the interpreter
+        @type str
+        @return tuple containing the environment name and a dictionary
+            containing a copy of the default virtual environment
+        @rtype tuple of (str, dict)
+        """
+        py = interpreter.replace("w.exe", ".exe")
+        for venvName in self.__virtualEnvironments:
+            if (py == self.__virtualEnvironments[venvName]["interpreter"]):
+                return (
+                    venvName,
+                    copy.copy(self.__virtualEnvironments[venvName])
+                )
+        
+        return ("", {})
+    
+    @pyqtSlot()
+    def createVirtualEnv(self, baseDir=""):
+        """
+        Public slot to create a new virtual environment.
+        
+        @param baseDir base directory for the virtual environments
+        @type str
+        """
+        from .VirtualenvConfigurationDialog import (
+            VirtualenvConfigurationDialog
+        )
+        
+        if not baseDir:
+            baseDir = self.__virtualEnvironmentsBaseDir
+        
+        dlg = VirtualenvConfigurationDialog(baseDir=baseDir)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            resultDict = dlg.getData()
+            
+            if resultDict["envType"] == "conda":
+                # create the conda environment
+                conda = ericApp().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()
+    
+    @pyqtSlot()
+    def upgradeVirtualEnv(self, venvName):
+        """
+        Public slot to upgrade a virtual environment.
+        
+        @param venvName name of the virtual environment
+        @type str
+        """
+        from .VirtualenvUpgradeConfigurationDialog import (
+            VirtualenvUpgradeConfigurationDialog
+        )
+        
+        venvDirectory = self.getVirtualenvDirectory(venvName)
+        if not os.path.exists(os.path.join(venvDirectory, "pyvenv.cfg")):
+            # The environment was not created by the 'venv' module.
+            return
+        
+        dlg = VirtualenvUpgradeConfigurationDialog(venvName, venvDirectory)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            pythonExe, args, createLog = dlg.getData()
+            
+            from .VirtualenvUpgradeExecDialog import (
+                VirtualenvUpgradeExecDialog
+            )
+            dia = VirtualenvUpgradeExecDialog(
+                venvName, pythonExe, createLog, self)
+            dia.show()
+            dia.start(args)
+            dia.exec()
+    
+    def addVirtualEnv(self, venvName, venvDirectory, venvInterpreter="",
+                      isGlobal=False, isConda=False, isRemote=False,
+                      execPath=""):
+        """
+        Public method to add a virtual environment.
+        
+        @param venvName logical name for the virtual environment
+        @type str
+        @param venvDirectory directory of the virtual environment
+        @type str
+        @param venvInterpreter interpreter of the virtual environment
+        @type str
+        @param isGlobal flag indicating a global environment
+        @type bool
+        @param isConda flag indicating an Anaconda virtual environment
+        @type bool
+        @param isRemote flag indicating a remotely accessed environment
+        @type bool
+        @param execPath search path string to be prepended to the PATH
+            environment variable
+        @type str
+        """
+        if venvName in self.__virtualEnvironments:
+            ok = EricMessageBox.yesNo(
+                None,
+                self.tr("Add Virtual Environment"),
+                self.tr("""A virtual environment named <b>{0}</b> exists"""
+                        """ already. Shall it be replaced?""")
+                .format(venvName),
+                icon=EricMessageBox.Warning)
+            if not ok:
+                from .VirtualenvNameDialog import VirtualenvNameDialog
+                dlg = VirtualenvNameDialog(
+                    list(self.__virtualEnvironments.keys()),
+                    venvName)
+                if dlg.exec() != QDialog.DialogCode.Accepted:
+                    return
+                
+                venvName = dlg.getName()
+        
+        if not venvInterpreter:
+            from .VirtualenvInterpreterSelectionDialog import (
+                VirtualenvInterpreterSelectionDialog
+            )
+            dlg = VirtualenvInterpreterSelectionDialog(venvName, venvDirectory)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                venvInterpreter = dlg.getData()
+        
+        if venvInterpreter:
+            self.__virtualEnvironments[venvName] = {
+                "path": venvDirectory,
+                "interpreter": venvInterpreter,
+                "variant": 3,                   # always 3
+                "is_global": isGlobal,
+                "is_conda": isConda,
+                "is_remote": isRemote,
+                "exec_path": execPath,
+            }
+            
+            self.__saveSettings()
+            
+            self.virtualEnvironmentAdded.emit()
+            self.virtualEnvironmentsListChanged.emit()
+    
+    def setVirtualEnv(self, venvName, venvDirectory, venvInterpreter,
+                      isGlobal, isConda, isRemote, execPath):
+        """
+        Public method to change a virtual environment.
+        
+        @param venvName logical name of the virtual environment
+        @type str
+        @param venvDirectory directory of the virtual environment
+        @type str
+        @param venvInterpreter interpreter of the virtual environment
+        @type str
+        @param isGlobal flag indicating a global environment
+        @type bool
+        @param isConda flag indicating an Anaconda virtual environment
+        @type bool
+        @param isRemote flag indicating a remotely accessed environment
+        @type bool
+        @param execPath search path string to be prepended to the PATH
+            environment variable
+        @type str
+        """
+        if venvName not in self.__virtualEnvironments:
+            EricMessageBox.yesNo(
+                None,
+                self.tr("Change Virtual Environment"),
+                self.tr("""A virtual environment named <b>{0}</b> does not"""
+                        """ exist. Aborting!""")
+                .format(venvName),
+                icon=EricMessageBox.Warning)
+            return
+        
+        self.__virtualEnvironments[venvName] = {
+            "path": venvDirectory,
+            "interpreter": venvInterpreter,
+            "variant": 3,                   # always 3
+            "is_global": isGlobal,
+            "is_conda": isConda,
+            "is_remote": isRemote,
+            "exec_path": execPath,
+        }
+        
+        self.__saveSettings()
+        
+        self.virtualEnvironmentChanged.emit(venvName)
+        self.virtualEnvironmentsListChanged.emit()
+    
+    def renameVirtualEnv(self, oldVenvName, venvName, venvDirectory,
+                         venvInterpreter, isGlobal, isConda,
+                         isRemote, execPath):
+        """
+        Public method to substitute a virtual environment entry with a new
+        name.
+        
+        @param oldVenvName old name of the virtual environment
+        @type str
+        @param venvName logical name for the virtual environment
+        @type str
+        @param venvDirectory directory of the virtual environment
+        @type str
+        @param venvInterpreter interpreter of the virtual environment
+        @type str
+        @param isGlobal flag indicating a global environment
+        @type bool
+        @param isConda flag indicating an Anaconda virtual environment
+        @type bool
+        @param isRemote flag indicating a remotely accessed environment
+        @type bool
+        @param execPath search path string to be prepended to the PATH
+            environment variable
+        @type str
+        """
+        if oldVenvName not in self.__virtualEnvironments:
+            EricMessageBox.yesNo(
+                None,
+                self.tr("Rename Virtual Environment"),
+                self.tr("""A virtual environment named <b>{0}</b> does not"""
+                        """ exist. Aborting!""")
+                .format(oldVenvName),
+                icon=EricMessageBox.Warning)
+            return
+        
+        del self.__virtualEnvironments[oldVenvName]
+        self.addVirtualEnv(venvName, venvDirectory, venvInterpreter,
+                           isGlobal, isConda, isRemote, execPath)
+    
+    def deleteVirtualEnvs(self, venvNames):
+        """
+        Public method to delete virtual environments from the list and disk.
+        
+        @param venvNames list of logical names for the virtual environments
+        @type list of str
+        """
+        venvMessages = []
+        for venvName in venvNames:
+            if (
+                venvName in self.__virtualEnvironments and
+                bool(self.__virtualEnvironments[venvName]["path"])
+            ):
+                venvMessages.append(self.tr("{0} - {1}").format(
+                    venvName, self.__virtualEnvironments[venvName]["path"]))
+        if venvMessages:
+            from UI.DeleteFilesConfirmationDialog import (
+                DeleteFilesConfirmationDialog
+            )
+            dlg = DeleteFilesConfirmationDialog(
+                None,
+                self.tr("Delete Virtual Environments"),
+                self.tr("""Do you really want to delete these virtual"""
+                        """ environments?"""),
+                venvMessages
+            )
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                for venvName in venvNames:
+                    if self.__isEnvironmentDeleteable(venvName):
+                        if self.isCondaEnvironment(venvName):
+                            conda = ericApp().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()
+                
+                self.virtualEnvironmentRemoved.emit()
+                self.virtualEnvironmentsListChanged.emit()
+    
+    def __isEnvironmentDeleteable(self, venvName):
+        """
+        Private method to check, if a virtual environment can be deleted from
+        disk.
+        
+        @param venvName name of the virtual environment
+        @type str
+        @return flag indicating it can be deleted
+        @rtype bool
+        """
+        ok = False
+        if venvName in self.__virtualEnvironments:
+            ok = True
+            ok &= bool(self.__virtualEnvironments[venvName]["path"])
+            ok &= not self.__virtualEnvironments[venvName]["is_global"]
+            ok &= not self.__virtualEnvironments[venvName]["is_remote"]
+            ok &= os.access(self.__virtualEnvironments[venvName]["path"],
+                            os.W_OK)
+        
+        return ok
+    
+    def removeVirtualEnvs(self, venvNames):
+        """
+        Public method to delete virtual environment from the list.
+        
+        @param venvNames list of logical names for the virtual environments
+        @type list of str
+        """
+        venvMessages = []
+        for venvName in venvNames:
+            if venvName in self.__virtualEnvironments:
+                venvMessages.append(self.tr("{0} - {1}").format(
+                    venvName, self.__virtualEnvironments[venvName]["path"]))
+        if venvMessages:
+            from UI.DeleteFilesConfirmationDialog import (
+                DeleteFilesConfirmationDialog
+            )
+            dlg = DeleteFilesConfirmationDialog(
+                None,
+                self.tr("Remove Virtual Environments"),
+                self.tr("""Do you really want to remove these virtual"""
+                        """ environments?"""),
+                venvMessages
+            )
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                for venvName in venvNames:
+                    if venvName in self.__virtualEnvironments:
+                        del self.__virtualEnvironments[venvName]
+                
+                self.__saveSettings()
+                
+                self.virtualEnvironmentRemoved.emit()
+                self.virtualEnvironmentsListChanged.emit()
+    
+    def getEnvironmentEntries(self):
+        """
+        Public method to get a dictionary containing the defined virtual
+        environment entries.
+        
+        @return dictionary containing a copy of the defined virtual
+            environments
+        @rtype dict
+        """
+        return copy.deepcopy(self.__virtualEnvironments)
+    
+    @pyqtSlot()
+    def showVirtualenvManagerDialog(self, modal=False):
+        """
+        Public slot to show the virtual environment manager dialog.
+        
+        @param modal flag indicating that the dialog should be shown in
+            a blocking mode
+        """
+        if modal:
+            from .VirtualenvManagerWidgets import VirtualenvManagerDialog
+            virtualenvManagerDialog = VirtualenvManagerDialog(
+                self, self.__ui)
+            virtualenvManagerDialog.exec()
+            self.virtualEnvironmentsListChanged.emit()
+        else:
+            self.__ui.activateVirtualenvManager()
+    
+    def isUnique(self, venvName):
+        """
+        Public method to check, if the give logical name is unique.
+        
+        @param venvName logical name for the virtual environment
+        @type str
+        @return flag indicating uniqueness
+        @rtype bool
+        """
+        return venvName not in self.__virtualEnvironments
+    
+    def getVirtualenvInterpreter(self, venvName):
+        """
+        Public method to get the interpreter for a virtual environment.
+        
+        @param venvName logical name for the virtual environment
+        @type str
+        @return interpreter path
+        @rtype str
+        """
+        if venvName in self.__virtualEnvironments:
+            return (
+                self.__virtualEnvironments[venvName]["interpreter"]
+                .replace("w.exe", ".exe")
+            )
+        else:
+            return ""
+    
+    def setVirtualEnvInterpreter(self, venvName, venvInterpreter):
+        """
+        Public method to change the interpreter for a virtual environment.
+        
+        @param venvName logical name for the virtual environment
+        @type str
+        @param venvInterpreter interpreter path to be set
+        @type str
+        """
+        if venvName in self.__virtualEnvironments:
+            self.__virtualEnvironments[venvName]["interpreter"] = (
+                venvInterpreter
+            )
+            self.__saveSettings()
+            
+            self.virtualEnvironmentChanged.emit(venvName)
+            self.virtualEnvironmentsListChanged.emit()
+    
+    def getVirtualenvDirectory(self, venvName):
+        """
+        Public method to get the directory of a virtual environment.
+        
+        @param venvName logical name for the virtual environment
+        @type str
+        @return directory path
+        @rtype str
+        """
+        if venvName in self.__virtualEnvironments:
+            return self.__virtualEnvironments[venvName]["path"]
+        else:
+            return ""
+    
+    def getVirtualenvNames(self, noRemote=False, noConda=False):
+        """
+        Public method to get a list of defined virtual environments.
+        
+        @param noRemote flag indicating to exclude environments for remote
+            debugging
+        @type bool
+        @param noConda flag indicating to exclude Conda environments
+        @type bool
+        @return list of defined virtual environments
+        @rtype list of str
+        """
+        environments = list(self.__virtualEnvironments.keys())
+        if noRemote:
+            environments = [name for name in environments
+                            if not self.isRemoteEnvironment(name)]
+        if noConda:
+            environments = [name for name in environments
+                            if not self.isCondaEnvironment(name)]
+        
+        return environments
+    
+    def isGlobalEnvironment(self, venvName):
+        """
+        Public method to test, if a given environment is a global one.
+        
+        @param venvName logical name of the virtual environment
+        @type str
+        @return flag indicating a global environment
+        @rtype bool
+        """
+        if venvName in self.__virtualEnvironments:
+            return self.__virtualEnvironments[venvName]["is_global"]
+        else:
+            return False
+    
+    def isCondaEnvironment(self, venvName):
+        """
+        Public method to test, if a given environment is an Anaconda
+        environment.
+        
+        @param venvName logical name of the virtual environment
+        @type str
+        @return flag indicating an Anaconda environment
+        @rtype bool
+        """
+        if venvName in self.__virtualEnvironments:
+            return self.__virtualEnvironments[venvName]["is_conda"]
+        else:
+            return False
+    
+    def isRemoteEnvironment(self, venvName):
+        """
+        Public method to test, if a given environment is a remotely accessed
+        environment.
+        
+        @param venvName logical name of the virtual environment
+        @type str
+        @return flag indicating a remotely accessed environment
+        @rtype bool
+        """
+        if venvName in self.__virtualEnvironments:
+            return self.__virtualEnvironments[venvName]["is_remote"]
+        else:
+            return False
+    
+    def getVirtualenvExecPath(self, venvName):
+        """
+        Public method to get the search path prefix of a virtual environment.
+        
+        @param venvName logical name for the virtual environment
+        @type str
+        @return search path prefix
+        @rtype str
+        """
+        if venvName in self.__virtualEnvironments:
+            return self.__virtualEnvironments[venvName]["exec_path"]
+        else:
+            return ""
+    
+    def setVirtualEnvironmentsBaseDir(self, baseDir):
+        """
+        Public method to set the base directory for the virtual environments.
+        
+        @param baseDir base directory for the virtual environments
+        @type str
+        """
+        self.__virtualEnvironmentsBaseDir = baseDir
+        self.__saveSettings()
+    
+    def getVirtualEnvironmentsBaseDir(self):
+        """
+        Public method to set the base directory for the virtual environments.
+        
+        @return base directory for the virtual environments
+        @rtype str
+        """
+        return self.__virtualEnvironmentsBaseDir

eric ide

mercurial