Sun, 03 Feb 2019 16:59:36 +0100
Conda interface: added capability to remove conda environments the conda way.
--- /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>