Sat, 21 Nov 2020 20:37:54 +0100
Moved the flask-babel support into its own package.
--- a/PluginFlask.e4p Sat Nov 21 17:50:57 2020 +0100 +++ b/PluginFlask.e4p Sat Nov 21 20:37:54 2020 +0100 @@ -18,11 +18,13 @@ <Source>ProjectFlask/AnsiTools.py</Source> <Source>ProjectFlask/ConfigurationPage/FlaskPage.py</Source> <Source>ProjectFlask/ConfigurationPage/__init__.py</Source> - <Source>ProjectFlask/FlaskBabelDetector.py</Source> + <Source>ProjectFlask/FlaskBabelExtension/FlaskBabelDetector.py</Source> + <Source>ProjectFlask/FlaskBabelExtension/PyBabelCommandDialog.py</Source> + <Source>ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.py</Source> + <Source>ProjectFlask/FlaskBabelExtension/PyBabelProjectExtension.py</Source> + <Source>ProjectFlask/FlaskBabelExtension/__init__.py</Source> <Source>ProjectFlask/FlaskCommandDialog.py</Source> <Source>ProjectFlask/Project.py</Source> - <Source>ProjectFlask/PyBabelCommandDialog.py</Source> - <Source>ProjectFlask/PyBabelConfigDialog.py</Source> <Source>ProjectFlask/RoutesDialog.py</Source> <Source>ProjectFlask/RunServerDialog.py</Source> <Source>ProjectFlask/ServerStartOptionsDialog.py</Source> @@ -31,8 +33,8 @@ </Sources> <Forms> <Form>ProjectFlask/ConfigurationPage/FlaskPage.ui</Form> + <Form>ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.ui</Form> <Form>ProjectFlask/FlaskCommandDialog.ui</Form> - <Form>ProjectFlask/PyBabelConfigDialog.ui</Form> <Form>ProjectFlask/RoutesDialog.ui</Form> <Form>ProjectFlask/RunServerDialog.ui</Form> <Form>ProjectFlask/ServerStartOptionsDialog.ui</Form>
--- a/ProjectFlask/FlaskBabelDetector.py Sat Nov 21 17:50:57 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module to check for the presence of 'flask-babel' by importing it. -""" - -import sys - -if __name__ == "__main__": - try: - import flask_babel # __IGNORE_EXCEPTION__ __IGNORE_WARNING__ - ret = 0 - except ImportError: - ret = 1 - - sys.exit(ret)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskBabelExtension/FlaskBabelDetector.py Sat Nov 21 20:37:54 2020 +0100 @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module to check for the presence of 'flask-babel' by importing it. +""" + +import sys + +if __name__ == "__main__": + try: + import flask_babel # __IGNORE_EXCEPTION__ __IGNORE_WARNING__ + ret = 0 + except ImportError: + ret = 1 + + sys.exit(ret)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskBabelExtension/PyBabelCommandDialog.py Sat Nov 21 20:37:54 2020 +0100 @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to run a flask command and show its output. +""" + +from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton + +from E5Gui import E5MessageBox + +from ..Ui_FlaskCommandDialog import Ui_FlaskCommandDialog + + +class PyBabelCommandDialog(QDialog, Ui_FlaskCommandDialog): + """ + Class implementing a dialog to run a flask command and show its output. + """ + def __init__(self, project, title="", msgSuccess="", msgError="", + parent=None): + """ + Constructor + + @param project reference to the project object + @type Project + @param title window title of the dialog + @type str + @param msgSuccess success message to be shown + @type str + @param msgError message to be shown on error + @type str + @param parent reference to the parent widget + @type QWidget + """ + super(PyBabelCommandDialog, self).__init__(parent) + self.setupUi(self) + + if title: + self.setWindowTitle(title) + + self.__project = project + self.__successMessage = msgSuccess + self.__errorMessage = msgError + + self.__process = None + self.__argsLists = [] + self.__workdir = "" + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + + def startCommand(self, command, args, workdir, clearOutput=True): + """ + Public method to start a pybabel command and show its output. + + @param command pybabel command to be run + @type str + @param args list of command line arguments for the command + @type list of str + @param workdir working directory for the command + @type str + @param clearOutput flag indicating to clear the output + @type bool + @return flag indicating a successful start + @rtype bool + """ + babelCommand = self.__project.getBabelCommand() + + self.__process = QProcess() + self.__process.setWorkingDirectory(workdir) + self.__process.setProcessChannelMode(QProcess.MergedChannels) + + self.__process.readyReadStandardOutput.connect(self.__readStdOut) + self.__process.finished.connect(self.__processFinished) + + if clearOutput: + self.outputEdit.clear() + + babelArgs = [command] + if args: + babelArgs += args + + self.__process.start(babelCommand, babelArgs) + ok = self.__process.waitForStarted(10000) + if not ok: + E5MessageBox.critical( + None, + self.tr("Execute PyBabel Command"), + self.tr("""The pybabel process could not be started.""")) + else: + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setFocus( + Qt.OtherFocusReason) + + return ok + + def startBatchCommand(self, argsLists, workdir): + """ + Public method to start a pybabel command repeatedly with a list of + arguments and show the output. + + @param argsLists list of command line arguments for the batch commands + @type list of lists of str + @param workdir working directory for the command + @type str + @return flag indicating a successful start of the first process + @rtype bool + """ + self.__argsLists = argsLists[:] + self.__workdir = workdir + + # start the first process + args = self.__argsLists.pop(0) + res = self.startCommand(args[0], args[1:], workdir) + if not res: + self.__argsLists = [] + + return res + + def closeEvent(self, evt): + """ + Protected method handling the close event of the dialog. + + @param evt reference to the close event object + @type QCloseEvent + """ + self.__argsLists = [] + self.__cancelProcess() + evt.accept() + + @pyqtSlot() + def __readStdOut(self): + """ + Private slot to add the server process output to the output pane. + """ + if self.__process is not None: + out = str(self.__process.readAllStandardOutput(), "utf-8") + self.outputEdit.insertPlainText(out) + + def __processFinished(self, exitCode, exitStatus): + """ + Private slot connected to the finished signal. + + @param exitCode exit code of the process + @type int + @param exitStatus exit status of the process + @type QProcess.ExitStatus + """ + normal = (exitStatus == QProcess.NormalExit) and (exitCode == 0) + self.__cancelProcess() + + if self.__argsLists: + args = self.__argsLists.pop(0) + self.startCommand(args[0], args[1:], self.__workdir, + clearOutput=False) + return + + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Close).setFocus( + Qt.OtherFocusReason) + + if normal and self.__successMessage: + self.outputEdit.insertPlainText(self.__successMessage) + elif not normal and self.__errorMessage: + self.outputEdit.insertPlainText(self.__errorMessage) + + @pyqtSlot() + def __cancelProcess(self): + """ + Private slot to terminate the current process. + """ + if ( + self.__process is not None and + self.__process.state() != QProcess.NotRunning + ): + self.__process.terminate() + QTimer.singleShot(2000, self.__process.kill) + self.__process.waitForFinished(3000) + + self.__process = None + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot handling presses of the button box buttons. + + @param button reference to the button been clicked + @type QAbstractButton + """ + if button is self.buttonBox.button(QDialogButtonBox.Close): + self.close() + elif button is self.buttonBox.button(QDialogButtonBox.Cancel): + self.__argsLists = [] + self.__cancelProcess()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.py Sat Nov 21 20:37:54 2020 +0100 @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to edit the PyBabel configuration. +""" + +import os + +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtWidgets import QDialog, QDialogButtonBox + +from E5Gui.E5PathPicker import E5PathPickerModes +from E5Gui.E5Application import e5App + +from .Ui_PyBabelConfigDialog import Ui_PyBabelConfigDialog + + +class PyBabelConfigDialog(QDialog, Ui_PyBabelConfigDialog): + """ + Class implementing a dialog to edit the PyBabel configuration. + """ + def __init__(self, configuration, parent=None): + """ + Constructor + + @param configuration current pybabel configuration + @type dict + @param parent reference to the parent widget + @type QWidget + """ + super(PyBabelConfigDialog, self).__init__(parent) + self.setupUi(self) + + self.__e5project = e5App().getObject("Project") + + self.configFilePicker.setMode( + E5PathPickerModes.SaveFileEnsureExtensionMode) + self.configFilePicker.setFilters(self.tr( + "Configuration Files (*.cfg);;" + "All Files (*)" + )) + self.configFilePicker.setDefaultDirectory( + self.__e5project.getProjectPath()) + + self.translationsDirectoryPicker.setMode( + E5PathPickerModes.DirectoryMode) + self.translationsDirectoryPicker.setDefaultDirectory( + self.__e5project.getProjectPath()) + + self.catalogFilePicker.setMode( + E5PathPickerModes.SaveFileEnsureExtensionMode) + self.catalogFilePicker.setFilters(self.tr( + "Message Catalog Files (*.pot);;" + "All Files (*)" + )) + self.catalogFilePicker.setDefaultDirectory( + self.__e5project.getProjectPath()) + + self.configFilePicker.setFocus(Qt.OtherFocusReason) + + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) + + if "configFile" in configuration: + self.configFilePicker.setText( + self.__e5project.getAbsoluteUniversalPath( + configuration["configFile"])) + if "translationsDirectory" in configuration: + self.translationsDirectoryPicker.setText( + self.__e5project.getAbsoluteUniversalPath( + configuration["translationsDirectory"])) + if "domain" in configuration: + self.domainEdit.setText(configuration["domain"]) + if "catalogFile" in configuration: + self.catalogFilePicker.setText( + self.__e5project.getAbsoluteUniversalPath( + configuration["catalogFile"])) + if "markersList" in configuration: + self.markersEdit.setText(" ".join(configuration["markersList"])) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def getConfiguration(self): + """ + Public method to get the entered configuration data. + + @return pybabel configuration + @rtype dict + """ + configuration = { + "configFile": self.__e5project.getRelativeUniversalPath( + self.configFilePicker.text()), + "translationsDirectory": self.__e5project.getRelativeUniversalPath( + self.translationsDirectoryPicker.text()), + } + + domain = self.domainEdit.text() + if domain: + configuration["domain"] = domain + else: + configuration["domain"] = "messages" + + catalogFile = self.catalogFilePicker.text() + if not catalogFile: + # use a default name made of translations dir and domain + catalogFile = os.path.join( + configuration["translationsDirectory"], + "{0}.pot".format(configuration["domain"])) + configuration["catalogFile"] = ( + self.__e5project.getRelativeUniversalPath(catalogFile) + ) + + if self.markersEdit.text(): + configuration["markersList"] = self.markersEdit.text().split() + + return configuration + + def __updateOK(self): + """ + Private method to update the status of the OK button. + """ + enable = ( + bool(self.configFilePicker.text()) and + bool(self.translationsDirectoryPicker.text()) + ) + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) + + def __updateCatalogPicker(self): + """ + Private method to update the contents of the catalog picker. + """ + translationsDirectory = self.translationsDirectoryPicker.text() + domain = self.domainEdit.text() + self.catalogFilePicker.setText(os.path.join( + translationsDirectory, "{0}.pot".format(domain))) + + @pyqtSlot(str) + def on_configFilePicker_textChanged(self, txt): + """ + Private slot to handle a change of the configuration file name. + + @param txt configuration file name + @type str + """ + self.__updateOK() + + @pyqtSlot(str) + def on_translationsDirectoryPicker_textChanged(self, txt): + """ + Private slot to handle a change of the catalog file name. + + @param txt configuration file name + @type str + """ + self.__updateOK() + self.__updateCatalogPicker() + + @pyqtSlot(str) + def on_domainEdit_textChanged(self, txt): + """ + Private slot to handle a change of the translations domain. + + @param txt entered translations domain + @type str + """ + self.__updateCatalogPicker()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.ui Sat Nov 21 20:37:54 2020 +0100 @@ -0,0 +1,185 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PyBabelConfigDialog</class> + <widget class="QDialog" name="PyBabelConfigDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>150</height> + </rect> + </property> + <property name="windowTitle"> + <string>PyBabel Configuration</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Configuration File:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="E5PathPicker" name="configFilePicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the name of the PyBabel configuration file</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Translations Directory:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="E5PathPicker" name="translationsDirectoryPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the name of the directory containing the translations</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Domain:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="domainEdit"> + <property name="toolTip"> + <string>Enter the name of the translations domain (leave empty for default)</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Message Catalog:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="E5PathPicker" name="catalogFilePicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the name of the message catalog file</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Translation Markers:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="markersEdit"> + <property name="toolTip"> + <string>Enter the translation markers separated by space (_ is included by default)</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>configFilePicker</tabstop> + <tabstop>translationsDirectoryPicker</tabstop> + <tabstop>domainEdit</tabstop> + <tabstop>catalogFilePicker</tabstop> + <tabstop>markersEdit</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PyBabelConfigDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PyBabelConfigDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskBabelExtension/PyBabelProjectExtension.py Sat Nov 21 20:37:54 2020 +0100 @@ -0,0 +1,578 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the project support for flask-babel. +""" + +import os +import re + +from PyQt5.QtCore import pyqtSlot, QObject, QProcess +from PyQt5.QtWidgets import QDialog + +from E5Gui import E5MessageBox +from E5Gui.E5Application import e5App + +from .PyBabelCommandDialog import PyBabelCommandDialog + +import Utilities + + +class PyBabelProject(QObject): + """ + Class implementing the Flask project support. + """ + def __init__(self, plugin, project, parent=None): + """ + Constructor + + @param plugin reference to the plugin object + @type ProjectFlaskPlugin + @param project reference to the project object + @type Project + @param parent parent + @type QObject + """ + super(PyBabelProject, self).__init__(parent) + + self.__plugin = plugin + self.__project = project + + self.__e5project = e5App().getObject("Project") + self.__virtualEnvManager = e5App().getObject("VirtualEnvManager") + + self.__hooksInstalled = False + + 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. + """ + if self.__project.hasCapability("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() + + def projectClosedHooks(self): + """ + Public method to remove our hook methods. + """ + if self.__hooksInstalled: + 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 + + self.__hooksInstalled = False + + def determineCapability(self): + """ + Public method to determine the availability of flask-babel. + """ + self.__project.setCapability("pybabel", self.flaskBabelAvailable()) + + ################################################################## + ## slots and methods below implement general functionality + ################################################################## + + def getBabelCommand(self): + """ + Public method to build the Babel command. + + @return full pybabel command + @rtype str + """ + return self.__project.getFullCommand("pybabel") + + ################################################################## + ## 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): + """ + Public slot to show a dialog to edit the pybabel configuration. + """ + from .PyBabelConfigDialog import PyBabelConfigDialog + + config = self.__project.getData("pybabel", "") + dlg = PyBabelConfigDialog(config) + if dlg.exec() == QDialog.Accepted: + config = dlg.getConfiguration() + self.__project.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.__project.getData("pybabel", "") + if not config: + self.__configurePybabel() + return True + + configFileName = self.__project.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: + workdir = self.__project.getApplication()[0] + started, pid = QProcess.startDetached(editor, [poFile], workdir) + 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.__project.getApplication()[0] + potFile = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "catalogFile")) + + try: + potFilePath = os.path.dirname(potFile) + os.makedirs(potFilePath) + except EnvironmentError: + pass + + args = [ + "-F", + os.path.relpath( + self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "configFile")), + workdir + ) + ] + if self.__project.getData("pybabel", "markersList"): + for marker in self.__project.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.__project.getApplication()[0] + langFile = self.__e5project.getAbsoluteUniversalPath( + self.__e5project.getTranslationPattern().replace( + "%language%", code)) + potFile = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "catalogFile")) + + args = [ + "--domain={0}".format( + self.__project.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.__project.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "translationsDirectory")) + + args = [ + "--domain={0}".format( + self.__project.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.__project.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "translationsDirectory")) + + argsList = [] + for loc in locales: + argsList.append([ + "compile", + "--domain={0}".format( + self.__project.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.__project.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "translationsDirectory")) + potFile = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "catalogFile")) + + args = [ + "--domain={0}".format( + self.__project.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.__project.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "translationsDirectory")) + potFile = self.__e5project.getAbsoluteUniversalPath( + self.__project.getData("pybabel", "catalogFile")) + argsList = [] + for loc in locales: + args = [ + "update", + "--domain={0}".format( + self.__project.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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskBabelExtension/__init__.py Sat Nov 21 20:37:54 2020 +0100 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing support for the flask-babel extension. +"""
--- 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)
--- a/ProjectFlask/PyBabelCommandDialog.py Sat Nov 21 17:50:57 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a dialog to run a flask command and show its output. -""" - -from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton - -from E5Gui import E5MessageBox - -from .Ui_FlaskCommandDialog import Ui_FlaskCommandDialog - - -class PyBabelCommandDialog(QDialog, Ui_FlaskCommandDialog): - """ - Class implementing a dialog to run a flask command and show its output. - """ - def __init__(self, project, title="", msgSuccess="", msgError="", - parent=None): - """ - Constructor - - @param project reference to the project object - @type Project - @param title window title of the dialog - @type str - @param msgSuccess success message to be shown - @type str - @param msgError message to be shown on error - @type str - @param parent reference to the parent widget - @type QWidget - """ - super(PyBabelCommandDialog, self).__init__(parent) - self.setupUi(self) - - if title: - self.setWindowTitle(title) - - self.__project = project - self.__successMessage = msgSuccess - self.__errorMessage = msgError - - self.__process = None - self.__argsLists = [] - self.__workdir = "" - - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) - - def startCommand(self, command, args, workdir, clearOutput=True): - """ - Public method to start a pybabel command and show its output. - - @param command pybabel command to be run - @type str - @param args list of command line arguments for the command - @type list of str - @param workdir working directory for the command - @type str - @param clearOutput flag indicating to clear the output - @type bool - @return flag indicating a successful start - @rtype bool - """ - babelCommand = self.__project.getBabelCommand() - - self.__process = QProcess() - self.__process.setWorkingDirectory(workdir) - self.__process.setProcessChannelMode(QProcess.MergedChannels) - - self.__process.readyReadStandardOutput.connect(self.__readStdOut) - self.__process.finished.connect(self.__processFinished) - - if clearOutput: - self.outputEdit.clear() - - babelArgs = [command] - if args: - babelArgs += args - - self.__process.start(babelCommand, babelArgs) - ok = self.__process.waitForStarted(10000) - if not ok: - E5MessageBox.critical( - None, - self.tr("Execute PyBabel Command"), - self.tr("""The pybabel process could not be started.""")) - else: - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setFocus( - Qt.OtherFocusReason) - - return ok - - def startBatchCommand(self, argsLists, workdir): - """ - Public method to start a pybabel command repeatedly with a list of - arguments and show the output. - - @param argsLists list of command line arguments for the batch commands - @type list of lists of str - @param workdir working directory for the command - @type str - @return flag indicating a successful start of the first process - @rtype bool - """ - self.__argsLists = argsLists[:] - self.__workdir = workdir - - # start the first process - args = self.__argsLists.pop(0) - res = self.startCommand(args[0], args[1:], workdir) - if not res: - self.__argsLists = [] - - return res - - def closeEvent(self, evt): - """ - Protected method handling the close event of the dialog. - - @param evt reference to the close event object - @type QCloseEvent - """ - self.__argsLists = [] - self.__cancelProcess() - evt.accept() - - @pyqtSlot() - def __readStdOut(self): - """ - Private slot to add the server process output to the output pane. - """ - if self.__process is not None: - out = str(self.__process.readAllStandardOutput(), "utf-8") - self.outputEdit.insertPlainText(out) - - def __processFinished(self, exitCode, exitStatus): - """ - Private slot connected to the finished signal. - - @param exitCode exit code of the process - @type int - @param exitStatus exit status of the process - @type QProcess.ExitStatus - """ - normal = (exitStatus == QProcess.NormalExit) and (exitCode == 0) - self.__cancelProcess() - - if self.__argsLists: - args = self.__argsLists.pop(0) - self.startCommand(args[0], args[1:], self.__workdir, - clearOutput=False) - return - - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) - self.buttonBox.button(QDialogButtonBox.Close).setFocus( - Qt.OtherFocusReason) - - if normal and self.__successMessage: - self.outputEdit.insertPlainText(self.__successMessage) - elif not normal and self.__errorMessage: - self.outputEdit.insertPlainText(self.__errorMessage) - - @pyqtSlot() - def __cancelProcess(self): - """ - Private slot to terminate the current process. - """ - if ( - self.__process is not None and - self.__process.state() != QProcess.NotRunning - ): - self.__process.terminate() - QTimer.singleShot(2000, self.__process.kill) - self.__process.waitForFinished(3000) - - self.__process = None - - @pyqtSlot(QAbstractButton) - def on_buttonBox_clicked(self, button): - """ - Private slot handling presses of the button box buttons. - - @param button reference to the button been clicked - @type QAbstractButton - """ - if button is self.buttonBox.button(QDialogButtonBox.Close): - self.close() - elif button is self.buttonBox.button(QDialogButtonBox.Cancel): - self.__argsLists = [] - self.__cancelProcess()
--- a/ProjectFlask/PyBabelConfigDialog.py Sat Nov 21 17:50:57 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,169 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a dialog to edit the PyBabel configuration. -""" - -import os - -from PyQt5.QtCore import pyqtSlot, Qt -from PyQt5.QtWidgets import QDialog, QDialogButtonBox - -from E5Gui.E5PathPicker import E5PathPickerModes -from E5Gui.E5Application import e5App - -from .Ui_PyBabelConfigDialog import Ui_PyBabelConfigDialog - - -class PyBabelConfigDialog(QDialog, Ui_PyBabelConfigDialog): - """ - Class implementing a dialog to edit the PyBabel configuration. - """ - def __init__(self, configuration, parent=None): - """ - Constructor - - @param configuration current pybabel configuration - @type dict - @param parent reference to the parent widget - @type QWidget - """ - super(PyBabelConfigDialog, self).__init__(parent) - self.setupUi(self) - - self.__e5project = e5App().getObject("Project") - - self.configFilePicker.setMode( - E5PathPickerModes.SaveFileEnsureExtensionMode) - self.configFilePicker.setFilters(self.tr( - "Configuration Files (*.cfg);;" - "All Files (*)" - )) - self.configFilePicker.setDefaultDirectory( - self.__e5project.getProjectPath()) - - self.translationsDirectoryPicker.setMode( - E5PathPickerModes.DirectoryMode) - self.translationsDirectoryPicker.setDefaultDirectory( - self.__e5project.getProjectPath()) - - self.catalogFilePicker.setMode( - E5PathPickerModes.SaveFileEnsureExtensionMode) - self.catalogFilePicker.setFilters(self.tr( - "Message Catalog Files (*.pot);;" - "All Files (*)" - )) - self.catalogFilePicker.setDefaultDirectory( - self.__e5project.getProjectPath()) - - self.configFilePicker.setFocus(Qt.OtherFocusReason) - - self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) - - if "configFile" in configuration: - self.configFilePicker.setText( - self.__e5project.getAbsoluteUniversalPath( - configuration["configFile"])) - if "translationsDirectory" in configuration: - self.translationsDirectoryPicker.setText( - self.__e5project.getAbsoluteUniversalPath( - configuration["translationsDirectory"])) - if "domain" in configuration: - self.domainEdit.setText(configuration["domain"]) - if "catalogFile" in configuration: - self.catalogFilePicker.setText( - self.__e5project.getAbsoluteUniversalPath( - configuration["catalogFile"])) - if "markersList" in configuration: - self.markersEdit.setText(" ".join(configuration["markersList"])) - - msh = self.minimumSizeHint() - self.resize(max(self.width(), msh.width()), msh.height()) - - def getConfiguration(self): - """ - Public method to get the entered configuration data. - - @return pybabel configuration - @rtype dict - """ - configuration = { - "configFile": self.__e5project.getRelativeUniversalPath( - self.configFilePicker.text()), - "translationsDirectory": self.__e5project.getRelativeUniversalPath( - self.translationsDirectoryPicker.text()), - } - - domain = self.domainEdit.text() - if domain: - configuration["domain"] = domain - else: - configuration["domain"] = "messages" - - catalogFile = self.catalogFilePicker.text() - if not catalogFile: - # use a default name made of translations dir and domain - catalogFile = os.path.join( - configuration["translationsDirectory"], - "{0}.pot".format(configuration["domain"])) - configuration["catalogFile"] = ( - self.__e5project.getRelativeUniversalPath(catalogFile) - ) - - if self.markersEdit.text(): - configuration["markersList"] = self.markersEdit.text().split() - - return configuration - - def __updateOK(self): - """ - Private method to update the status of the OK button. - """ - enable = ( - bool(self.configFilePicker.text()) and - bool(self.translationsDirectoryPicker.text()) - ) - self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) - - def __updateCatalogPicker(self): - """ - Private method to update the contents of the catalog picker. - """ - translationsDirectory = self.translationsDirectoryPicker.text() - domain = self.domainEdit.text() - self.catalogFilePicker.setText(os.path.join( - translationsDirectory, "{0}.pot".format(domain))) - - @pyqtSlot(str) - def on_configFilePicker_textChanged(self, txt): - """ - Private slot to handle a change of the configuration file name. - - @param txt configuration file name - @type str - """ - self.__updateOK() - - @pyqtSlot(str) - def on_translationsDirectoryPicker_textChanged(self, txt): - """ - Private slot to handle a change of the catalog file name. - - @param txt configuration file name - @type str - """ - self.__updateOK() - self.__updateCatalogPicker() - - @pyqtSlot(str) - def on_domainEdit_textChanged(self, txt): - """ - Private slot to handle a change of the translations domain. - - @param txt entered translations domain - @type str - """ - self.__updateCatalogPicker()
--- a/ProjectFlask/PyBabelConfigDialog.ui Sat Nov 21 17:50:57 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,185 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>PyBabelConfigDialog</class> - <widget class="QDialog" name="PyBabelConfigDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>600</width> - <height>150</height> - </rect> - </property> - <property name="windowTitle"> - <string>PyBabel Configuration</string> - </property> - <property name="sizeGripEnabled"> - <bool>true</bool> - </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Configuration File:</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="E5PathPicker" name="configFilePicker" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <property name="toolTip"> - <string>Enter the name of the PyBabel configuration file</string> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>Translations Directory:</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="E5PathPicker" name="translationsDirectoryPicker" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <property name="toolTip"> - <string>Enter the name of the directory containing the translations</string> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>Domain:</string> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QLineEdit" name="domainEdit"> - <property name="toolTip"> - <string>Enter the name of the translations domain (leave empty for default)</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Message Catalog:</string> - </property> - </widget> - </item> - <item row="3" column="1"> - <widget class="E5PathPicker" name="catalogFilePicker" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <property name="toolTip"> - <string>Enter the name of the message catalog file</string> - </property> - </widget> - </item> - <item row="4" column="0"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Translation Markers:</string> - </property> - </widget> - </item> - <item row="4" column="1"> - <widget class="QLineEdit" name="markersEdit"> - <property name="toolTip"> - <string>Enter the translation markers separated by space (_ is included by default)</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="5" column="0" colspan="2"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - </layout> - </widget> - <customwidgets> - <customwidget> - <class>E5PathPicker</class> - <extends>QWidget</extends> - <header>E5Gui/E5PathPicker.h</header> - <container>1</container> - </customwidget> - </customwidgets> - <tabstops> - <tabstop>configFilePicker</tabstop> - <tabstop>translationsDirectoryPicker</tabstop> - <tabstop>domainEdit</tabstop> - <tabstop>catalogFilePicker</tabstop> - <tabstop>markersEdit</tabstop> - </tabstops> - <resources/> - <connections> - <connection> - <sender>buttonBox</sender> - <signal>accepted()</signal> - <receiver>PyBabelConfigDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>248</x> - <y>254</y> - </hint> - <hint type="destinationlabel"> - <x>157</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>buttonBox</sender> - <signal>rejected()</signal> - <receiver>PyBabelConfigDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>316</x> - <y>260</y> - </hint> - <hint type="destinationlabel"> - <x>286</x> - <y>274</y> - </hint> - </hints> - </connection> - </connections> -</ui>