src/eric7/Project/Project.py

branch
eric7-maintenance
changeset 9371
1da8bc75946f
parent 9305
3b7ef53c34c7
parent 9361
718bc86e1c3f
child 9442
906485dcd210
diff -r 90dc02c8a384 -r 1da8bc75946f src/eric7/Project/Project.py
--- 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

eric ide

mercurial