ProjectPyramid/Project.py

branch
eric7
changeset 148
dcbd3a96f03c
parent 147
eb28b4b6f7f5
child 150
b916658d5014
--- a/ProjectPyramid/Project.py	Tue Jun 01 19:37:46 2021 +0200
+++ b/ProjectPyramid/Project.py	Sun Jun 06 16:30:37 2021 +0200
@@ -7,12 +7,17 @@
 Module implementing the Pyramid project support.
 """
 
+import configparser
+import contextlib
+import glob
 import os
 import re
-import configparser
-import contextlib
+import subprocess       # secok
+import sys
 
-from PyQt6.QtCore import QObject, QFileInfo, QTimer, QUrl, QIODeviceBase
+from PyQt6.QtCore import (
+    pyqtSlot, QObject, QFileInfo, QTimer, QUrl, QIODeviceBase
+)
 from PyQt6.QtGui import QDesktopServices
 from PyQt6.QtWidgets import QMenu, QDialog, QInputDialog, QLineEdit
 from PyQt6.QtCore import QProcess as QProcessPyQt
@@ -125,6 +130,8 @@
         self.__serverProc = None
         
         self.__pyramidVersion = ""
+        
+        self.__migrationSummaryDialog = None
     
     def initActions(self):
         """
@@ -184,21 +191,6 @@
         self.runServerAct.triggered.connect(self.__runServer)
         self.actions.append(self.runServerAct)
         
-        self.runLoggingServerAct = EricAction(
-            self.tr('Run Server with Logging'),
-            self.tr('Run Server with &Logging'),
-            0, 0,
-            self, 'pyramid_run_logging_server')
-        self.runLoggingServerAct.setStatusTip(self.tr(
-            'Starts the Pyramid Web server with logging'))
-        self.runLoggingServerAct.setWhatsThis(self.tr(
-            """<b>Run Server with Logging</b>"""
-            """<p>Starts the Pyramid Web server with logging using"""
-            """ "pserve --log-file=server.log --reload development.ini".</p>"""
-        ))
-        self.runLoggingServerAct.triggered.connect(self.__runLoggingServer)
-        self.actions.append(self.runLoggingServerAct)
-        
         self.runBrowserAct = EricAction(
             self.tr('Run Web-Browser'),
             self.tr('Run &Web-Browser'),
@@ -229,45 +221,6 @@
         self.runPythonShellAct.triggered.connect(self.__runPythonShell)
         self.actions.append(self.runPythonShellAct)
         
-        ##############################
-        ## setup actions below      ##
-        ##############################
-        
-        self.setupDevelopAct = EricAction(
-            self.tr('Setup Development Environment'),
-            self.tr('Setup &Development Environment'),
-            0, 0,
-            self, 'pyramid_setup_development')
-        self.setupDevelopAct.setStatusTip(self.tr(
-            'Setup the Pyramid project in development mode'))
-        self.setupDevelopAct.setWhatsThis(self.tr(
-            """<b>Setup Development Environment</b>"""
-            """<p>Setup the Pyramid project in development mode using"""
-            """ "python setup.py develop".</p>"""
-        ))
-        self.setupDevelopAct.triggered.connect(self.__setupDevelop)
-        self.actions.append(self.setupDevelopAct)
-        
-        ###############################
-        ## database actions below    ##
-        ###############################
-        
-        self.initializeDbAct = EricAction(
-            self.tr('Initialize Database'),
-            self.tr('Initialize &Database'),
-            0, 0,
-            self, 'pyramid_initialize_database')
-        self.initializeDbAct.setStatusTip(self.tr(
-            'Initializes (or re-initializes) the database of the current'
-            ' Pyramid project'))
-        self.initializeDbAct.setWhatsThis(self.tr(
-            """<b>Initialize Database</b>"""
-            """<p>Initializes (or re-initializes) the database of the"""
-            """ current Pyramid project.</p>"""
-        ))
-        self.initializeDbAct.triggered.connect(self.__initializeDatabase)
-        self.actions.append(self.initializeDbAct)
-        
         ###############################
         ## show actions below        ##
         ###############################
@@ -372,8 +325,120 @@
         self.aboutPyramidAct.triggered.connect(self.__pyramidInfo)
         self.actions.append(self.aboutPyramidAct)
         
+        self.__initDatabaseActions()
+        
         self.__setCurrentProject(None)
     
+    def __initDatabaseActions(self):
+        """
+        Private method to initialize the database related actions.
+        """
+        self.initializeDbAct = EricAction(
+            self.tr('Initialize Database'),
+            self.tr('Initialize &Database'),
+            0, 0,
+            self, 'pyramid_initialize_database')
+        self.initializeDbAct.setStatusTip(self.tr(
+            'Initializes (or re-initializes) the database of the current'
+            ' Pyramid project'))
+        self.initializeDbAct.setWhatsThis(self.tr(
+            """<b>Initialize Database</b>"""
+            """<p>Initializes (or re-initializes) the database of the"""
+            """ current Pyramid project.</p>"""
+        ))
+        self.initializeDbAct.triggered.connect(self.__initializeDatabase)
+        self.actions.append(self.initializeDbAct)
+        
+        #########################################################
+        ## action to create a new database migration
+        #########################################################
+        
+        self.migrateCreateAct = EricAction(
+            self.tr('Create Migration'),
+            self.tr('&Create Migration'),
+            0, 0,
+            self, 'flask_create_migration')
+        self.migrateCreateAct.setStatusTip(self.tr(
+            'Create a new migration for the current database'))
+        self.migrateCreateAct.setWhatsThis(self.tr(
+            """<b>Create Migration</b>"""
+            """<p>Creates a new migration for the current database"""
+            """ and stores it  in the configured migrations directory.</p>"""
+        ))
+        self.migrateCreateAct.triggered.connect(
+            self.__createMigration)
+        self.actions.append(self.migrateCreateAct)
+        
+        #########################################################
+        ## action to up- and downgrade a databse
+        #########################################################
+        
+        self.upgradeDatabaseAct = EricAction(
+            self.tr('Upgrade Database'),
+            self.tr('&Upgrade Database'),
+            0, 0,
+            self, 'flask_upgrade_database')
+        self.upgradeDatabaseAct.setStatusTip(self.tr(
+            'Upgrade the database to the current migration'))
+        self.upgradeDatabaseAct.setWhatsThis(self.tr(
+            """<b>Upgrade Database</b>"""
+            """<p>Upgrades the database to the current migration.</p>"""
+        ))
+        self.upgradeDatabaseAct.triggered.connect(
+            self.upgradeDatabase)
+        self.actions.append(self.upgradeDatabaseAct)
+        
+        self.downgradeDatabaseAct = EricAction(
+            self.tr('Downgrade Database'),
+            self.tr('&Downgrade Database'),
+            0, 0,
+            self, 'flask_downgrade_database')
+        self.downgradeDatabaseAct.setStatusTip(self.tr(
+            'Downgrade the database to the previous version'))
+        self.downgradeDatabaseAct.setWhatsThis(self.tr(
+            """<b>Downgrade Database</b>"""
+            """<p>Downgrades the database to the previous version.</p>"""
+        ))
+        self.downgradeDatabaseAct.triggered.connect(
+            self.downgradeDatabase)
+        self.actions.append(self.downgradeDatabaseAct)
+        
+        #########################################################
+        ## actions to show migrations history information
+        #########################################################
+        
+        self.migrationSummaryAct = EricAction(
+            self.tr('Show Migrations Summary'),
+            self.tr('Show Migrations &Summary'),
+            0, 0,
+            self, 'flask_show_migrations_summary')
+        self.migrationSummaryAct.setStatusTip(self.tr(
+            'Show a summary of the created database migrations'))
+        self.migrationSummaryAct.setWhatsThis(self.tr(
+            """<b>Show Migrations Summary</b>"""
+            """<p>Shows a summary list of the created database"""
+            """ migrations.</p>"""
+        ))
+        self.migrationSummaryAct.triggered.connect(
+            self.__showMigrationsSummary)
+        self.actions.append(self.migrationSummaryAct)
+        
+        self.migrationHistoryAct = EricAction(
+            self.tr('Show Migrations History'),
+            self.tr('Show Migrations &History'),
+            0, 0,
+            self, 'flask_show_migrations_history')
+        self.migrationHistoryAct.setStatusTip(self.tr(
+            'Show the full history of the created database migrations'))
+        self.migrationHistoryAct.setWhatsThis(self.tr(
+            """<b>Show Migrations History</b>"""
+            """<p>Shows the full history of the created database"""
+            """ migrations.</p>"""
+        ))
+        self.migrationHistoryAct.triggered.connect(
+            self.__showMigrationsHistory)
+        self.actions.append(self.migrationHistoryAct)
+    
     def initMenu(self):
         """
         Public slot to initialize the Pyramid menu.
@@ -383,27 +448,39 @@
         """
         self.__menus = {}   # clear menus references
         
+        # Database menu
+        dbMenu = QMenu(self.tr("Database"))
+        dbMenu.setTearOffEnabled(True)
+        
+        dbMenu.addAction(self.initializeDbAct)
+        dbMenu.addSeparator()
+        dbMenu.addAction(self.migrateCreateAct)
+        dbMenu.addSeparator()
+        dbMenu.addAction(self.upgradeDatabaseAct)
+        dbMenu.addAction(self.downgradeDatabaseAct)
+        dbMenu.addSeparator()
+        dbMenu.addAction(self.migrationSummaryAct)
+        dbMenu.addAction(self.migrationHistoryAct)
+        
+        # main Pyramid menu
         menu = QMenu(self.tr('P&yramid'), self.__ui)
         menu.setTearOffEnabled(True)
         
         menu.addAction(self.selectProjectAct)
         menu.addSeparator()
         menu.addAction(self.runServerAct)
-        menu.addAction(self.runLoggingServerAct)
         menu.addAction(self.runBrowserAct)
         menu.addSeparator()
+        menu.addAction(self.runPythonShellAct)
+        menu.addSeparator()
         menu.addAction(self.createProjectAct)
         menu.addSeparator()
-        menu.addAction(self.setupDevelopAct)
-        menu.addSeparator()
-        menu.addAction(self.initializeDbAct)
+        menu.addMenu(dbMenu)
         menu.addSeparator()
         menu.addAction(self.showViewsAct)
         menu.addAction(self.showRoutesAct)
         menu.addAction(self.showTweensAct)
         menu.addSeparator()
-        menu.addAction(self.runPythonShellAct)
-        menu.addSeparator()
         menu.addAction(self.buildDistroAct)
         menu.addSeparator()
         menu.addAction(self.documentationAct)
@@ -411,6 +488,9 @@
         menu.addAction(self.aboutPyramidAct)
         
         self.__menus["main"] = menu
+        self.__menus["database"] = dbMenu
+        
+        self.__setCurrentProject(None)
         
         return menu
     
@@ -581,6 +661,10 @@
         if self.__serverProc is not None:
             self.__serverProcFinished()
         self.__setCurrentProject(None)
+        
+        for dlg in (self.__migrationSummaryDialog,):
+            if dlg is not None:
+                dlg.close()
     
     def __getExecutablePaths(self, file):
         """
@@ -626,8 +710,7 @@
         @rtype list of str
         """
         variants = []
-        # TODO: that doesn't exist anymore
-        cmd = "pcreate"
+        cmd = "cookiecutter"
         
         for variant in ['Python3']:
             virtEnv = self.__getVirtualEnvironment(variant)
@@ -737,7 +820,16 @@
         
         return ""
 
-    def getPyramidCommand(self, cmd, language=""):
+    def getProjectVirtualEnvironment(self):
+        """
+        Public method to generate the path of the project virtual environment.
+        
+        @return path of the Pyramid project virtual environment
+        @rtype str
+        """
+        return os.path.join(self.projectPath(), "env")
+    
+    def getPyramidCommand(self, cmd, language="", virtualEnv=""):
         """
         Public method to build a Pyramid command.
         
@@ -746,15 +838,18 @@
         @param language Python variant to get the virtual environment
             for (one of '' or 'Python3')
         @type str
+        @param virtualEnv path of the project's Python virtual environment
+        @type str
         @return full pyramid command
         @rtype str
         """
         if not language:
             language = self.__ericProject.getProjectLanguage()
         
-        virtualEnv = self.__getVirtualEnvironment(language)
-        if isWindowsPlatform() and not virtualEnv:
-            virtualEnv = self.__getDebugEnvironment(language)
+        if not virtualEnv:
+            virtualEnv = self.__getVirtualEnvironment(language)
+            if not virtualEnv:
+                virtualEnv = self.__getDebugEnvironment(language)
         if isWindowsPlatform():
             fullCmds = [
                 os.path.join(virtualEnv, "Scripts", cmd + '.exe'),
@@ -776,6 +871,24 @@
                     break
         return cmd
     
+    def __assemblePyramidCommand(self, cmd, virtualEnv):
+        """
+        Private method to assemble the full pyramid command for a given virtual
+        environment.
+        
+        @param cmd command
+        @type str
+        @param virtualEnv path of the project's Python virtual environment
+        @type str
+        @return assembled pyramid command
+        @rtype str
+        """
+        return (
+            os.path.join(virtualEnv, "Scripts", cmd + '.exe')
+            if isWindowsPlatform() else
+            os.path.join(virtualEnv, "bin", cmd)
+        )
+    
     def getPythonCommand(self):
         """
         Public method to build the Python command.
@@ -835,19 +948,18 @@
         @rtype str
         """
         if not self.__pyramidVersion:
-            # TODO: that doesn't exist anymore
-            cmd = self.getPyramidCommand("pcreate")
-            if isWindowsPlatform():
-                cmd = os.path.join(os.path.dirname(cmd), "pcreate-script.py")
+            cmd = self.getPyramidCommand(
+                "pdistreport",
+                virtualEnv=self.getProjectVirtualEnvironment()
+            )
             try:
-                with open(cmd, 'r', encoding="utf-8") as f:
-                    lines = f.read().splitlines()
-                for line in lines:
-                    if line.startswith("__requires__"):
-                        #- sample: __requires__ = 'pyramid==1.4'
-                        vers = line.strip().split()[-1][1:-1].split("==")[1]
-                        self.__pyramidVersion = vers
-            except OSError:
+                output = subprocess.check_output([cmd])     # secok
+                outputLines = output.decode().splitlines()
+                for line in outputLines:
+                    if line.startswith("Pyramid version:"):
+                        self.__pyramidVersion = line.rsplit(None, 1)[1]
+                        break
+            except (OSError, subprocess.CalledProcessError):
                 self.__pyramidVersion = ""
         
         return self.__pyramidVersion
@@ -913,32 +1025,31 @@
         """
         from .CreateParametersDialog import CreateParametersDialog
         
-        dlg = CreateParametersDialog(self)
+        dlg = CreateParametersDialog(self.__ui)
         if dlg.exec() == QDialog.DialogCode.Accepted:
-            scaffold, project, overwrite, simulate = dlg.getData()
+            template, version, overwrite, contextData = dlg.getData()
             
-            # TODO: that doesn't exist anymore
-            cmd = self.getPyramidCommand("pcreate")
-            args = []
+            cmd = self.getPyramidCommand("cookiecutter")
+            args = ["--no-input"]
             if overwrite:
-                args.append("--overwrite")
-            else:
-                args.append("--interactive")
-            if simulate:
-                args.append("--simulate")
-            args.append("--scaffold={0}".format(scaffold))
-            args.append(project)
+                args.append("--overwrite-if-exists")
+            if version:
+                args += ["--checkout", version]
+            args.append(template)
+            for context, data in contextData.items():
+                args.append("{0}={1}".format(context, data))
             dlg = PyramidDialog(self.tr("Create Pyramid Project"),
                                 linewrap=False, parent=self.__ui)
             if dlg.startProcess(
                 cmd, args, self.__ericProject.getProjectPath()
             ):
                 dlg.exec()
-                if dlg.normalExit() and not simulate:
-                    # search for files created by pcreate and add them to the
-                    # project
+                if dlg.normalExit() and "repo_name" in contextData:
+                    # search for files created by cookiecutter and add them
+                    # to the project
                     projectPath = os.path.join(
-                        self.__ericProject.getProjectPath(), project)
+                        self.__ericProject.getProjectPath(),
+                        contextData["repo_name"])
                     for entry in os.walk(projectPath):
                         for fileName in entry[2]:
                             fullName = os.path.join(entry[0], fileName)
@@ -946,12 +1057,63 @@
                     
                     # create the base directory for translations
                     i18nPath = os.path.join(
-                        projectPath, project.lower(), "i18n")
+                        projectPath, contextData["repo_name"].lower(),
+                        "i18n")
                     if not os.path.exists(i18nPath):
                         os.makedirs(i18nPath)
                     self.__ericProject.setDirty(True)
                     
-                    self.__setCurrentProject(project)
+                    combinedOutput = False
+                    argsLists = []
+                    
+                    # 1. create a Python virtual environment for the project
+                    argsLists.append([sys.executable, "-m", "venv", "env"])
+                    # 2. upgrade packaging tools
+                    python = self.__assemblePyramidCommand(
+                        "python", os.path.join(projectPath, "env"))
+                    argsLists.append([python, "-m", "pip", "install",
+                                      "--upgrade", "pip", "setuptools"])
+                    # 3. install project in editable mode with testing
+                    argsLists.append([python, "-m", "pip", "install", "-e",
+                                      ".[testing]"])
+                    
+                    if (
+                        "backend" in contextData and
+                        contextData["backend"] == "sqlalchemy"
+                    ):
+                        # only SQLAlchemy needs initialization of alembic
+                        combinedOutput = True
+                        
+                        # 4. initialize database
+                        alembic = self.__assemblePyramidCommand(
+                            "alembic", os.path.join(projectPath, "env"))
+                        argsLists.append([alembic, "-c", "development.ini",
+                                          "revision", "--autogenerate",
+                                          "--message", "initialized database"])
+                        # 5. upgrade database to initial version
+                        argsLists.append([alembic, "-c", "development.ini",
+                                          "upgrade", "head"])
+                    
+                    dlg = PyramidDialog(
+                        self.tr("Initializing Pyramid Project"),
+                        linewrap=False, combinedOutput=combinedOutput,
+                        parent=self.__ui)
+                    if dlg.startBatchProcesses(argsLists,
+                                               workingDir=projectPath):
+                        dlg.exec()
+                    
+                    self.__setCurrentProject(contextData["repo_name"])
+                    
+                    if (
+                        "backend" in contextData and
+                        contextData["backend"] == "sqlalchemy"
+                    ):
+                        # add the alembic files created above to the project
+                        migrationsPath = self.migrationsPath()
+                        for entry in os.walk(migrationsPath):
+                            for fileName in entry[2]:
+                                fullName = os.path.join(entry[0], fileName)
+                                self.__ericProject.appendFile(fullName)
     
     ##################################################################
     ## methods below implement site related functions
@@ -973,7 +1135,7 @@
                 os.path.isdir(os.path.join(ppath, entry))
             ):
                 projects.append(entry)
-        return projects
+        return sorted(projects)
     
     def __selectProject(self):
         """
@@ -1002,9 +1164,9 @@
                 projects = None
         self.__setCurrentProject(project)
     
-    def __projectPath(self):
+    def projectPath(self):
         """
-        Private method to calculate the full path of the Pyramid project.
+        Public method to calculate the full path of the Pyramid project.
         
         @return path of the project
         @rtype str
@@ -1045,7 +1207,7 @@
         else:
             lowerProject = self.__project().lower()
             config = configparser.ConfigParser()
-            config.read(os.path.join(self.__projectPath(), "setup.cfg"))
+            config.read(os.path.join(self.projectPath(), "setup.cfg"))
             try:
                 outputDir = config.get("init_catalog", "output_dir")
             except (configparser.NoOptionError, configparser.NoSectionError):
@@ -1061,9 +1223,16 @@
         
         if self.__currentProject is None:
             self.initializeDbAct.setEnabled(False)
+            with contextlib.suppress(KeyError):
+                self.__menus["database"].setEnabled(False)
         else:
             initCmd = self.__getInitDbCommand()
             self.initializeDbAct.setEnabled(os.path.exists(initCmd))
+            
+            alembicDir = os.path.join(
+                self.projectPath(), self.__currentProject,
+                "alembic", "versions")
+            self.__menus["database"].setEnabled(os.path.exists(alembicDir))
     
     def __project(self):
         """
@@ -1086,18 +1255,15 @@
     ## slots below implement run functions
     ##################################################################
     
-    def __runServer(self, logging=False):
+    def __runServer(self):
         """
         Private slot to start the Pyramid Web server.
-        
-        @param logging flag indicating to enable logging
-        @type bool
         """
         consoleCmd = self.isSpawningConsole(
             self.__plugin.getPreferences("ConsoleCommand"))[1]
         if consoleCmd:
             try:
-                projectPath = self.__projectPath()
+                projectPath = self.projectPath()
             except PyramidNoProjectSelectedException:
                 EricMessageBox.warning(
                     self.__ui,
@@ -1108,9 +1274,10 @@
             
             args = Utilities.parseOptionString(consoleCmd)
             args[0] = Utilities.getExecutablePath(args[0])
-            args.append(self.getPyramidCommand("pserve"))
-            if logging:
-                args.append("--log-file=server.log")
+            args.append(self.getPyramidCommand(
+                "pserve",
+                virtualEnv=self.getProjectVirtualEnvironment()
+            ))
             args.append("--reload")
             args.append(os.path.join(projectPath, "development.ini"))
             
@@ -1132,12 +1299,6 @@
                     self.tr('Process Generation Error'),
                     self.tr('The Pyramid server could not be started.'))
     
-    def __runLoggingServer(self):
-        """
-        Private slot to start the Pyramid Web server with logging.
-        """
-        self.__runServer(True)
-    
     def __serverProcFinished(self):
         """
         Private slot connected to the finished signal.
@@ -1156,7 +1317,7 @@
         Private slot to start the default web browser with the server URL.
         """
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1168,10 +1329,10 @@
         config = configparser.ConfigParser()
         config.read(os.path.join(projectPath, "development.ini"))
         try:
-            port = config.get("server:main", "port")
+            listen = config.get("server:main", "listen")
         except (configparser.NoOptionError, configparser.NoSectionError):
-            port = "8080"
-        url = "http://localhost:{0}".format(port)
+            listen = "localhost:6543"
+        url = "http://{0}".format(listen)
         if self.__plugin.getPreferences("UseExternalBrowser"):
             res = QDesktopServices.openUrl(QUrl(url))
             if not res:
@@ -1191,7 +1352,7 @@
             self.__plugin.getPreferences("ConsoleCommand"))[1]
         if consoleCmd:
             try:
-                projectPath = self.__projectPath()
+                projectPath = self.projectPath()
             except PyramidNoProjectSelectedException:
                 EricMessageBox.warning(
                     self.__ui,
@@ -1202,7 +1363,10 @@
             
             args = Utilities.parseOptionString(consoleCmd)
             args[0] = Utilities.getExecutablePath(args[0])
-            args.append(self.getPyramidCommand("pshell"))
+            args.append(self.getPyramidCommand(
+                "pshell",
+                virtualEnv=self.getProjectVirtualEnvironment()
+            ))
             consoleType = self.__plugin.getPreferences("Python3ConsoleType")
             args.append("--python-shell={0}".format(consoleType))
             args.append(os.path.join(projectPath, "development.ini"))
@@ -1218,41 +1382,6 @@
                             ' started.'))
 
     ##################################################################
-    ## slots below implement setup functions
-    ##################################################################
-    
-    def __setupDevelop(self):
-        """
-        Private slot to set up the development environment for the current
-        project.
-        """
-        title = self.tr("Setup Development Environment")
-        try:
-            wd = self.__projectPath()
-        except PyramidNoProjectSelectedException:
-            EricMessageBox.warning(
-                self.__ui,
-                title,
-                self.tr('No current Pyramid project selected or no Pyramid'
-                        ' project created yet. Aborting...'))
-            return
-        
-        cmd = self.getPythonCommand()
-        args = []
-        args.append("setup.py")
-        args.append("develop")
-        
-        dia = PyramidDialog(
-            title,
-            msgSuccess=self.tr("Pyramid development environment setup"
-                               " successfully."))
-        res = dia.startProcess(cmd, args, wd)
-        if res:
-            dia.exec()
-            initCmd = self.__getInitDbCommand()
-            self.initializeDbAct.setEnabled(os.path.exists(initCmd))
-    
-    ##################################################################
     ## slots below implement distribution functions
     ##################################################################
     
@@ -1263,7 +1392,7 @@
         """
         title = self.tr("Build Distribution File")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1279,7 +1408,10 @@
         dlg = DistributionTypeSelectionDialog(self, projectPath, self.__ui)
         if dlg.exec() == QDialog.DialogCode.Accepted:
             formats = dlg.getFormats()
-            cmd = self.getPythonCommand()
+            cmd = self.getPyramidCommand(
+                "python",
+                virtualEnv=self.getProjectVirtualEnvironment()
+            )
             args = []
             args.append("setup.py")
             args.append("sdist")
@@ -1295,54 +1427,6 @@
                 dia.exec()
     
     ##################################################################
-    ## slots below implement database functions
-    ##################################################################
-    
-    def __getInitDbCommand(self):
-        """
-        Private method to create the path to the initialization script.
-        
-        @return path to the initialization script
-        @rtype str
-        """
-        try:
-            cmd = "initialize_{0}_db".format(self.__project())
-            return self.getPyramidCommand(cmd)
-        except PyramidNoProjectSelectedException:
-            EricMessageBox.warning(
-                self.__ui,
-                self.tr("Initialize Database"),
-                self.tr('No current Pyramid project selected or no Pyramid'
-                        ' project created yet. Aborting...'))
-            return ""
-    
-    def __initializeDatabase(self):
-        """
-        Private slot to initialize the database of the Pyramid project.
-        """
-        title = self.tr("Initialize Database")
-        try:
-            projectPath = self.__projectPath()
-        except PyramidNoProjectSelectedException:
-            EricMessageBox.warning(
-                self.__ui,
-                title,
-                self.tr('No current Pyramid project selected or no Pyramid'
-                        ' project created yet. Aborting...'))
-            return
-        
-        cmd = self.__getInitDbCommand()
-        args = []
-        args.append("development.ini")
-        
-        dia = PyramidDialog(
-            title,
-            msgSuccess=self.tr("Database initialized successfully."))
-        res = dia.startProcess(cmd, args, projectPath)
-        if res:
-            dia.exec()
-    
-    ##################################################################
     ## slots below implement various debugging functions
     ##################################################################
     
@@ -1352,7 +1436,7 @@
         """
         title = self.tr("Show Matching Views")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1370,7 +1454,10 @@
         if not ok or url == "":
             return
         
-        cmd = self.getPyramidCommand("pviews")
+        cmd = self.getPyramidCommand(
+            "pviews",
+            virtualEnv=self.getProjectVirtualEnvironment()
+        )
         args = []
         args.append("development.ini")
         args.append(url)
@@ -1386,7 +1473,7 @@
         """
         title = self.tr("Show Routes")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1408,7 +1495,7 @@
         """
         title = self.tr("Show Tween Objects")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1417,7 +1504,10 @@
                         ' project created yet. Aborting...'))
             return
         
-        cmd = self.getPyramidCommand("ptweens")
+        cmd = self.getPyramidCommand(
+            "ptweens",
+            virtualEnv=self.getProjectVirtualEnvironment()
+        )
         args = []
         args.append("development.ini")
 
@@ -1505,7 +1595,7 @@
         """
         title = self.tr("Extract messages")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1559,7 +1649,7 @@
         title = self.tr(
             "Initializing message catalog for '{0}'").format(code)
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1596,7 +1686,7 @@
         """
         title = self.tr("Compiling message catalogs")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1633,7 +1723,7 @@
         """
         title = self.tr("Compiling message catalogs")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1686,7 +1776,7 @@
         """
         title = self.tr("Updating message catalogs")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1716,7 +1806,7 @@
         """
         title = self.tr("Updating message catalogs")
         try:
-            projectPath = self.__projectPath()
+            projectPath = self.projectPath()
         except PyramidNoProjectSelectedException:
             EricMessageBox.warning(
                 self.__ui,
@@ -1763,7 +1853,7 @@
         editor = self.__plugin.getPreferences("TranslationsEditor")
         if poFile.endswith(".po") and editor:
             try:
-                wd = self.__projectPath()
+                wd = self.projectPath()
             except PyramidNoProjectSelectedException:
                 wd = ""
             started, pid = QProcess.startDetached(editor, [poFile], wd)
@@ -1774,3 +1864,199 @@
                     self.tr('The translations editor process ({0}) could'
                             ' not be started.').format(
                         os.path.basename(editor)))
+    
+    #######################################################################
+    ## database related methods and slots below
+    #######################################################################
+    
+    def getAlembicCommand(self):
+        """
+        Public method to get the path to the alembic executable of the current
+        Pyramid project.
+        
+        @return path to the alembic executable
+        @rtype str
+        """
+        return self.getPyramidCommand(
+            "alembic",
+            virtualEnv=self.getProjectVirtualEnvironment()
+        )
+    
+    def migrationsPath(self):
+        """
+        Public method to get the path to the migrations directory of the
+        current Pyramid project.
+        
+        @return pathof the directory containing the migrations
+        @rtype str
+        """
+        return os.path.join(self.projectPath(), self.__currentProject,
+                            "alembic", "versions")
+    
+    def __getInitDbCommand(self):
+        """
+        Private method to create the path to the initialization script.
+        
+        @return path to the initialization script
+        @rtype str
+        """
+        try:
+            cmd = "initialize_{0}_db".format(self.__project())
+            return self.getPyramidCommand(
+                cmd,
+                virtualEnv=self.getProjectVirtualEnvironment()
+            )
+        except PyramidNoProjectSelectedException:
+            EricMessageBox.warning(
+                self.__ui,
+                self.tr("Initialize Database"),
+                self.tr('No current Pyramid project selected or no Pyramid'
+                        ' project created yet. Aborting...'))
+            return ""
+    
+    @pyqtSlot()
+    def __initializeDatabase(self):
+        """
+        Private slot to initialize the database of the Pyramid project.
+        """
+        title = self.tr("Initialize Database")
+        try:
+            projectPath = self.projectPath()
+        except PyramidNoProjectSelectedException:
+            EricMessageBox.warning(
+                self.__ui,
+                title,
+                self.tr('No current Pyramid project selected or no Pyramid'
+                        ' project created yet. Aborting...'))
+            return
+        
+        cmd = self.__getInitDbCommand()
+        args = []
+        args.append("development.ini")
+        
+        dia = PyramidDialog(
+            title,
+            msgSuccess=self.tr("Database initialized successfully."))
+        res = dia.startProcess(cmd, args, projectPath)
+        if res:
+            dia.exec()
+    
+    @pyqtSlot()
+    def __createMigration(self):
+        """
+        Private slot to create a new database migration.
+        """
+        title = self.tr("Create Migration")
+        projectPath = self.projectPath()
+        migrations = self.migrationsPath()
+        
+        message, ok = QInputDialog.getText(
+            None,
+            title,
+            self.tr("Enter a short message for the migration:"),
+            QLineEdit.EchoMode.Normal)
+        if ok:
+            args = ["-c", "development.ini", "revision", "--autogenerate"]
+            if migrations:
+                args += ["--version-path", migrations]
+            if message:
+                args += ["--message", message]
+            
+            dlg = PyramidDialog(
+                title,
+                msgSuccess=self.tr("\nMigration created successfully."),
+                linewrap=False, combinedOutput=True,
+                parent=self.__ui
+            )
+            if dlg.startProcess(self.getAlembicCommand(), args,
+                                workingDir=projectPath):
+                dlg.exec()
+                if dlg.normalExit():
+                    versionsPattern = os.path.join(migrations, "*.py")
+                    for fileName in glob.iglob(versionsPattern):
+                        self.__ericProject.appendFile(fileName)
+    
+    @pyqtSlot()
+    def upgradeDatabase(self, revision=None):
+        """
+        Public slot to upgrade the database to the head or a given version.
+        
+        @param revision migration revision to upgrade to
+        @type str
+        """
+        title = self.tr("Upgrade Database")
+        projectPath = self.projectPath()
+        
+        args = ["-c", "development.ini", "upgrade"]
+        if revision:
+            args.append(revision)
+        else:
+            args.append("head")
+        
+        dlg = PyramidDialog(
+            title,
+            msgSuccess=self.tr("\nDatabase upgraded successfully."),
+            linewrap=False, combinedOutput=True, parent=self.__ui
+        )
+        if dlg.startProcess(self.getAlembicCommand(), args,
+                            workingDir=projectPath):
+            dlg.exec()
+    
+    @pyqtSlot()
+    def downgradeDatabase(self, revision=None):
+        """
+        Public slot to downgrade the database to the previous or a given
+        version.
+        
+        @param revision migration revision to downgrade to
+        @type str
+        """
+        title = self.tr("Downgrade Database")
+        projectPath = self.projectPath()
+        
+        args = ["-c", "development.ini", "downgrade"]
+        if revision:
+            args.append(revision)
+        else:
+            args.append("-1")
+        
+        dlg = PyramidDialog(
+            title,
+            msgSuccess=self.tr("\nDatabase downgraded successfully."),
+            linewrap=False, combinedOutput=True, parent=self.__ui
+        )
+        if dlg.startProcess(self.getAlembicCommand(), args,
+                            workingDir=projectPath):
+            dlg.exec()
+    
+    @pyqtSlot()
+    def __showMigrationsSummary(self):
+        """
+        Private slot to show a migrations history summary.
+        """
+        from .MigrateSummaryDialog import MigrateSummaryDialog
+        
+        if self.__migrationSummaryDialog is None:
+            self.__migrationSummaryDialog = MigrateSummaryDialog(
+                self, parent=self.__ui)
+        
+        self.__migrationSummaryDialog.showSummary()
+    
+    @pyqtSlot()
+    def __showMigrationsHistory(self):
+        """
+        Private slot to show the full migrations history.
+        """
+        title = self.tr("Migrations History")
+        projectPath = self.projectPath()
+        
+        args = ["-c", "development.ini", "history", "--indicate-current",
+                "--verbose"]
+        
+        dlg = PyramidDialog(
+            title,
+            linewrap=False, combinedOutput=True, parent=self.__ui
+        )
+        if dlg.startProcess(self.getAlembicCommand(), args,
+                            workingDir=projectPath):
+            dlg.exec()

eric ide

mercurial