eric7/VirtualEnv/VirtualenvConfigurationDialog.py

Mon, 04 Apr 2022 17:43:43 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 04 Apr 2022 17:43:43 +0200
branch
eric7
changeset 9016
6f079c524e99
parent 8881
54e42bc2437a
permissions
-rw-r--r--

Changed occurrences of sys.executable with a method to get rid of the "w" on Windows and macOS systems (i.e. change pythonw to python).

# -*- coding: utf-8 -*-

# Copyright (c) 2014 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to enter the parameters for the
virtual environment.
"""

import os
import re

from PyQt6.QtCore import pyqtSlot, QProcess, QTimer
from PyQt6.QtWidgets import QDialog, QDialogButtonBox

from EricWidgets.EricPathPicker import EricPathPickerModes
from EricWidgets.EricApplication import ericApp

from .Ui_VirtualenvConfigurationDialog import Ui_VirtualenvConfigurationDialog

import Globals
import Preferences
import Utilities

import CondaInterface


class VirtualenvConfigurationDialog(QDialog, Ui_VirtualenvConfigurationDialog):
    """
    Class implementing a dialog to enter the parameters for the
    virtual environment.
    """
    def __init__(self, baseDir="", parent=None):
        """
        Constructor
        
        @param baseDir base directory for the virtual environments
        @type str
        @param parent reference to the parent widget
        @type QWidget
        """
        super().__init__(parent)
        self.setupUi(self)
        
        if not baseDir:
            baseDir = Utilities.getHomeDir()
        self.__envBaseDir = baseDir
        
        self.targetDirectoryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
        self.targetDirectoryPicker.setWindowTitle(
            self.tr("Virtualenv Target Directory"))
        self.targetDirectoryPicker.setText(baseDir)
        self.targetDirectoryPicker.setDefaultDirectory(baseDir)
        
        self.extraSearchPathPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
        self.extraSearchPathPicker.setWindowTitle(
            self.tr("Extra Search Path for setuptools/pip"))
        self.extraSearchPathPicker.setDefaultDirectory(Utilities.getHomeDir())
        
        self.pythonExecPicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
        self.pythonExecPicker.setWindowTitle(
            self.tr("Python Interpreter"))
        self.pythonExecPicker.setDefaultDirectory(
            Globals.getPythonExecutable())
        
        self.condaTargetDirectoryPicker.setMode(
            EricPathPickerModes.DIRECTORY_MODE)
        self.condaTargetDirectoryPicker.setWindowTitle(
            self.tr("Conda Environment Location"))
        self.condaTargetDirectoryPicker.setDefaultDirectory(
            Utilities.getHomeDir())
        
        self.condaCloneDirectoryPicker.setMode(
            EricPathPickerModes.DIRECTORY_MODE)
        self.condaCloneDirectoryPicker.setWindowTitle(
            self.tr("Conda Environment Location"))
        self.condaCloneDirectoryPicker.setDefaultDirectory(
            Utilities.getHomeDir())
        
        self.condaRequirementsFilePicker.setMode(
            EricPathPickerModes.OPEN_FILE_MODE)
        self.condaRequirementsFilePicker.setWindowTitle(
            self.tr("Conda Requirements File"))
        self.condaRequirementsFilePicker.setDefaultDirectory(
            Utilities.getHomeDir())
        self.condaRequirementsFilePicker.setFilters(
            self.tr("Text Files (*.txt);;All Files (*)"))
        
        self.__versionRe = re.compile(r""".*?(\d+\.\d+\.\d+).*""")
        
        self.__virtualenvFound = False
        self.__pyvenvFound = False
        self.__condaFound = False
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Ok).setEnabled(False)
        
        self.__mandatoryStyleSheet = (
            "QLineEdit {border: 2px solid; border-color: #dd8888}"
            if ericApp().usesDarkPalette() else
            "QLineEdit {border: 2px solid; border-color: #800000}"
        )
        self.targetDirectoryPicker.setStyleSheet(self.__mandatoryStyleSheet)
        self.nameEdit.setStyleSheet(self.__mandatoryStyleSheet)
        self.condaTargetDirectoryPicker.setStyleSheet(
            self.__mandatoryStyleSheet)
        self.condaNameEdit.setStyleSheet(self.__mandatoryStyleSheet)
        
        self.__setVirtualenvVersion()
        self.__setPyvenvVersion()
        self.__setCondaVersion()
        if self.__pyvenvFound:
            self.pyvenvButton.setChecked(True)
        elif self.__virtualenvFound:
            self.virtualenvButton.setChecked(True)
        elif self.__condaFound:
            self.condaButton.setChecked(True)
        
        self.condaInsecureCheckBox.setEnabled(
            CondaInterface.condaVersion() >= (4, 3, 18))
        
        msh = self.minimumSizeHint()
        self.resize(max(self.width(), msh.width()), msh.height())
    
    def __updateOK(self):
        """
        Private method to update the enabled status of the OK button.
        """
        if self.virtualenvButton.isChecked() or self.pyvenvButton.isChecked():
            enable = (
                (self.__virtualenvFound or self.__pyvenvFound) and
                bool(self.targetDirectoryPicker.text()) and
                bool(self.nameEdit.text())
            )
            enable &= self.targetDirectoryPicker.text() != self.__envBaseDir
            self.buttonBox.button(
                QDialogButtonBox.StandardButton.Ok).setEnabled(enable)
        elif self.condaButton.isChecked():
            enable = (
                bool(self.condaNameEdit.text()) or
                bool(self.condaTargetDirectoryPicker.text())
            )
            if self.condaSpecialsGroup.isChecked():
                if self.condaCloneButton.isChecked():
                    enable &= (
                        bool(self.condaCloneNameEdit.text()) or
                        bool(self.condaCloneDirectoryPicker.text())
                    )
                elif self.condaRequirementsButton.isChecked():
                    enable &= bool(self.condaRequirementsFilePicker.text())
            self.buttonBox.button(
                QDialogButtonBox.StandardButton.Ok).setEnabled(enable)
        else:
            self.buttonBox.button(
                QDialogButtonBox.StandardButton.Ok).setEnabled(False)
    
    def __updateUi(self):
        """
        Private method to update the UI depending on the selected
        virtual environment creator (virtualenv or pyvenv).
        """
        # venv page
        enable = self.virtualenvButton.isChecked()
        self.extraSearchPathLabel.setEnabled(enable)
        self.extraSearchPathPicker.setEnabled(enable)
        self.promptPrefixLabel.setEnabled(enable)
        self.promptPrefixEdit.setEnabled(enable)
        self.verbosityLabel.setEnabled(enable)
        self.verbositySpinBox.setEnabled(enable)
        self.versionLabel.setEnabled(enable)
        self.versionComboBox.setEnabled(enable)
        self.unzipCheckBox.setEnabled(enable)
        self.noSetuptoolsCheckBox.setEnabled(enable)
        self.symlinkCheckBox.setEnabled(not enable)
        self.upgradeCheckBox.setEnabled(not enable)
        
        # conda page
        enable = not self.condaSpecialsGroup.isChecked()
        self.condaPackagesEdit.setEnabled(enable)
        self.condaPythonEdit.setEnabled(enable)
        self.condaInsecureCheckBox.setEnabled(
            enable and CondaInterface.condaVersion() >= (4, 3, 18))
        self.condaDryrunCheckBox.setEnabled(enable)
        
        # select page
        if self.condaButton.isChecked():
            self.venvStack.setCurrentWidget(self.condaPage)
        else:
            self.venvStack.setCurrentWidget(self.venvPage)
    
    @pyqtSlot(str)
    def on_nameEdit_textChanged(self, txt):
        """
        Private slot handling a change of the virtual environment name.
        
        @param txt name of the virtual environment
        @type str
        """
        self.__updateOK()
    
    @pyqtSlot(str)
    def on_targetDirectoryPicker_textChanged(self, txt):
        """
        Private slot handling a change of the target directory.
        
        @param txt target directory
        @type str
        """
        self.__updateOK()
    
    @pyqtSlot(str)
    def on_pythonExecPicker_textChanged(self, txt):
        """
        Private slot to react to a change of the Python executable.
        
        @param txt contents of the picker's line edit
        @type str
        """
        self.__setVirtualenvVersion()
        self.__setPyvenvVersion()
        self.__updateOK()
    
    @pyqtSlot(bool)
    def on_virtualenvButton_toggled(self, checked):
        """
        Private slot to react to the selection of 'virtualenv'.
        
        @param checked state of the checkbox
        @type bool
        """
        self.__updateUi()
    
    @pyqtSlot(bool)
    def on_pyvenvButton_toggled(self, checked):
        """
        Private slot to react to the selection of 'pyvenv'.
        
        @param checked state of the checkbox
        @type bool
        """
        self.__updateUi()
    
    @pyqtSlot(bool)
    def on_condaButton_toggled(self, checked):
        """
        Private slot to react to the selection of 'conda'.
        
        @param checked state of the checkbox
        @type bool
        """
        self.__updateUi()
    
    @pyqtSlot(str)
    def on_condaNameEdit_textChanged(self, txt):
        """
        Private slot handling a change of the conda environment name.
        
        @param txt environment name
        @type str
        """
        self.__updateOK()
    
    @pyqtSlot(str)
    def on_condaTargetDirectoryPicker_textChanged(self, txt):
        """
        Private slot handling a change of the conda target directory.
        
        @param txt target directory
        @type str
        """
        self.__updateOK()
    
    @pyqtSlot()
    def on_condaSpecialsGroup_clicked(self):
        """
        Private slot handling the selection of the specials group.
        """
        self.__updateOK()
        self.__updateUi()
    
    @pyqtSlot(str)
    def on_condaCloneNameEdit_textChanged(self, txt):
        """
        Private slot handling a change of the conda source environment name.
        
        @param txt name of the environment to be cloned
        @type str
        """
        self.__updateOK()
    
    @pyqtSlot(str)
    def on_condaCloneDirectoryPicker_textChanged(self, txt):
        """
        Private slot handling a change of the cloned from directory.
        
        @param txt target directory
        @type str
        """
        self.__updateOK()
    
    @pyqtSlot()
    def on_condaCloneButton_clicked(self):
        """
        Private slot handling the selection of the clone button.
        """
        self.__updateOK()
    
    @pyqtSlot()
    def on_condaRequirementsButton_clicked(self):
        """
        Private slot handling the selection of the requirements button.
        """
        self.__updateOK()
    
    @pyqtSlot(str)
    def on_condaRequirementsFilePicker_textChanged(self, txt):
        """
        Private slot handling a change of the requirements file entry.
        
        @param txt current text of the requirements file entry
        @type str
        """
        self.__updateOK()
    
    def __setVirtualenvVersion(self):
        """
        Private method to determine the virtualenv version and set the
        respective label.
        """
        calls = []
        if self.pythonExecPicker.text():
            calls.append((self.pythonExecPicker.text(),
                          ["-m", "virtualenv", "--version"]))
        calls.extend([
            (Globals.getPythonExecutable(), ["-m", "virtualenv", "--version"]),
            ("virtualenv", ["--version"]),
        ])
        
        proc = QProcess()
        for prog, args in calls:
            proc.start(prog, args)
            
            if not proc.waitForStarted(5000):
                # try next entry
                continue
            
            if not proc.waitForFinished(5000):
                # process hangs, kill it
                QTimer.singleShot(2000, proc.kill)
                proc.waitForFinished(3000)
                version = self.tr('<virtualenv did not finish within 5s.>')
                self.__virtualenvFound = False
                break
            
            if proc.exitCode() != 0:
                # returned with error code, try next
                continue
            
            output = str(proc.readAllStandardOutput(),
                         Preferences.getSystem("IOEncoding"),
                         'replace').strip()
            match = re.match(self.__versionRe, output)
            if match:
                self.__virtualenvFound = True
                version = match.group(1)
                break
        else:
            self.__virtualenvFound = False
            version = self.tr('<No suitable virtualenv found.>')
        
        self.virtualenvButton.setText(self.tr(
            "virtualenv Version: {0}".format(version)))
        self.virtualenvButton.setEnabled(self.__virtualenvFound)
        if not self.__virtualenvFound:
            self.virtualenvButton.setChecked(False)
    
    def __setPyvenvVersion(self):
        """
        Private method to determine the pyvenv version and set the respective
        label.
        """
        calls = []
        if self.pythonExecPicker.text():
            calls.append((self.pythonExecPicker.text(),
                          ["-m", "venv"]))
        calls.extend([
            (Globals.getPythonExecutable(), ["-m", "venv"]),
            ("python3", ["-m", "venv"]),
            ("python", ["-m", "venv"]),
        ])
        
        proc = QProcess()
        for prog, args in calls:
            proc.start(prog, args)
            
            if not proc.waitForStarted(5000):
                # try next entry
                continue
            
            if not proc.waitForFinished(5000):
                # process hangs, kill it
                QTimer.singleShot(2000, proc.kill)
                proc.waitForFinished(3000)
                version = self.tr('<pyvenv did not finish within 5s.>')
                self.__pyvenvFound = False
                break
            
            if proc.exitCode() not in [0, 2]:
                # returned with error code, try next
                continue
            
            proc.start(prog, ["--version"])
            proc.waitForFinished(5000)
            output = str(proc.readAllStandardOutput(),
                         Preferences.getSystem("IOEncoding"),
                         'replace').strip()
            match = re.match(self.__versionRe, output)
            if match:
                self.__pyvenvFound = True
                version = match.group(1)
                break
        else:
            self.__pyvenvFound = False
            version = self.tr('<No suitable pyvenv found.>')
        
        self.pyvenvButton.setText(self.tr(
            "pyvenv Version: {0}".format(version)))
        self.pyvenvButton.setEnabled(self.__pyvenvFound)
        if not self.__pyvenvFound:
            self.pyvenvButton.setChecked(False)
    
    def __setCondaVersion(self):
        """
        Private method to determine the conda version and set the respective
        label.
        """
        self.__condaFound = bool(CondaInterface.condaVersion())
        self.condaButton.setText(self.tr(
            "conda Version: {0}".format(CondaInterface.condaVersionStr())))
        self.condaButton.setEnabled(self.__condaFound)
        if not self.__condaFound:
            self.condaButton.setChecked(False)
    
    def __generateTargetDir(self):
        """
        Private method to generate a valid target directory path.
        
        @return target directory path
        @rtype str
        """
        targetDirectory = Utilities.toNativeSeparators(
            self.targetDirectoryPicker.text())
        if not os.path.isabs(targetDirectory):
            targetDirectory = os.path.join(os.path.expanduser("~"),
                                           targetDirectory)
        return targetDirectory
    
    def __generateArguments(self):
        """
        Private method to generate the process arguments.
        
        @return process arguments
        @rtype list of str
        """
        args = []
        if self.condaButton.isChecked():
            if bool(self.condaNameEdit.text()):
                args.extend(["--name", self.condaNameEdit.text()])
            if bool(self.condaTargetDirectoryPicker.text()):
                args.extend(["--prefix",
                             self.condaTargetDirectoryPicker.text()])
            if self.condaSpecialsGroup.isChecked():
                if self.condaCloneButton.isChecked():
                    if bool(self.condaCloneNameEdit.text()):
                        args.extend(
                            ["--clone", self.condaCloneNameEdit.text()]
                        )
                    elif bool(self.condaCloneDirectoryPicker.text()):
                        args.extend(["--clone",
                                     self.condaCloneDirectoryPicker.text()])
                elif self.condaRequirementsButton.isChecked():
                    args.extend(
                        ["--file", self.condaRequirementsFilePicker.text()]
                    )
            if self.condaInsecureCheckBox.isChecked():
                args.append("--insecure")
            if self.condaDryrunCheckBox.isChecked():
                args.append("--dry-run")
            if not self.condaSpecialsGroup.isChecked():
                if bool(self.condaPythonEdit.text()):
                    args.append("python={0}".format(
                        self.condaPythonEdit.text()))
                if bool(self.condaPackagesEdit.text()):
                    args.extend(self.condaPackagesEdit.text().split())
        else:
            if self.virtualenvButton.isChecked():
                if self.extraSearchPathPicker.text():
                    args.append("--extra-search-dir={0}".format(
                        Utilities.toNativeSeparators(
                            self.extraSearchPathPicker.text())))
                if self.promptPrefixEdit.text():
                    args.append("--prompt={0}".format(
                        self.promptPrefixEdit.text().replace(" ", "_")))
                if self.pythonExecPicker.text():
                    args.append("--python={0}".format(
                        Utilities.toNativeSeparators(
                            self.pythonExecPicker.text())))
                elif self.versionComboBox.currentText():
                    args.append("--python=python{0}".format(
                        self.versionComboBox.currentText()))
                if self.verbositySpinBox.value() == 1:
                    args.append("--verbose")
                elif self.verbositySpinBox.value() == -1:
                    args.append("--quiet")
                if self.clearCheckBox.isChecked():
                    args.append("--clear")
                if self.systemCheckBox.isChecked():
                    args.append("--system-site-packages")
                if self.unzipCheckBox.isChecked():
                    args.append("--unzip-setuptools")
                if self.noSetuptoolsCheckBox.isChecked():
                    args.append("--no-setuptools")
                if self.noPipCcheckBox.isChecked():
                    args.append("--no-pip")
                if self.copyCheckBox.isChecked():
                    args.append("--always-copy")
            elif self.pyvenvButton.isChecked():
                if self.clearCheckBox.isChecked():
                    args.append("--clear")
                if self.systemCheckBox.isChecked():
                    args.append("--system-site-packages")
                if self.noPipCcheckBox.isChecked():
                    args.append("--without-pip")
                if self.copyCheckBox.isChecked():
                    args.append("--copies")
                if self.symlinkCheckBox.isChecked():
                    args.append("--symlinks")
                if self.upgradeCheckBox.isChecked():
                    args.append("--upgrade")
            targetDirectory = self.__generateTargetDir()
            args.append(targetDirectory)
        
        return args

    def getData(self):
        """
        Public method to retrieve the dialog data.
        
        @return dictionary containing the data for the two environment
            variants. The keys for both variants are 'arguments' containing the
            command line arguments, 'logicalName' containing the environment
            name to be used with the virtual env manager and 'envType'
            containing the environment type (virtualenv, pyvenv or conda). The
            virtualenv/pyvenv specific keys are 'openTarget' containg a flag to
            open the target directory after creation, 'createLog' containing a
            flag to write a log file, 'createScript' containing a flag to write
            a script, 'targetDirectory' containing the target directory and
            'pythonExe' containing the Python interpreter to be used. The
            conda specific key is 'command' giving the conda command to be
            executed (always 'create').
        @rtype dict
        """
        args = self.__generateArguments()
        resultDict = {
            "arguments": args,
            "logicalName": self.nameEdit.text(),
        }
        if self.condaButton.isChecked():
            resultDict.update({
                "envType": "conda",
                "command": "create",
            })
        else:
            resultDict.update({
                "envType": ("pyvenv" if self.pyvenvButton.isChecked() else
                            "virtualenv"),
                "openTarget": self.openCheckBox.isChecked(),
                "createLog": self.logCheckBox.isChecked(),
                "createScript": self.scriptCheckBox.isChecked(),
                "targetDirectory": self.__generateTargetDir(),
                "pythonExe": Utilities.toNativeSeparators(
                    self.pythonExecPicker.text()),
            })
        
        return resultDict

eric ide

mercurial