ProjectFlask/Project.py

changeset 16
dd3f6bfb85f7
parent 15
3f5c05eb2d5f
child 17
f31df56510a1
--- a/ProjectFlask/Project.py	Thu Nov 19 20:19:55 2020 +0100
+++ b/ProjectFlask/Project.py	Sat Nov 21 17:50:57 2020 +0100
@@ -8,6 +8,7 @@
 """
 
 import os
+import re
 
 from PyQt5.QtCore import (
     pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer
@@ -24,8 +25,11 @@
 import Utilities
 
 from .FlaskCommandDialog import FlaskCommandDialog
+from .PyBabelCommandDialog import PyBabelCommandDialog
 
 
+# TODO: move PyBabel related code to a separate package (FlaskBabelExtension)
+# TODO: move database related code to a separate package (FlaskMigrateExtension)
 class Project(QObject):
     """
     Class implementing the Flask project support.
@@ -67,6 +71,11 @@
             "flask": "",
             "werkzeug": "",
         }
+        
+        self.__capabilities = {
+            "pybabel": False,
+            "migrate": False,
+        }
     
     def initActions(self):
         """
@@ -178,7 +187,7 @@
         self.actions.append(self.initDatabaseAct)
         
         ##################################
-        ## database action below        ##
+        ## pybabel action below         ##
         ##################################
         
         self.pybabelConfigAct = E5Action(
@@ -243,19 +252,18 @@
         menu = QMenu(self.tr('&Flask'), self.__ui)
         menu.setTearOffEnabled(True)
         
-        menu.addSection("flask run")
         menu.addAction(self.runServerAct)
         menu.addAction(self.runDevServerAct)
         menu.addAction(self.askForServerOptionsAct)
-        menu.addSection("flask shell")
+        menu.addSeparator()
         menu.addAction(self.runPythonShellAct)
-        menu.addSection("flask routes")
+        menu.addSeparator()
         menu.addAction(self.showRoutesAct)
-        menu.addSection("flask init-db")
+        menu.addSeparator()
         menu.addAction(self.initDatabaseAct)
-        menu.addSection(self.tr("Translations"))
+        menu.addSeparator()
         menu.addAction(self.pybabelConfigAct)
-        menu.addSection(self.tr("Various"))
+        menu.addSeparator()
         menu.addAction(self.documentationAct)
         menu.addSeparator()
         menu.addAction(self.aboutFlaskAct)
@@ -308,13 +316,16 @@
         Public method to add our hook methods.
         """
         if self.__e5project.getProjectType() == "Flask":
+            # TODO: add some methods for standard templates
 ##            self.__formsBrowser = (
 ##                e5App().getObject("ProjectBrowser")
 ##                .getProjectBrowser("forms"))
 ##            self.__formsBrowser.addHookMethodAndMenuEntry(
 ##                "newForm", self.newForm, self.tr("New template..."))
 ##            
-            if self.flaskBabelAvailable():
+            self.__determineCapabilities()
+            
+            if self.__capabilities["pybabel"]:
                 self.__e5project.projectLanguageAddedByCode.connect(
                     self.__projectLanguageAdded)
                 self.__translationsBrowser = (
@@ -333,8 +344,15 @@
                     "generateAll", self.updateCatalogs,
                     self.tr("Update All Catalogs"))
                 self.__translationsBrowser.addHookMethodAndMenuEntry(
+                    "generateAllWithObsolete", self.updateCatalogsObsolete,
+                    self.tr("Update All Catalogs (with obsolete)"))
+                self.__translationsBrowser.addHookMethodAndMenuEntry(
                     "generateSelected", self.updateSelectedCatalogs,
                     self.tr("Update Selected Catalogs"))
+                self.__translationsBrowser.addHookMethodAndMenuEntry(
+                    "generateSelectedWithObsolete",
+                    self.updateSelectedCatalogsObsolete,
+                    self.tr("Update Selected Catalogs (with obsolete)"))
                 
                 self.__hooksInstalled = True
             
@@ -350,12 +368,22 @@
 ##            
             self.__e5project.projectLanguageAddedByCode.disconnect(
                 self.__projectLanguageAdded)
-            self.__translationsBrowser.removeHookMethod("extractMessages")
-            self.__translationsBrowser.removeHookMethod("releaseAll")
-            self.__translationsBrowser.removeHookMethod("releaseSelected")
-            self.__translationsBrowser.removeHookMethod("generateAll")
-            self.__translationsBrowser.removeHookMethod("generateSelected")
-            self.__translationsBrowser.removeHookMethod("open")
+            self.__translationsBrowser.removeHookMethod(
+                "extractMessages")
+            self.__translationsBrowser.removeHookMethod(
+                "releaseAll")
+            self.__translationsBrowser.removeHookMethod(
+                "releaseSelected")
+            self.__translationsBrowser.removeHookMethod(
+                "generateAll")
+            self.__translationsBrowser.removeHookMethod(
+                "generateAllWithObsolete")
+            self.__translationsBrowser.removeHookMethod(
+                "generateSelected")
+            self.__translationsBrowser.removeHookMethod(
+                "generateSelectedWithObsolete")
+            self.__translationsBrowser.removeHookMethod(
+                "open")
             self.__translationsBrowser = None
         
         self.__hooksInstalled = False
@@ -649,8 +677,33 @@
         self.__e5project.setData(
             "PROJECTTYPESPECIFICDATA", category, self.__projectData[category])
     
+    def __determineCapabilities(self):
+        """
+        Private method to determine capabilities provided by supported
+        extensions.
+        """
+        # 1. support for flask-babel (i.e. pybabel)
+        self.__capabilities["pybabel"] = self.flaskBabelAvailable()
+        self.pybabelConfigAct.setEnabled(self.__capabilities["pybabel"])
+        
+        # 2. support for flask-migrate
+        # TODO: add support for flask-migrate
+    
     ##################################################################
-    ## slots below implement documentation functions
+    ## slot below implements project specific flask configuration
+    ##################################################################
+    
+    @pyqtSlot()
+    def __configureFlaskForProject(self):
+        """
+        Private slot to configure the project specific flask parameters.
+        """
+        # TODO: implement the flask project config dialog
+        # 1. check boxes to override flask-babel and flask-migrate
+        # 2. support for project specific virtual environment
+    
+    ##################################################################
+    ## slot below implements documentation function
     ##################################################################
     
     def __showDocumentation(self):
@@ -759,13 +812,14 @@
             dlg.show()
             self.__routesDialog = dlg
     
+    # TODO: replace this by commands made by flask-migrate (flask db ...)
     @pyqtSlot()
     def __initDatabase(self):
         """
         Private slot showing the result of the database creation.
         """
         dlg = FlaskCommandDialog(self)
-        if dlg.startFlaskCommand("init-db"):
+        if dlg.startCommand("init-db"):
             dlg.exec()
     
     ##################################################################
@@ -799,7 +853,6 @@
         """
         Private slot to show a dialog to edit the pybabel configuration.
         """
-        # TODO: implement this
         from .PyBabelConfigDialog import PyBabelConfigDialog
         
         config = self.getData("pybabel", "")
@@ -808,6 +861,12 @@
             config = dlg.getConfiguration()
             self.setData("pybabel", "", config)
             
+            self.__e5project.setTranslationPattern(os.path.join(
+                config["translationsDirectory"], "%language%", "LC_MESSAGES",
+                "{0}.po".format(config["domain"])
+            ))
+            self.__e5project.setDirty(True)
+            
             cfgFileName = self.__e5project.getAbsoluteUniversalPath(
                 config["configFile"])
             if not os.path.exists(cfgFileName):
@@ -840,6 +899,8 @@
         """
         Private method to create a template pybabel configuration file.
         
+        @param configFile name of the configuration file to be created
+        @type str
         @return flag indicating successful configuration file creation
         @rtype bool
         """
@@ -878,9 +939,30 @@
             )
             return False
     
-    def __projectLanguageAdded(self, code):
-        # TODO: implement this with pybabel ...
-        pass
+    def __getLocale(self, filename):
+        """
+        Private method to extract the locale out of a file name.
+        
+        @param filename name of the file used for extraction
+        @type str
+        @return extracted locale
+        @rtype str or None
+        """
+        if self.__e5project.getTranslationPattern():
+            filename = os.path.splitext(filename)[0] + ".po"
+            
+            # On Windows, path typically contains backslashes. This leads
+            # to an invalid search pattern '...\(' because the opening bracket
+            # will be escaped.
+            pattern = self.__e5project.getTranslationPattern()
+            pattern = os.path.normpath(pattern)
+            pattern = pattern.replace("%language%", "(.*?)")
+            pattern = pattern.replace('\\', '\\\\')
+            match = re.search(pattern, filename)
+            if match is not None:
+                return match.group(1)
+        
+        return None
     
     def openPOEditor(self, poFile):
         """
@@ -907,6 +989,7 @@
         """
         title = self.tr("Extract messages")
         if self.__ensurePybabelConfigured():
+            workdir = self.getApplication()[0]
             potFile = self.__e5project.getAbsoluteUniversalPath(
                 self.getData("pybabel", "catalogFile"))
             
@@ -918,41 +1001,251 @@
             
             args = [
                 "-F",
-                self.__e5project.getAbsoluteUniversalPath(
-                    self.getData("pybabel", "configFile"))
+                os.path.relpath(
+                    self.__e5project.getAbsoluteUniversalPath(
+                        self.getData("pybabel", "configFile")),
+                    workdir
+                )
             ]
             if self.getData("pybabel", "markersList"):
                 for marker in self.getData("pybabel", "markersList"):
                     args += ["-k", marker]
             args += [
                 "-o",
-                potFile,
+                os.path.relpath(potFile, workdir),
                 "."
             ]
             
-            dlg = FlaskCommandDialog(self)
-            res = dlg.startBabelCommand(
-                "extract", args, title,
+            dlg = PyBabelCommandDialog(
+                self, title,
                 msgSuccess=self.tr("\nMessages extracted successfully.")
             )
+            res = dlg.startCommand("extract", args, workdir)
             if res:
                 dlg.exec()
                 self.__e5project.appendFile(potFile)
-        # TODO: implement this with pybabel ...
-        pass
     
-    def compileCatalogs(self):
-        # TODO: implement this with pybabel ...
-        pass
+    def __projectLanguageAdded(self, code):
+        """
+        Private slot handling the addition of a new language.
+        
+        @param code language code of the new language
+        @type str
+        """
+        title = self.tr(
+            "Initializing message catalog for '{0}'").format(code)
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.getApplication()[0]
+            langFile = self.__e5project.getAbsoluteUniversalPath(
+                self.__e5project.getTranslationPattern().replace(
+                    "%language%", code))
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "catalogFile"))
+            
+            args = [
+                "--domain={0}".format(self.getData("pybabel", "domain")),
+                "--input-file={0}".format(os.path.relpath(potFile, workdir)),
+                "--output-file={0}".format(os.path.relpath(langFile, workdir)),
+                "--locale={0}".format(code),
+            ]
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr(
+                    "\nMessage catalog initialized successfully.")
+            )
+            res = dlg.startCommand("init", args, workdir)
+            if res:
+                dlg.exec()
+                
+                self.__e5project.appendFile(langFile)
+    
+    def compileCatalogs(self, filenames):
+        """
+        Public method to compile the message catalogs.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        """
+        title = self.tr("Compiling message catalogs")
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "translationsDirectory"))
+            
+            args = [
+                "--domain={0}".format(self.getData("pybabel", "domain")),
+                "--directory={0}".format(
+                    os.path.relpath(translationsDirectory, workdir)),
+                "--use-fuzzy",
+                "--statistics",
+            ]
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
+            )
+            res = dlg.startCommand("compile", args, workdir)
+            if res:
+                dlg.exec()
+            
+                for entry in os.walk(translationsDirectory):
+                    for fileName in entry[2]:
+                        fullName = os.path.join(entry[0], fileName)
+                        if fullName.endswith('.mo'):
+                            self.__e5project.appendFile(fullName)
     
-    def compileSelectedCatalogs(self):
-        # TODO: implement this with pybabel ...
-        pass
+    def compileSelectedCatalogs(self, filenames):
+        """
+        Public method to update the message catalogs.
+        
+        @param filenames list of file names
+        @type list of str
+        """
+        title = self.tr("Compiling message catalogs")
+        
+        locales = {self.__getLocale(f) for f in filenames}
+        
+        if len(locales) == 0:
+            E5MessageBox.warning(
+                self.__ui,
+                title,
+                self.tr('No locales detected. Aborting...'))
+            return
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "translationsDirectory"))
+            
+            argsList = []
+            for loc in locales:
+                argsList.append([
+                    "compile",
+                    "--domain={0}".format(self.getData("pybabel", "domain")),
+                    "--directory={0}".format(
+                        os.path.relpath(translationsDirectory, workdir)),
+                    "--use-fuzzy",
+                    "--statistics",
+                    "--locale={0}".format(loc),
+                ])
+            
+            dlg = PyBabelCommandDialog(
+                self, title=title,
+                msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
+            )
+            res = dlg.startBatchCommand(argsList, workdir)
+            if res:
+                dlg.exec()
+            
+                for entry in os.walk(translationsDirectory):
+                    for fileName in entry[2]:
+                        fullName = os.path.join(entry[0], fileName)
+                        if fullName.endswith('.mo'):
+                            self.__e5project.appendFile(fullName)
     
-    def updateCatalogs(self):
-        # TODO: implement this with pybabel ...
-        pass
+    def updateCatalogs(self, filenames, withObsolete=False):
+        """
+        Public method to update the message catalogs.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        @param withObsolete flag indicating to keep obsolete translations
+        @type bool
+        """
+        title = self.tr("Updating message catalogs")
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "translationsDirectory"))
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "catalogFile"))
+            
+            args = [
+                "--domain={0}".format(self.getData("pybabel", "domain")),
+                "--input-file={0}".format(os.path.relpath(potFile, workdir)),
+                "--output-dir={0}".format(
+                    os.path.relpath(translationsDirectory, workdir)),
+            ]
+            if not withObsolete:
+                args.append("--ignore-obsolete")
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr("\nMessage catalogs updated successfully.")
+            )
+            res = dlg.startCommand("update", args, workdir)
+            if res:
+                dlg.exec()
+    
+    def updateCatalogsObsolete(self, filenames):
+        """
+        Public method to update the message catalogs keeping obsolete
+        translations.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        """
+        self.updateCatalogs(filenames, withObsolete=True)
     
-    def updateSelectedCatalogs(self):
-        # TODO: implement this with pybabel ...
-        pass
+    def updateSelectedCatalogs(self, filenames, withObsolete=False):
+        """
+        Public method to update the selected message catalogs.
+        
+        @param filenames list of filenames
+        @type list of str
+        @param withObsolete flag indicating to keep obsolete translations
+        @type bool
+        """
+        title = self.tr("Updating message catalogs")
+        
+        locales = {self.__getLocale(f) for f in filenames}
+        
+        if len(locales) == 0:
+            E5MessageBox.warning(
+                self.__ui,
+                title,
+                self.tr('No locales detected. Aborting...'))
+            return
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "translationsDirectory"))
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.getData("pybabel", "catalogFile"))
+            argsList = []
+            for loc in locales:
+                args = [
+                    "update",
+                    "--domain={0}".format(self.getData("pybabel", "domain")),
+                    "--input-file={0}".format(
+                        os.path.relpath(potFile, workdir)),
+                    "--output-dir={0}".format(
+                        os.path.relpath(translationsDirectory, workdir)),
+                    "--locale={0}".format(loc),
+                ]
+                if not withObsolete:
+                    args.append("--ignore-obsolete")
+                argsList.append(args)
+            
+            dlg = PyBabelCommandDialog(
+                self, title=title,
+                msgSuccess=self.tr("\nMessage catalogs updated successfully.")
+            )
+            res = dlg.startBatchCommand(argsList, workdir)
+            if res:
+                dlg.exec()
+    
+    def updateSelectedCatalogsObsolete(self, filenames):
+        """
+        Public method to update the message catalogs keeping obsolete
+        translations.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        """
+        self.updateSelectedCatalogs(filenames, withObsolete=True)

eric ide

mercurial