diff -r 16a129d168f9 -r 8a15b05eeee3 src/eric7/VirtualEnv/VirtualenvManager.py --- a/src/eric7/VirtualEnv/VirtualenvManager.py Wed Apr 23 17:23:57 2025 +0200 +++ b/src/eric7/VirtualEnv/VirtualenvManager.py Wed Apr 23 18:02:09 2025 +0200 @@ -20,10 +20,12 @@ from eric7 import Preferences from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp +from eric7.EricWidgets.EricComboSelectionDialog import EricComboSelectionDialog from eric7.SystemUtilities import FileSystemUtilities, OSUtilities, PythonUtilities from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog from .VirtualenvMeta import VirtualenvMetaData +from .VirtualenvRegistry import VirtualenvType, VirtualenvTypeRegistry class VirtualenvManager(QObject): @@ -60,6 +62,25 @@ self.__ui = parent + self.__registry = VirtualenvTypeRegistry(venvManager=self) + self.__virtualEnvironments = {} + + # register built-in virtual environment types + self.__registry.registerType( + VirtualenvType( + name="standard", + visual_name=self.tr("Standard"), + createFunc=self.__createStandardVirtualEnv, + deleteFunc=self.__deleteStandardVirtualEnv, + ) + ) + self.__registry.registerType( + VirtualenvType(name="remote", visual_name=self.tr("Remote")) + ) + self.__registry.registerType( + VirtualenvType(name="eric_server", visual_name=self.tr("eric-ide Server")) + ) + self.__loadSettings() def __loadSettings(self): @@ -70,32 +91,20 @@ "PyVenv/VirtualEnvironmentsBaseDir", "" ) - venvString = Preferences.getSettings().value( - "PyVenv/VirtualEnvironments", "{}" # __IGNORE_WARNING_M-613__ - ) - environments = json.loads(venvString) + for key in ("PyVenv/VirtualEnvironmentsV2", "PyVenv/VirtualEnvironments"): + venvString = Preferences.getSettings().value(key, "{}") # noqa: M-613 + environments = json.loads(venvString) + if environments: + break 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 - # is_eric_server a flag indicating an eric-ide server environment - # eric_server a string giving the server name in case of an - # eric-ide server environment - # exec_path: a string to be prefixed to the PATH environment - # setting - # description a description of the environment - # + # each environment entry is a VirtualenvMetaData object: for venvName in environments: environment = environments[venvName] environment["name"] = venvName if ( - environment["is_remote"] + environment.get("environment_type", "standard") == "remote" + or environment.get("is_remote", False) # meta data V1 or os.access(environment["interpreter"], os.X_OK) ) and "is_global" not in environment: environment["is_global"] = environment["path"] == "" @@ -140,7 +149,7 @@ ) Preferences.getSettings().setValue( - "PyVenv/VirtualEnvironments", + "PyVenv/VirtualEnvironmentsV2", json.dumps( {env.name: env.as_dict() for env in self.__virtualEnvironments.values()} ), @@ -163,10 +172,10 @@ for venvName in list(self.__virtualEnvironments): venvItem = self.__virtualEnvironments[venvName] - if not venvItem.is_remote: + if venvItem.environment_type != "remote": venvPath = venvItem.path if venvPath: - if venvItem.is_eric_server: + if venvItem.environment_type == "eric_server": with contextlib.suppress(KeyError): # It is an eric-ide server environment; check it is # still valid. @@ -238,40 +247,47 @@ """ Public slot to create a new virtual environment. - @param baseDir base directory for the virtual environments - @type str + @param baseDir base directory for the virtual environments (defaults to "") + @type str (optional) + """ + if not baseDir: + baseDir = self.__virtualEnvironmentsBaseDir + + environmentTypes = self.__registry.getCreatableEnvironmentTypes() + if len(environmentTypes) == 1: + environmentTypes[0].createFunc(baseDir=baseDir) + elif len(environmentTypes) > 1: + dlg = EricComboSelectionDialog( + [(t.visual_name, t.name) for t in environmentTypes], + title=self.tr("Create Virtual Environment"), + message=self.tr("Select the virtual environment type:"), + parent=self.__ui, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + selectedVenvType = dlg.getSelection()[1] + for venvType in environmentTypes: + if venvType.name == selectedVenvType: + venvType.createFunc(baseDir=baseDir) + break + + def __createStandardVirtualEnv(self, baseDir=""): + """ + Private method to create a standard (pyvenv or virtualenv) environment. + + @param baseDir base directory for the virtual environments (defaults to "") + @type str (optional) """ from .VirtualenvConfigurationDialog import VirtualenvConfigurationDialog from .VirtualenvExecDialog import VirtualenvExecDialog - if not baseDir: - baseDir = self.__virtualEnvironmentsBaseDir - dlg = VirtualenvConfigurationDialog(baseDir=baseDir, parent=self.__ui) 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( - VirtualenvMetaData( - name=resultDict["logicalName"], - path=prefix, - interpreter=interpreter, - is_conda=True, - ) - ) - else: - # now do the call - dia = VirtualenvExecDialog(resultDict, self, parent=self.__ui) - dia.show() - dia.start(resultDict["arguments"]) - dia.exec() + # now do the call + dia = VirtualenvExecDialog(resultDict, self, parent=self.__ui) + dia.show() + dia.start(resultDict["arguments"]) + dia.exec() @pyqtSlot() def upgradeVirtualEnv(self, venvName): @@ -432,18 +448,14 @@ ) 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, - ignore_errors=True, - ) + envType = self.__registry.getEnvironmentType( + self.__virtualEnvironments[venvName].environment_type + ) + if envType and envType.deleteFunc: + deleted = envType.deleteFunc( + self.__virtualEnvironments[venvName] + ) + if deleted: del self.__virtualEnvironments[venvName] self.__saveSettings() @@ -466,15 +478,28 @@ ok = True ok &= bool(self.__virtualEnvironments[venvName].path) ok &= not self.__virtualEnvironments[venvName].is_global - ok &= not self.__virtualEnvironments[venvName].is_remote - ok &= not self.__virtualEnvironments[venvName].is_eric_server ok &= os.access(self.__virtualEnvironments[venvName].path, os.W_OK) return ok + def __deleteStandardVirtualEnv(self, venvMetaData): + """ + Private method to delete a given virtual environment from disk. + + @param venvMetaData virtual environment meta data structure + @type VirtualenvMetaData + @return flag indicating success + @rtype bool + """ + if self.__isEnvironmentDeleteable(venvMetaData.name): + shutil.rmtree(venvMetaData.path, ignore_errors=True) + return True + else: + return False + def removeVirtualEnvs(self, venvNames): """ - Public method to delete virtual environment from the list. + Public method to delete virtual environments from the list. @param venvNames list of logical names for the virtual environments @type list of str @@ -531,7 +556,7 @@ interpreters = { i for i in interpreters if not self.environmentForInterpreter(i)[0] - } # filter the list into a set to make the remaining ones unique + } # convert the list into a set to make the remaining ones unique return list(interpreters) def getEnvironmentEntries(self): @@ -620,43 +645,42 @@ else: return "" - def getVirtualenvNames( - self, noRemote=False, noConda=False, noGlobals=False, noServer=False - ): + def getVirtualenvNames(self, noGlobals=False, filterList=("all",)): """ Public method to get a list of defined virtual environments. - @param noRemote flag indicating to exclude environments for remote - debugging (defaults to False) - @type bool (optional) - @param noConda flag indicating to exclude Conda environments (defaults to False) - @type bool (optional) @param noGlobals flag indicating to exclude global environments (defaults to False) @type bool (optional) - @param noServer flag indicating to exclude eric-ide server environments - (defaults to False) - @type bool (optional) + @param filterList tuple containing the list of virtual environment types to + be included (prefixed by +) or excluded (prefixed by -) (defaults to + ("all",) ) + @type tuple of str ((optional) @return list of defined virtual environments @rtype list of str """ environments = list(self.__virtualEnvironments) - 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) - ] if noGlobals: environments = [ name for name in environments if not self.isGlobalEnvironment(name) ] - if noServer: - environments = [ - name for name in environments if not self.isEricServerEnvironment(name) - ] + if filterList != ("all",): + includeFilter = [f[1:] for f in filterList if f.startswith("+")] + excludeFilter = [f[1:] for f in filterList if f.startswith("-")] + if includeFilter: + environments = [ + name + for name in environments + if self.__virtualEnvironments[name].environment_type + in includeFilter + ] + if excludeFilter: + environments = [ + name + for name in environments + if self.__virtualEnvironments[name].environment_type + not in excludeFilter + ] return environments @@ -674,36 +698,6 @@ except KeyError: 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 - """ - try: - return self.__virtualEnvironments[venvName].is_conda - except KeyError: - 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 - """ - try: - return self.__virtualEnvironments[venvName].is_remote - except KeyError: - return False - def getVirtualenvExecPath(self, venvName): """ Public method to get the search path prefix of a virtual environment. @@ -754,13 +748,16 @@ if host: return self.__virtualEnvironments[ venvName - ].is_eric_server and self.__virtualEnvironments[ + ].environment_type == "eric_server" and self.__virtualEnvironments[ venvName ].eric_server.startswith( f"{host}:" ) else: - return self.__virtualEnvironments[venvName].is_eric_server + return ( + self.__virtualEnvironments[venvName].environment_type + == "eric_server" + ) except KeyError: return False @@ -780,3 +777,44 @@ ] return environments + + ####################################################################### + ## Interface to the virtual environment types registry + ####################################################################### + + def getEnvironmentTypesRegistry(self): + """ + Public method to get a reference to the virtual environment types registry + object. + + @return reference to the virtual environment types registry object + @rtype VirtualenvTypeRegistry + """ + return self.__registry + + def registerType(self, venvType): + """ + Public method to register a new virtual environment type. + + @param venvType virtual environment data + @type VirtualenvType + """ + self.__registry.registerType(venvType=venvType) + + def unregisterType(self, name): + """ + Public method to unregister the virtual environment type of the given name. + + @param name name of the virtual environment type + @type str + """ + self.__registry.unregisterType(name=name) + + def getEnvironmentTypeNames(self): + """ + Public method to get a list of names of registered virtual environment types. + + @return list of tuples of virtual environment type names and their visual name + @rtype list of tuple of (str, str) + """ + return self.__registry.getEnvironmentTypeNames() if self.__registry else []