Mon, 09 Nov 2020 20:00:56 +0100
Started implementing the "Run Server" function.
--- a/PluginFlask.e4p Sun Nov 08 17:59:31 2020 +0100 +++ b/PluginFlask.e4p Mon Nov 09 20:00:56 2020 +0100 @@ -18,10 +18,12 @@ <Source>ProjectFlask/ConfigurationPage/FlaskPage.py</Source> <Source>ProjectFlask/ConfigurationPage/__init__.py</Source> <Source>ProjectFlask/Project.py</Source> + <Source>ProjectFlask/RunServerDialog.py</Source> <Source>ProjectFlask/__init__.py</Source> <Source>__init__.py</Source> </Sources> <Forms> + <Form>ProjectFlask/RunServerDialog.ui</Form> <Form>ProjectFlask/ConfigurationPage/FlaskPage.ui</Form> </Forms> <Others>
--- a/ProjectFlask/Project.py Sun Nov 08 17:59:31 2020 +0100 +++ b/ProjectFlask/Project.py Mon Nov 09 20:00:56 2020 +0100 @@ -9,7 +9,9 @@ import os -from PyQt5.QtCore import pyqtSlot, QObject, QProcess, QTimer +from PyQt5.QtCore import ( + pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer +) from PyQt5.QtWidgets import QMenu from E5Gui import E5MessageBox @@ -21,6 +23,8 @@ import UI.PixmapCache import Utilities +from .RunServerDialog import RunServerDialog + class Project(QObject): """ @@ -48,7 +52,8 @@ self.__menus = {} # dictionary with references to menus - self.__serverProc = None +## self.__serverProc = None + self.__serverDialog = None self.__flaskVersions = { "python": "", @@ -61,6 +66,42 @@ Public method to define the Flask actions. """ self.actions = [] + + ############################## + ## run actions below ## + ############################## + + self.runServerAct = E5Action( + self.tr('Run Server'), + self.tr('Run &Server'), + 0, 0, + self, 'flask_run_server') + self.runServerAct.setStatusTip(self.tr( + 'Starts the Flask Web server')) + self.runServerAct.setWhatsThis(self.tr( + """<b>Run Server</b>""" + """<p>Starts the Flask Web server.</p>""" + )) + self.runServerAct.triggered.connect(self.__runServer) + self.actions.append(self.runServerAct) + + ################################## + ## documentation action below ## + ################################## + + self.documentationAct = E5Action( + self.tr('Documentation'), + self.tr('D&ocumentation'), + 0, 0, + self, 'flask_documentation') + self.documentationAct.setStatusTip(self.tr( + 'Shows the help viewer with the Flask documentation')) + self.documentationAct.setWhatsThis(self.tr( + """<b>Documentation</b>""" + """<p>Shows the help viewer with the Flask documentation.</p>""" + )) + self.documentationAct.triggered.connect(self.__showDocumentation) + self.actions.append(self.documentationAct) ############################## ## about action below ## @@ -92,6 +133,10 @@ menu = QMenu(self.tr('&Flask'), self.__ui) menu.setTearOffEnabled(True) + menu.addAction(self.runServerAct) + menu.addSeparator() + menu.addAction(self.documentationAct) + menu.addSeparator() menu.addAction(self.aboutFlaskAct) self.__menus["main"] = menu @@ -130,8 +175,8 @@ """ Public method to handle the closing of a project. """ - if self.__serverProc is not None: - self.__serverProcFinished() +## if self.__serverProc is not None: +## self.__serverProcFinished() def supportedPythonVariants(self): """ @@ -246,7 +291,7 @@ Private slot to show some info about Flask. """ versions = self.getFlaskVersionStrings() - url = "https://flask.palletsprojects.com" + url = "https://palletsprojects.com/p/flask/" msgBox = E5MessageBox.E5MessageBox( E5MessageBox.Question, @@ -260,7 +305,7 @@ "<tr><td>Werkzeug Version:</td><td>{1}</td></tr>" "<tr><td>Python Version:</td><td>{2}</td></tr>" "<tr><td>Flask URL:</td><td><a href=\"{3}\">" - "{3}</a></td></tr>" + "The Pallets Projects - Flask</a></td></tr>" "</table></p>" ).format(versions["flask"], versions["werkzeug"], versions["python"], url), @@ -291,31 +336,93 @@ return self.__flaskVersions + def prepareRuntimeEnvironment(self, development=False): + """ + Public method to prepare a QProcessEnvironment object and determine + the appropriate working directory. + + @param development flag indicating development mode + @type bool + @return tuple containing the working directory and a prepared + environment object to be used with QProcess + @rtype tuple of (str, QProcessEnvironment) + """ + mainScript = self.__e5project.getMainScript(normalized=True) + if not mainScript: + E5MessageBox.critical( + self.__ui, + self.tr("Prepare Environment"), + self.tr("""The project has no configured main script""" + """ (= Flask application). Aborting...""")) + return "", None + + scriptPath, scriptName = os.path.split(mainScript) + if scriptName == "__init__.py": + workdir, app = os.path.split(scriptPath) + else: + workdir, app = scriptPath, scriptName + + env = QProcessEnvironment.systemEnvironment() + env.insert("FLASK_APP", app) + if development: + env.insert("FLASK_ENV", "development") + + return workdir, env + + ################################################################## + ## slots below implement documentation functions + ################################################################## + + def __showDocumentation(self): + """ + Private slot to show the helpviewer with the Flask documentation. + """ + page = self.__plugin.getPreferences("FlaskDocUrl") + self.__ui.launchHelpViewer(page) + ################################################################## ## slots below implement run functions ################################################################## + # TODO: does the flask server support logging? def __runServer(self, logging=False): """ - Private slot to start the Pyramid Web server. + Private slot to start the Flask Web server. @param logging flag indicating to enable logging @type bool """ # TODO: implement this (flask run) + workdir, env = self.prepareRuntimeEnvironment() + if env is not None: + cmd = self.getFlaskCommand() + + dlg = RunServerDialog() + if dlg.startServer(cmd, workdir, env): + dlg.show() + self.__serverDialog = dlg - def __serverProcFinished(self): - """ - Private slot connected to the finished signal. + def __runDevelopmentServer(self, logging=False): """ - if ( - self.__serverProc is not None and - self.__serverProc.state() != QProcess.NotRunning - ): - self.__serverProc.terminate() - QTimer.singleShot(2000, self.__serverProc.kill) - self.__serverProc.waitForFinished(3000) - self.__serverProc = None + Private slot to start the Flask Web server in development mode. + + @param logging flag indicating to enable logging + @type bool + """ + # TODO: implement this (flask run with FLASK_ENV=development) + +## def __serverProcFinished(self): +## """ +## Private slot connected to the finished signal. +## """ +## if ( +## self.__serverProc is not None and +## self.__serverProc.state() != QProcess.NotRunning +## ): +## self.__serverProc.terminate() +## QTimer.singleShot(2000, self.__serverProc.kill) +## self.__serverProc.waitForFinished(3000) +## self.__serverProc = None def __runPythonShell(self): """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/RunServerDialog.py Mon Nov 09 20:00:56 2020 +0100 @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to run the Flask server. +""" + +from PyQt5.QtCore import pyqtSlot, QProcess +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton + +from E5Gui import E5MessageBox + +from .Ui_RunServerDialog import Ui_RunServerDialog + + +class RunServerDialog(QDialog, Ui_RunServerDialog): + """ + Class implementing a dialog to run the Flask server. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(RunServerDialog, self).__init__(parent) + self.setupUi(self) + + self.__process = None + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + + def startServer(self, command, workdir, env): + """ + Public method to start the Flask server process. + + @param command path of the flask command + @type str + @param workdir working directory for the Flask server + @type str + @param env environment for the Flask server process + @type QProcessEnvironment + @return flag indicating a successful start + @rtype bool + """ + self.errorsEdit.hide() + + self.__process = QProcess() + self.__process.readyReadStandardOutput.connect(self.__readStdOut) + self.__process.readyReadStandardError.connect(self.__readStdErr) + self.__process.finished.connect(self.__processFinished) + self.__process.setProcessEnvironment(env) + self.__process.setWorkingDirectory(workdir) + + self.__process.start(command, ["run"]) + ok = self.__process.waitForStarted(10000) + if not ok: + E5MessageBox.critical( + None, + self.tr("Run Flask Server"), + self.tr("""The Flask server process could not be started.""")) + else: + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + + return ok + + def closeEvent(self, evt): + """ + Private method handling a close event. + + @param evt reference to the close event + @type QCloseEvent + """ + # TODO: not implemented yet + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot handling button presses. + + @param button button that was pressed + @type QAbstractButton + """ + if button is self.buttonBox.button(QDialogButtonBox.Cancel): + self.__cancel() + elif button is self.buttonBox.button(QDialogButtonBox.Close): + self.close() + + @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.appendPlainText(out) + + @pyqtSlot() + def __readStdErr(self): + """ + Private slot to add the server process errors to the errors pane. + """ + if self.__process is not None: + err = str(self.__process.readAllStandardError(), "utf-8") + self.errorsEdit.appendPlainText(err) + + self.errorsEdit.show() + + @pyqtSlot() + def __processFinished(self): + # TODO: implement it + pass + + @pyqtSlot() + def __cancel(self): + """ + Private slot to cancel the running server. + """ + # TODO: not implemented yet
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/RunServerDialog.ui Mon Nov 09 20:00:56 2020 +0100 @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>RunServerDialog</class> + <widget class="QDialog" name="RunServerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>Flask Server</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Output</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPlainTextEdit" name="outputEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Errors</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QPlainTextEdit" name="errorsEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>