--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/VirtualEnv/VirtualenvUpgradeExecDialog.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the virtualenv upgrade execution dialog. +""" + +import os + +from PyQt6.QtCore import QProcess, QTimer +from PyQt6.QtWidgets import QDialog, QDialogButtonBox + +from .Ui_VirtualenvExecDialog import Ui_VirtualenvExecDialog + +from Globals import getPythonExecutable +import Preferences + + +class VirtualenvUpgradeExecDialog(QDialog, Ui_VirtualenvExecDialog): + """ + Class implementing the virtualenv upgrade execution dialog. + """ + def __init__(self, venvName, interpreter, createLog, venvManager, + parent=None): + """ + Constructor + + @param venvName name of the virtual environment to be upgraded + @type str + @param interpreter interpreter to be used for the upgrade + @type str + @param createLog flag indicating to create a log file for the upgrade + @type bool + @param venvManager reference to the virtual environment manager + @type VirtualenvManager + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + self.setupUi(self) + + self.buttonBox.button( + QDialogButtonBox.StandardButton.Close).setEnabled(False) + self.buttonBox.button( + QDialogButtonBox.StandardButton.Cancel).setDefault(True) + + self.__process = None + self.__cmd = "" + + self.__progs = [] + if interpreter: + self.__progs.append(interpreter) + self.__progs.extend([ + getPythonExecutable(), + "python3", + "python", + ]) + self.__callIndex = 0 + self.__callArgs = [] + + self.__venvName = venvName + self.__venvDirectory = "" + self.__createLog = createLog + self.__manager = venvManager + + def start(self, arguments): + """ + Public slot to start the virtualenv command. + + @param arguments commandline arguments for virtualenv/pyvenv program + (list of strings) + """ + if self.__callIndex == 0: + # first attempt, add a given python interpreter and do + # some other setup + self.errorGroup.hide() + self.contents.clear() + self.errors.clear() + + self.__process = QProcess() + self.__process.readyReadStandardOutput.connect(self.__readStdout) + self.__process.readyReadStandardError.connect(self.__readStderr) + self.__process.finished.connect(self.__finish) + + self.__callArgs = arguments + self.__venvDirectory = arguments[-1] + + prog = self.__progs[self.__callIndex] + self.__cmd = "{0} {1}".format(prog, " ".join(arguments)) + self.__logOutput(self.tr("Executing: {0}\n").format( + self.__cmd)) + self.__process.start(prog, arguments) + procStarted = self.__process.waitForStarted(5000) + if not procStarted: + self.__logOutput(self.tr("Failed\n\n")) + self.__nextAttempt() + + def on_buttonBox_clicked(self, button): + """ + Private slot called by a button of the button box clicked. + + @param button button that was clicked (QAbstractButton) + """ + if button == self.buttonBox.button( + QDialogButtonBox.StandardButton.Close + ): + self.accept() + elif button == self.buttonBox.button( + QDialogButtonBox.StandardButton.Cancel + ): + self.__finish(0, 0, giveUp=True) + + def __finish(self, exitCode, exitStatus, giveUp=False): + """ + Private slot called when the process finished. + + 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) + @param giveUp flag indicating to not start another attempt (boolean) + """ + if ( + self.__process is not None and + self.__process.state() != QProcess.ProcessState.NotRunning + ): + self.__process.terminate() + QTimer.singleShot(2000, self.__process.kill) + self.__process.waitForFinished(3000) + + self.buttonBox.button( + QDialogButtonBox.StandardButton.Close).setEnabled(True) + self.buttonBox.button( + QDialogButtonBox.StandardButton.Cancel).setEnabled(False) + self.buttonBox.button( + QDialogButtonBox.StandardButton.Close).setDefault(True) + + if not giveUp: + if exitCode != 0: + self.__logOutput(self.tr("Failed\n\n")) + if len(self.errors.toPlainText().splitlines()) == 1: + self.errors.clear() + self.errorGroup.hide() + self.__nextAttempt() + return + + self.__process = None + + self.__logOutput(self.tr('\npyvenv finished.\n')) + + if self.__createLog: + self.__writeLogFile() + + self.__changeVirtualEnvironmentInterpreter() + + def __nextAttempt(self): + """ + Private method to start another attempt. + """ + self.__callIndex += 1 + if self.__callIndex < len(self.__progs): + self.start(self.__callArgs) + else: + self.__logError( + self.tr('No suitable pyvenv program could be' + ' started.\n')) + self.__cmd = "" + self.__finish(0, 0, giveUp=True) + + def __readStdout(self): + """ + Private slot to handle the readyReadStandardOutput signal. + + It reads the output of the process, formats it and inserts it into + the contents pane. + """ + self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput) + + while self.__process.canReadLine(): + s = str(self.__process.readLine(), + Preferences.getSystem("IOEncoding"), + 'replace') + self.__logOutput(s) + + def __readStderr(self): + """ + Private slot to handle the readyReadStandardError signal. + + It reads the error output of the process and inserts it into the + error pane. + """ + self.__process.setReadChannel(QProcess.ProcessChannel.StandardError) + + while self.__process.canReadLine(): + s = str(self.__process.readLine(), + Preferences.getSystem("IOEncoding"), + 'replace') + self.__logError(s) + + def __logOutput(self, s): + """ + Private method to log some output. + + @param s output string to log (string) + """ + self.contents.insertPlainText(s) + self.contents.ensureCursorVisible() + + def __logError(self, s): + """ + Private method to log an error. + + @param s error string to log (string) + """ + self.errorGroup.show() + self.errors.insertPlainText(s) + self.errors.ensureCursorVisible() + + def __writeLogFile(self): + """ + Private method to write a log file to the virtualenv directory. + """ + outtxt = self.contents.toPlainText() + logFile = os.path.join(self.__venvDirectory, "pyvenv_upgrade.log") + self.__logOutput(self.tr("\nWriting log file '{0}'.\n") + .format(logFile)) + + try: + with open(logFile, "w", encoding="utf-8") as f: + f.write(self.tr("Output:\n")) + f.write(outtxt) + errtxt = self.errors.toPlainText() + if errtxt: + f.write("\n") + f.write(self.tr("Errors:\n")) + f.write(errtxt) + except OSError as err: + self.__logError( + self.tr("""The logfile '{0}' could not be written.\n""" + """Reason: {1}\n""").format(logFile, str(err))) + self.__logOutput(self.tr("Done.\n")) + + def __changeVirtualEnvironmentInterpreter(self): + """ + Private method to change the interpreter of the upgraded virtual + environment. + """ + from .VirtualenvInterpreterSelectionDialog import ( + VirtualenvInterpreterSelectionDialog + ) + + venvInterpreter = "" + dlg = VirtualenvInterpreterSelectionDialog( + self.__venvName, self.__venvDirectory) + if dlg.exec() == QDialog.DialogCode.Accepted: + venvInterpreter = dlg.getData() + + if venvInterpreter: + self.__manager.setVirtualEnvInterpreter( + self.__venvName, venvInterpreter)