diff -r dd3f6bfb85f7 -r f31df56510a1 ProjectFlask/Project.py --- a/ProjectFlask/Project.py Sat Nov 21 17:50:57 2020 +0100 +++ b/ProjectFlask/Project.py Sat Nov 21 20:37:54 2020 +0100 @@ -8,12 +8,11 @@ """ import os -import re from PyQt5.QtCore import ( pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer ) -from PyQt5.QtWidgets import QMenu, QDialog +from PyQt5.QtWidgets import QMenu from E5Gui import E5MessageBox from E5Gui.E5Action import E5Action @@ -25,10 +24,10 @@ import Utilities from .FlaskCommandDialog import FlaskCommandDialog -from .PyBabelCommandDialog import PyBabelCommandDialog + +from .FlaskBabelExtension.PyBabelProjectExtension import PyBabelProject -# TODO: move PyBabel related code to a separate package (FlaskBabelExtension) # TODO: move database related code to a separate package (FlaskMigrateExtension) class Project(QObject): """ @@ -72,10 +71,9 @@ "werkzeug": "", } - self.__capabilities = { - "pybabel": False, - "migrate": False, - } + self.__capabilities = {} + + self.__pybabelProject = PyBabelProject(self.__plugin, self, self.__ui) def initActions(self): """ @@ -201,7 +199,8 @@ """<b>Configure PyBabel</b>""" """<p>Shows a dialog to edit the configuration for pybabel.</p>""" )) - self.pybabelConfigAct.triggered.connect(self.__configurePybabel) + self.pybabelConfigAct.triggered.connect( + self.__pybabelProject.configurePyBabel) self.actions.append(self.pybabelConfigAct) ################################## @@ -296,21 +295,6 @@ """ return list(self.__menus.keys()) - def registerOpenHook(self): - """ - Public method to register the open hook to open a translations file - in a translations editor. - """ - if self.__hooksInstalled: - editor = self.__plugin.getPreferences("TranslationsEditor") - if editor: - self.__translationsBrowser.addHookMethodAndMenuEntry( - "open", self.openPOEditor, - self.tr("Open with {0}").format( - os.path.basename(editor))) - else: - self.__translationsBrowser.removeHookMethod("open") - def projectOpenedHooks(self): """ Public method to add our hook methods. @@ -325,66 +309,19 @@ ## self.__determineCapabilities() - if self.__capabilities["pybabel"]: - self.__e5project.projectLanguageAddedByCode.connect( - self.__projectLanguageAdded) - self.__translationsBrowser = ( - e5App().getObject("ProjectBrowser") - .getProjectBrowser("translations")) - self.__translationsBrowser.addHookMethodAndMenuEntry( - "extractMessages", self.extractMessages, - self.tr("Extract Messages")) - self.__translationsBrowser.addHookMethodAndMenuEntry( - "releaseAll", self.compileCatalogs, - self.tr("Compile All Catalogs")) - self.__translationsBrowser.addHookMethodAndMenuEntry( - "releaseSelected", self.compileSelectedCatalogs, - self.tr("Compile Selected Catalogs")) - self.__translationsBrowser.addHookMethodAndMenuEntry( - "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 - - self.registerOpenHook() + self.__pybabelProject.projectOpenedHooks() +## self.__hooksInstalled = True def projectClosedHooks(self): """ Public method to remove our hook methods. """ + self.__pybabelProject.projectClosedHooks() + if self.__hooksInstalled: ## self.__formsBrowser.removeHookMethod("newForm") ## self.__formsBrowser = None -## - 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( - "generateAllWithObsolete") - self.__translationsBrowser.removeHookMethod( - "generateSelected") - self.__translationsBrowser.removeHookMethod( - "generateSelectedWithObsolete") - self.__translationsBrowser.removeHookMethod( - "open") - self.__translationsBrowser = None + pass self.__hooksInstalled = False @@ -483,20 +420,11 @@ @return full flask command @rtype str """ - return self.__getFullCommand("flask") + return self.getFullCommand("flask") - def getBabelCommand(self): + def getFullCommand(self, command): """ - Public method to build the Babel command. - - @return full pybabel command - @rtype str - """ - return self.__getFullCommand("pybabel") - - def __getFullCommand(self, command): - """ - Private method to get the full command for a given command name. + Public method to get the full command for a given command name. @param command command name @type str @@ -683,12 +611,37 @@ extensions. """ # 1. support for flask-babel (i.e. pybabel) - self.__capabilities["pybabel"] = self.flaskBabelAvailable() - self.pybabelConfigAct.setEnabled(self.__capabilities["pybabel"]) + self.__pybabelProject.determineCapability() + self.pybabelConfigAct.setEnabled(self.hasCapability("pybabel")) # 2. support for flask-migrate # TODO: add support for flask-migrate + def hasCapability(self, key): + """ + Public method to check, if a capability is available. + + @param key key of the capability to check + @type str + @return flag indicating the availability of the capability + @rtype bool + """ + try: + return self.__capabilities[key] + except KeyError: + return False + + def setCapability(self, key, available): + """ + Public method to set the availability status of a capability. + + @param key key of the capability to set + @type str + @param available flag indicating the availability of the capability + @type bool + """ + self.__capabilities[key] = available + ################################################################## ## slot below implements project specific flask configuration ################################################################## @@ -821,431 +774,3 @@ dlg = FlaskCommandDialog(self) if dlg.startCommand("init-db"): dlg.exec() - - ################################################################## - ## slots and methods below implement i18n and l10n support - ################################################################## - - def flaskBabelAvailable(self): - """ - Public method to check, if the 'flask-babel' package is available. - - @return flag indicating the availability of 'flask-babel' - @rtype bool - """ - venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3") - interpreter = self.__virtualEnvManager.getVirtualenvInterpreter( - venvName) - if interpreter and Utilities.isinpath(interpreter): - detector = os.path.join( - os.path.dirname(__file__), "FlaskBabelDetector.py") - proc = QProcess() - proc.setProcessChannelMode(QProcess.MergedChannels) - proc.start(interpreter, [detector]) - finished = proc.waitForFinished(30000) - if finished and proc.exitCode() == 0: - return True - - return False - - @pyqtSlot() - def __configurePybabel(self): - """ - Private slot to show a dialog to edit the pybabel configuration. - """ - from .PyBabelConfigDialog import PyBabelConfigDialog - - config = self.getData("pybabel", "") - dlg = PyBabelConfigDialog(config) - if dlg.exec() == QDialog.Accepted: - 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): - self.__createBabelCfg(cfgFileName) - - def __ensurePybabelConfigured(self): - """ - Private method to ensure, that PyBabel has been configured. - - @return flag indicating successful configuration - @rtype bool - """ - config = self.getData("pybabel", "") - if not config: - self.__configurePybabel() - return True - - configFileName = self.getData("pybabel", "configFile") - if configFileName: - cfgFileName = self.__e5project.getAbsoluteUniversalPath( - configFileName) - if os.path.exists(cfgFileName): - return True - else: - return self.__createBabelCfg(cfgFileName) - - return False - - def __createBabelCfg(self, configFile): - """ - 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 - """ - _, app = self.getApplication() - if app.endswith(".py"): - template = ( - "[python: {0}]\n" - "[jinja2: templates/**.html]\n" - "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n" - ) - else: - template = ( - "[python: {0}/**.py]\n" - "[jinja2: {0}/templates/**.html]\n" - "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n" - ) - try: - with open(configFile, "w") as f: - f.write(template.format(app)) - self.__e5project.appendFile(configFile) - E5MessageBox.information( - None, - self.tr("Generate PyBabel Configuration File"), - self.tr("""The PyBabel configuration file was created.""" - """ Please edit it to adjust the entries as""" - """ required.""") - ) - return True - except EnvironmentError as err: - E5MessageBox.warning( - None, - self.tr("Generate PyBabel Configuration File"), - self.tr("""<p>The PyBabel Configuration File could not be""" - """ generated.</p><p>Reason: {0}</p>""") - .format(str(err)) - ) - return False - - 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): - """ - Public method to edit the given file in an external .po editor. - - @param poFile name of the .po file - @type str - """ - editor = self.__plugin.getPreferences("TranslationsEditor") - if poFile.endswith(".po") and editor: - wd, _ = self.getApplication() - started, pid = QProcess.startDetached(editor, [poFile], wd) - if not started: - E5MessageBox.critical( - None, - self.tr('Process Generation Error'), - self.tr('The translations editor process ({0}) could' - ' not be started.').format( - os.path.basename(editor))) - - def extractMessages(self): - """ - Public method to extract the messages catalog template file. - """ - title = self.tr("Extract messages") - if self.__ensurePybabelConfigured(): - workdir = self.getApplication()[0] - potFile = self.__e5project.getAbsoluteUniversalPath( - self.getData("pybabel", "catalogFile")) - - try: - potFilePath = os.path.dirname(potFile) - os.makedirs(potFilePath) - except EnvironmentError: - pass - - args = [ - "-F", - 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", - os.path.relpath(potFile, workdir), - "." - ] - - dlg = PyBabelCommandDialog( - self, title, - msgSuccess=self.tr("\nMessages extracted successfully.") - ) - res = dlg.startCommand("extract", args, workdir) - if res: - dlg.exec() - self.__e5project.appendFile(potFile) - - 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, 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, 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, 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)