--- a/src/eric7/Project/Project.py Fri Sep 02 14:10:44 2022 +0200 +++ b/src/eric7/Project/Project.py Sat Oct 01 13:06:10 2022 +0200 @@ -7,15 +7,16 @@ Module implementing the project management functionality. """ -import os -import time -import shutil -import glob +import contextlib +import copy import fnmatch -import copy +import glob +import json +import os +import pathlib +import shutil +import time import zipfile -import contextlib -import pathlib from PyQt6.QtCore import ( pyqtSlot, @@ -32,7 +33,7 @@ from PyQt6.Qsci import QsciScintilla from EricWidgets.EricApplication import ericApp -from EricWidgets import EricFileDialog, EricMessageBox +from EricWidgets import EricFileDialog, EricMessageBox, EricPathPickerDialog from EricWidgets.EricListSelectionDialog import EricListSelectionDialog from EricWidgets.EricProgressDialog import EricProgressDialog from EricGui.EricOverrideCursor import EricOverrideCursor, EricOverridenCursor @@ -544,6 +545,7 @@ "DOCSTRING": "", "TESTING_FRAMEWORK": "", "LICENSE": "", + "EMBEDDED_VENV": False, } self.__initDebugProperties() @@ -555,6 +557,8 @@ self.vcs = self.initVCS() + self.__initVenvConfiguration() + def getData(self, category, key): """ Public method to get data out of the project data store. @@ -808,6 +812,10 @@ if self.__dirty: self.projectChanged.emit() + # autosave functionality + if dirty and Preferences.getProject("AutoSaveProject"): + self.saveProject() + def isDirty(self): """ Public method to return the dirty state. @@ -2786,6 +2794,10 @@ self.newProjectHooks.emit() self.newProject.emit() + if self.pdata["EMBEDDED_VENV"]: + self.__createEmbeddedEnvironment() + self.menuEnvironmentAct.setEnabled(self.pdata["EMBEDDED_VENV"]) + def newProjectAddFiles(self, mainscript): """ Public method to add files to a new project. @@ -2953,6 +2965,9 @@ if self.pdata["PROJECTTYPE"] != projectType: self.__reorganizeFiles() + if self.pdata["EMBEDDED_VENV"] and not self.__findEmbeddedEnvironment(): + self.__createEmbeddedEnvironment() + def __showUserProperties(self): """ Private slot to display the user specific properties dialog. @@ -3192,8 +3207,23 @@ self.__readDebugProperties(True) self.__model.projectOpened() - self.projectOpenedHooks.emit() - self.projectOpened.emit() + + if self.pdata["EMBEDDED_VENV"]: + envPath = self.__findEmbeddedEnvironment() + if bool(envPath): + self.__loadEnvironmentConfiguration() + if not bool( + self.__venvConfiguration["interpreter"] + ) or not os.access( + self.__venvConfiguration["interpreter"], os.X_OK + ): + self.__configureEnvironment(envPath) + else: + self.__createEmbeddedEnvironment() + self.menuEnvironmentAct.setEnabled(self.pdata["EMBEDDED_VENV"]) + + self.projectOpenedHooks.emit() + self.projectOpened.emit() if Preferences.getProject("SearchNewFiles"): self.__doSearchNewFiles() @@ -3446,6 +3476,7 @@ self.menuMakeAct.setEnabled(False) self.menuOtherToolsAct.setEnabled(False) self.menuFormattingAct.setEnabled(False) + self.menuEnvironmentAct.setEnabled(False) self.__model.projectClosed() self.projectClosedHooks.emit() @@ -3885,7 +3916,11 @@ @return name of the project's virtual environment @rtype str """ - venvName = self.getDebugProperty("VIRTUALENV") + venvName = ( + self.__venvConfiguration["name"] + if self.pdata["EMBEDDED_VENV"] and bool(self.__venvConfiguration["name"]) + else self.getDebugProperty("VIRTUALENV") + ) if ( not venvName and resolveDebugger @@ -3906,14 +3941,19 @@ @return path of the project's interpreter @rtype str """ - interpreter = "" - venvName = self.getProjectVenv() - if venvName: - interpreter = ( - ericApp() - .getObject("VirtualEnvManager") - .getVirtualenvInterpreter(venvName) - ) + interpreter = ( + self.__venvConfiguration["interpreter"] + if self.pdata["EMBEDDED_VENV"] + else "" + ) + if not interpreter: + venvName = self.getProjectVenv() + if venvName: + interpreter = ( + ericApp() + .getObject("VirtualEnvManager") + .getVirtualenvInterpreter(venvName) + ) if not interpreter and resolveGlobal: interpreter = Globals.getPythonExecutable() @@ -3926,12 +3966,17 @@ @return executable search path prefix @rtype str """ - execPath = "" - venvName = self.getProjectVenv() - if venvName: - execPath = ( - ericApp().getObject("VirtualEnvManager").getVirtualenvExecPath(venvName) - ) + if self.pdata["EMBEDDED_VENV"]: + execPath = self.__venvConfiguration["exec_path"] + else: + execPath = "" + venvName = self.getProjectVenv() + if venvName: + execPath = ( + ericApp() + .getObject("VirtualEnvManager") + .getVirtualenvExecPath(venvName) + ) return execPath @@ -4919,6 +4964,77 @@ ) self.actions.append(self.blackDiffFormattingAct) + self.blackConfigureAct = EricAction( + self.tr("Configure"), + self.tr("Configure"), + 0, + 0, + self.blackFormattingGrp, + "project_black_configure", + ) + self.blackConfigureAct.setStatusTip( + self.tr( + "Enter the parameters for formatting the project sources with 'Black'." + ) + ) + self.blackConfigureAct.setWhatsThis( + self.tr( + "<b>Configure</b>" + "<p>This shows a dialog to enter the parameters for formatting the" + " project sources with 'Black'.</p>" + ) + ) + self.blackConfigureAct.triggered.connect(self.__configureBlack) + self.actions.append(self.blackConfigureAct) + + ################################################################### + ## Project - embedded environment actions + ################################################################### + + self.embeddedEnvironmentGrp = createActionGroup(self) + + self.installVenvAct = EricAction( + self.tr("Install Project"), + self.tr("&Install Project"), + 0, + 0, + self.embeddedEnvironmentGrp, + "project_venv_install", + ) + self.installVenvAct.setStatusTip( + self.tr("Install the project into the embedded environment.") + ) + self.installVenvAct.setWhatsThis( + self.tr( + "<b>Install Project</b>" + "<p>This installs the project into the embedded virtual environment" + " in editable mode (i.e. development mode).</p>" + ) + ) + self.installVenvAct.triggered.connect(self.__installProjectIntoEnvironment) + self.actions.append(self.installVenvAct) + + self.configureVenvAct = EricAction( + self.tr("Configure"), + self.tr("&Configure"), + 0, + 0, + self.embeddedEnvironmentGrp, + "project_venv_configure", + ) + self.configureVenvAct.setStatusTip( + self.tr("Configure the embedded environment.") + ) + self.configureVenvAct.setWhatsThis( + self.tr( + "<b>Configure</b>" + "<p>This opens a dialog to configure the embedded virtual environment" + " of the project.</p>" + ) + ) + self.configureVenvAct.triggered.connect(self.__configureEnvironment) + self.actions.append(self.configureVenvAct) + self.closeAct.setEnabled(False) self.saveAct.setEnabled(False) self.saveasAct.setEnabled(False) @@ -4942,6 +5058,7 @@ self.recentMenu = QMenu(self.tr("Open &Recent Projects"), menu) self.sessionMenu = QMenu(self.tr("Session"), menu) self.debuggerMenu = QMenu(self.tr("Debugger"), menu) + self.environmentMenu = QMenu(self.tr("Embedded Environment"), menu) toolsMenu = QMenu(self.tr("Project-T&ools"), self.parent()) self.vcsMenu = QMenu(self.tr("&Version Control"), toolsMenu) @@ -4974,6 +5091,7 @@ "Make": self.makeMenu, "OtherTools": self.othersMenu, "Formatting": self.formattingMenu, + "Environment": self.environmentMenu, } # connect the aboutToShow signals @@ -4990,6 +5108,7 @@ self.makeMenu.aboutToShow.connect(self.__showContextMenuMake) self.othersMenu.aboutToShow.connect(self.__showContextMenuOthers) self.formattingMenu.aboutToShow.connect(self.__showContextMenuFormat) + self.environmentMenu.aboutToShow.connect(self.__showContextMenuEnvironment) menu.aboutToShow.connect(self.__showMenu) # build the show menu @@ -5012,6 +5131,10 @@ self.debuggerMenu.setTearOffEnabled(True) self.debuggerMenu.addActions(self.dbgActGrp.actions()) + # build the environment menu + self.environmentMenu.setTearOffEnabled(True) + self.environmentMenu.addActions(self.embeddedEnvironmentGrp.actions()) + # build the packagers menu self.packagersMenu.setTearOffEnabled(True) self.packagersMenu.addActions(self.pluginGrp.actions()) @@ -5049,6 +5172,8 @@ menu.addAction(self.filetypesAct) menu.addAction(self.lexersAct) menu.addSeparator() + self.menuEnvironmentAct = menu.addMenu(self.environmentMenu) + menu.addSeparator() self.menuDebuggerAct = menu.addMenu(self.debuggerMenu) self.menuSessionAct = menu.addMenu(self.sessionMenu) @@ -5083,6 +5208,7 @@ self.menuMakeAct.setEnabled(False) self.menuOtherToolsAct.setEnabled(False) self.menuFormattingAct.setEnabled(False) + self.menuEnvironmentAct.setEnabled(False) self.__menu = menu self.__toolsMenu = toolsMenu @@ -5127,6 +5253,7 @@ Private method to set up the project menu. """ self.menuRecentAct.setEnabled(len(self.recent) > 0) + self.menuEnvironmentAct.setEnabled(self.pdata["EMBEDDED_VENV"]) self.showMenu.emit("Main", self.__menus["Main"]) @@ -6558,7 +6685,7 @@ """ Private slot called before the 'Code Formatting' menu is shown. """ - self.showMenu.emit("Formatting", self.othersMenu) + self.showMenu.emit("Formatting", self.formattingMenu) @pyqtSlot() def __aboutBlack(self): @@ -6599,7 +6726,7 @@ if ericApp().getObject("ViewManager").checkAllDirty(): dlg = BlackConfigurationDialog(withProject=True) if dlg.exec() == QDialog.DialogCode.Accepted: - config = dlg.getConfiguration() + config = dlg.getConfiguration(saveToProject=True) formattingDialog = BlackFormattingDialog( config, @@ -6609,6 +6736,165 @@ ) formattingDialog.exec() + @pyqtSlot() + def __configureBlack(self): + """ + Private slot to enter the parameters for formatting the project sources with + 'Black'. + """ + from CodeFormatting.BlackConfigurationDialog import BlackConfigurationDialog + + dlg = BlackConfigurationDialog(withProject=True, onlyProject=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + dlg.getConfiguration(saveToProject=True) + # The data is saved to the project as a side effect. + + ######################################################################### + ## Below are methods implementing the 'Embedded Environment' support + ######################################################################### + + def __showContextMenuEnvironment(self): + """ + Private slot called before the 'Embedded Environment' menu is shown. + """ + self.showMenu.emit("Environment", self.environmentMenu) + + def __findEmbeddedEnvironment(self): + """ + Private method to find the path of the embedded virtual environment. + + @return path of the embedded virtual environment (empty if not found) + @rtype str + """ + for venvPathName in (".venv", "venv", ".env", "env"): + venvPath = os.path.join(self.getProjectPath(), venvPathName) + if os.path.isdir(venvPath): + return venvPath + + return "" + + def __setEmbeddedEnvironmentProjectConfig(self, value): + """ + Private method to set the embedded environment project configuration. + + @param value flag indicating an embedded environment + @type bool + """ + if value != self.pdata["EMBEDDED_VENV"]: + self.pdata["EMBEDDED_VENV"] = value + self.setDirty(True) + + def __initVenvConfiguration(self): + """ + Private method to initialize the environment configuration. + """ + self.__venvConfiguration = { + "name": "embedded environment", + "interpreter": "", + "exec_path": "", + } + + def __createEmbeddedEnvironment(self): + """ + Private method to create the embedded virtual environment. + """ + pythonPath, ok = EricPathPickerDialog.getStrPath( + None, + self.tr("Python Executable"), + self.tr("Enter the Python interpreter for the virtual environment:"), + defaultDirectory=Globals.getPythonExecutable(), + ) + if not ok: + # user canceled the environment creation + self.__setEmbeddedEnvironmentProjectConfig(False) + return + + configuration = { + "envType": "pyvenv", + "targetDirectory": os.path.join(self.getProjectPath(), ".venv"), + "openTarget": False, + "createLog": True, + "createScript": True, + "logicalName": self.__venvConfiguration["name"], + "pythonExe": pythonPath, + } + from VirtualEnv.VirtualenvExecDialog import VirtualenvExecDialog + + dia = VirtualenvExecDialog(configuration, None) + dia.show() + dia.start([configuration["targetDirectory"]]) + dia.exec() + + self.__configureEnvironment() + if not self.__venvConfiguration["interpreter"]: + # user canceled the environment creation + self.__setEmbeddedEnvironmentProjectConfig(False) + return + + @pyqtSlot() + def __configureEnvironment(self, environmentPath=""): + """ + Private slot to configure the embedded environment. + + @param environmentPath path of the virtual environment (defaults to "") + @type str (optional) + """ + from .ProjectVenvConfigurationDialog import ProjectVenvConfigurationDialog + + if not environmentPath: + environmentPath = os.path.join(self.getProjectPath(), ".venv") + + dlg = ProjectVenvConfigurationDialog( + self.__venvConfiguration["name"], + environmentPath, + self.__venvConfiguration["interpreter"], + self.__venvConfiguration["exec_path"], + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + ( + self.__venvConfiguration["interpreter"], + self.__venvConfiguration["exec_path"], + ) = dlg.getData() + self.__saveEnvironmentConfiguration() + self.__setEmbeddedEnvironmentProjectConfig(True) + elif not self.__venvConfiguration["interpreter"]: + self.__setEmbeddedEnvironmentProjectConfig(False) + + def __installProjectIntoEnvironment(self): + """ + Private method to install the project into the embedded environment in + development mode. + """ + pip = ericApp().getObject("Pip") + pip.installEditableProject(self.getProjectInterpreter(), self.getProjectPath()) + + def __saveEnvironmentConfiguration(self): + """ + Private method to save the embedded environment configuration. + """ + with contextlib.suppress(OSError), open( + os.path.join(self.getProjectManagementDir(), "venv_config.json"), "w" + ) as f: + json.dump(self.__venvConfiguration, f, indent=2) + + def __loadEnvironmentConfiguration(self): + """ + Private method to load the embedded environment configuration. + """ + try: + with open( + os.path.join(self.getProjectManagementDir(), "venv_config.json"), "r" + ) as f: + self.__venvConfiguration = json.load(f) + + if not os.path.isfile( + self.__venvConfiguration["interpreter"] + ) or not os.access(self.__venvConfiguration["interpreter"], os.X_OK): + self.__venvConfiguration["interpreter"] = "" + except (OSError, json.JSONDecodeError): + # the configuration file does not exist or is invalid JSON + self.__initVenvConfiguration() + # # eflag: noqa = M601