ProjectFlask/Project.py

Thu, 12 Nov 2020 19:43:14 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 12 Nov 2020 19:43:14 +0100
changeset 7
a140b2a8ba93
parent 6
d491ccab7343
child 8
cfbd3a2757fd
permissions
-rw-r--r--

Started implementing the "flask routes" function.

# -*- coding: utf-8 -*-

# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the Flask project support.
"""

import os

from PyQt5.QtCore import (
    pyqtSlot, QObject, QProcess, QProcessEnvironment
)
from PyQt5.QtWidgets import QMenu

from E5Gui import E5MessageBox
from E5Gui.E5Action import E5Action
from E5Gui.E5Application import e5App

from Globals import isWindowsPlatform

import UI.PixmapCache
import Utilities

from .RunServerDialog import RunServerDialog
from .RoutesDialog import RoutesDialog


class Project(QObject):
    """
    Class implementing the Flask project support.
    """
    def __init__(self, plugin, iconSuffix, parent=None):
        """
        Constructor
        
        @param plugin reference to the plugin object
        @type ProjectFlaskPlugin
        @param iconSuffix suffix for the icons
        @type str
        @param parent parent
        @type QObject
        """
        super(Project, self).__init__(parent)
        
        self.__plugin = plugin
        self.__iconSuffix = iconSuffix
        self.__ui = parent

        self.__e5project = e5App().getObject("Project")
        self.__virtualEnvManager = e5App().getObject("VirtualEnvManager")
        
        self.__menus = {}   # dictionary with references to menus
         
##        self.__serverProc = None
        self.__serverDialog = None
        self.__routesDialog = None
       
        self.__flaskVersions = {
            "python": "",
            "flask": "",
            "werkzeug": "",
        }
    
    def initActions(self):
        """
        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)
        
        self.runDevServerAct = E5Action(
            self.tr('Run Development Server'),
            self.tr('Run &Development Server'),
            0, 0,
            self, 'flask_run_dev_server')
        self.runDevServerAct.setStatusTip(self.tr(
            'Starts the Flask Web server in development mode'))
        self.runDevServerAct.setWhatsThis(self.tr(
            """<b>Run Development Server</b>"""
            """<p>Starts the Flask Web server in development mode.</p>"""
        ))
        self.runDevServerAct.triggered.connect(self.__runDevelopmentServer)
        self.actions.append(self.runDevServerAct)
        
        ##############################
        ## routes action below        ##
        ##############################
        
        self.showRoutesAct = E5Action(
            self.tr('Show Routes'),
            self.tr('Show &Routes'),
            0, 0,
            self, 'flask_show_routes')
        self.showRoutesAct.setStatusTip(self.tr(
            'Shows a dialog with the routes of the flask app'))
        self.showRoutesAct.setWhatsThis(self.tr(
            """<b>Show Routes</b>"""
            """<p>Shows a dialog with the routes of the flask app.</p>"""
        ))
        self.showRoutesAct.triggered.connect(self.__showRoutes)
        self.actions.append(self.showRoutesAct)
        
        ##################################
        ## 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       ##
        ##############################
        
        self.aboutFlaskAct = E5Action(
            self.tr('About Flask'),
            self.tr('About &Flask'),
            0, 0,
            self, 'flask_about')
        self.aboutFlaskAct.setStatusTip(self.tr(
            'Shows some information about Flask'))
        self.aboutFlaskAct.setWhatsThis(self.tr(
            """<b>About Flask</b>"""
            """<p>Shows some information about Flask.</p>"""
        ))
        self.aboutFlaskAct.triggered.connect(self.__flaskInfo)
        self.actions.append(self.aboutFlaskAct)
    
    def initMenu(self):
        """
        Public method to initialize the Flask menu.
        
        @return the menu generated
        @rtype QMenu
        """
        self.__menus = {}   # clear menus references
        
        menu = QMenu(self.tr('&Flask'), self.__ui)
        menu.setTearOffEnabled(True)
        
        menu.addAction(self.runServerAct)
        menu.addAction(self.runDevServerAct)
        menu.addSeparator()
        menu.addAction(self.showRoutesAct)
        menu.addSeparator()
        menu.addAction(self.documentationAct)
        menu.addSeparator()
        menu.addAction(self.aboutFlaskAct)
        
        self.__menus["main"] = menu
        
        return menu
    
    def getMenu(self, name):
        """
        Public method to get a reference to the requested menu.
        
        @param name name of the menu
        @type str
        @return reference to the menu or None, if no menu with the given
            name exists
        @rtype QMenu or None
        """
        if name in self.__menus:
            return self.__menus[name]
        else:
            return None
    
    def getMenuNames(self):
        """
        Public method to get the names of all menus.
        
        @return menu names
        @rtype list of str
        """
        return list(self.__menus.keys())
    
    ##################################################################
    ## slots below implement general functionality
    ##################################################################
    
    def projectClosed(self):
        """
        Public method to handle the closing of a project.
        """
        if self.__serverDialog is not None:
            self.__serverDialog.close()
##        if self.__serverProc is not None:
##            self.__serverProcFinished()
    
    def supportedPythonVariants(self):
        """
        Public method to get the supported Python variants.
        
        @return list of supported Python variants
        @rtype list of str
        """
        variants = []
        
        virtEnv = self.__getVirtualEnvironment()
        if virtEnv:
            fullCmd = self.getFlaskCommand()
            if fullCmd:
                variants.append("Python3")
        else:
            fullCmd = self.getFlaskCommand()
            if isWindowsPlatform():
                if fullCmd:
                    variants.append("Python3")
            else:
                fullCmds = Utilities.getExecutablePaths("flask")
                for fullCmd in fullCmds:
                    try:
                        with open(fullCmd, 'r', encoding='utf-8') as f:
                            l0 = f.readline()
                    except (IOError, OSError):
                        l0 = ""
                    if self.__isSuitableForVariant("Python3", l0):
                        variants.append("Python3")
                        break
        
        return variants
    
    def __isSuitableForVariant(self, variant, line0):
        """
        Private method to test, if a detected command file is suitable for the
        given Python variant.
        
        @param variant Python variant to test for
        @type str
        @param line0 first line of the executable
        @type str
        @return flag indicating a suitable file was found
        @rtype bool
        """
        l0 = line0.lower()
        ok = (variant.lower() in l0 or
              "{0}.".format(variant[-1]) in l0)
        ok |= "pypy3" in l0
        
        return ok
    
    def __getVirtualEnvironment(self):
        """
        Private method to get the path of the virtual environment.
        
        @return path of the virtual environment
        @rtype str
        """
        language = self.__e5project.getProjectLanguage()
        if language == "Python3":
            venvName = self.__plugin.getPreferences(
                "VirtualEnvironmentNamePy3")
        else:
            venvName = ""
        if venvName:
            virtEnv = self.__virtualEnvManager.getVirtualenvDirectory(
                venvName)
        else:
            virtEnv = ""
        
        if virtEnv and not os.path.exists(virtEnv):
            virtEnv = ""
        
        return virtEnv      # __IGNORE_WARNING_M834__
    
    def getFlaskCommand(self):
        """
        Public method to build the Flask command.
        
        @return full flask command
        @rtype str
        """
        cmd = "flask"
        
        virtualEnv = self.__getVirtualEnvironment()
        if isWindowsPlatform():
            fullCmds = [
                os.path.join(virtualEnv, "Scripts", cmd + '.exe'),
                os.path.join(virtualEnv, "bin", cmd + '.exe'),
                cmd     # fall back to just cmd
            ]
            for cmd in fullCmds:
                if os.path.exists(cmd):
                    break
        else:
            fullCmds = [
                os.path.join(virtualEnv, "bin", cmd),
                os.path.join(virtualEnv, "local", "bin", cmd),
                Utilities.getExecutablePath(cmd),
                cmd     # fall back to just cmd
            ]
            for cmd in fullCmds:
                if os.path.exists(cmd):
                    break
        return cmd
    
    @pyqtSlot()
    def __flaskInfo(self):
        """
        Private slot to show some info about Flask.
        """
        versions = self.getFlaskVersionStrings()
        url = "https://palletsprojects.com/p/flask/"
        
        msgBox = E5MessageBox.E5MessageBox(
            E5MessageBox.Question,
            self.tr("About Flask"),
            self.tr(
                "<p>Flask is a lightweight WSGI web application framework."
                " It is designed to make getting started quick and easy,"
                " with the ability to scale up to complex applications.</p>"
                "<p><table>"
                "<tr><td>Flask Version:</td><td>{0}</td></tr>"
                "<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}\">"
                "The Pallets Projects - Flask</a></td></tr>"
                "</table></p>"
            ).format(versions["flask"], versions["werkzeug"],
                     versions["python"], url),
            modal=True,
            buttons=E5MessageBox.Ok)
        msgBox.setIconPixmap(UI.PixmapCache.getPixmap(
            os.path.join("ProjectFlask", "icons",
                         "flask64-{0}".format(self.__iconSuffix))))
        msgBox.exec()
    
    def getFlaskVersionStrings(self):
        """
        Public method to get the Flask, Werkzeug and Python versions as a
        string.
        
        @return dictionary containing the Flask, Werkzeug and Python versions
        @rtype dict
        """
        if not self.__flaskVersions["flask"]:
            cmd = self.getFlaskCommand()
            proc = QProcess()
            proc.start(cmd, ["--version"])
            if proc.waitForFinished(10000):
                output = str(proc.readAllStandardOutput(), "utf-8")
                for line in output.lower().splitlines():
                    key, version = line.strip().split(None, 1)
                    self.__flaskVersions[key] = version
        
        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
    ##################################################################
    
    @pyqtSlot()
    def __runServer(self, development=False):
        """
        Private slot to start the Flask Web server.
        
        @param development flag indicating development mode
        @type bool
        """
        if self.__serverDialog is not None:
            self.__serverDialog.close()
        
        dlg = RunServerDialog(self.__plugin)
        if dlg.startServer(self, development=development):
            dlg.show()
            self.__serverDialog = dlg
    
    @pyqtSlot()
    def __runDevelopmentServer(self):
        """
        Private slot to start the Flask Web server in development mode.
        """
        self.__runServer(development=True)
    
    # TODO: add method to start a server with parameters
    
##    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):
        """
        Private slot to start a Python console in the app context.
        """
        # TODO: implement this (flask shell)
    
    ##################################################################
    ## slots below implement various debugging functions
    ##################################################################
    
    def __showRoutes(self):
        """
        Private slot showing all URL dispatch routes.
        """
        # TODO: implement this (flask routes)
        if self.__routesDialog is not None:
            self.__routesDialog.close()
        
        dlg = RoutesDialog()
        if dlg.showRoutes(self):
            dlg.show()
            self.__routesDialog = dlg

eric ide

mercurial