Started implementing the "Run Server" function.

Mon, 09 Nov 2020 20:00:56 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 09 Nov 2020 20:00:56 +0100
changeset 4
e164b9ad3819
parent 3
265c3c2914e2
child 5
550e5ea385cb

Started implementing the "Run Server" function.

PluginFlask.e4p file | annotate | diff | comparison | revisions
ProjectFlask/Project.py file | annotate | diff | comparison | revisions
ProjectFlask/RunServerDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/RunServerDialog.ui file | annotate | diff | comparison | revisions
--- 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>

eric ide

mercurial