ProjectPyramid/Project.py

Fri, 25 Oct 2013 18:51:52 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 25 Oct 2013 18:51:52 +0200
changeset 54
71c83a661c83
parent 48
c313efdb01de
child 57
e654970c913e
permissions
-rw-r--r--

Fixed code style issues.

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

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

"""
Module implementing the Pyramid project support.
"""

import os
import configparser
import re

from PyQt4.QtCore import QObject, QFileInfo, QProcess, QTimer, QUrl
from PyQt4.QtGui import QMenu, QDialog, QInputDialog, QDesktopServices, \
    QLineEdit

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

from .PyramidDialog import PyramidDialog

import Utilities
from Globals import isWindowsPlatform
import UI.PixmapCache


class PyramidNoProjectSelectedException(Exception):
    """
    Exception thrown to signal, that there is no current Pyramid project.
    """
    pass


class Project(QObject):
    """
    Class implementing the Pyramid project support.
    """
    def __init__(self, plugin, parent=None):
        """
        Constructor
        
        @param plugin reference to the plugin object
        @param parent parent (QObject)
        """
        super().__init__(parent)
        
        self.__plugin = plugin
        self.__ui = parent
        self.__e5project = e5App().getObject("Project")
        self.__hooksInstalled = False
        
        self.__mainMenu = None
        
        self.__serverProc = None
        
        self.__pyramidVersion = ""
    
    def initActions(self):
        """
        Public method to define the Pyramid actions.
        """
        self.actions = []
    
        self.selectProjectAct = E5Action(
            self.trUtf8('Current Pyramid Project'),
            "",
            0, 0,
            self, 'pyramid_current_project')
        self.selectProjectAct.setStatusTip(self.trUtf8(
            'Selects the current Pyramid project'))
        self.selectProjectAct.setWhatsThis(self.trUtf8(
            """<b>Current Pyramid Project</b>"""
            """<p>Selects the Pyramid project. Used for multi-project """
            """Pyramid projects to switch between the projects.</p>"""
        ))
        self.selectProjectAct.triggered[()].connect(self.__selectProject)
        self.actions.append(self.selectProjectAct)
        
        ###############################
        ## create actions below      ##
        ###############################
        
        self.createProjectAct = E5Action(
            self.trUtf8('Create Pyramid Project'),
            self.trUtf8('Create Pyramid &Project'),
            0, 0,
            self, 'pyramid_create_project')
        self.createProjectAct.setStatusTip(self.trUtf8(
            'Creates a new Pyramid project'))
        self.createProjectAct.setWhatsThis(self.trUtf8(
            """<b>Create Pyramid Project</b>"""
            """<p>Creates a new Pyramid project using "pcreate".</p>"""
        ))
        self.createProjectAct.triggered[()].connect(self.__createProject)
        self.actions.append(self.createProjectAct)
        
        ##############################
        ## run actions below        ##
        ##############################
        
        self.runServerAct = E5Action(
            self.trUtf8('Run Server'),
            self.trUtf8('Run &Server'),
            0, 0,
            self, 'pyramid_run_server')
        self.runServerAct.setStatusTip(self.trUtf8(
            'Starts the Pyramid Web server'))
        self.runServerAct.setWhatsThis(self.trUtf8(
            """<b>Run Server</b>"""
            """<p>Starts the Pyramid Web server using"""
            """ "pserve --reload development.ini".</p>"""
        ))
        self.runServerAct.triggered[()].connect(self.__runServer)
        self.actions.append(self.runServerAct)
        
        self.runLoggingServerAct = E5Action(
            self.trUtf8('Run Server with Logging'),
            self.trUtf8('Run Server with &Logging'),
            0, 0,
            self, 'pyramid_run_logging_server')
        self.runLoggingServerAct.setStatusTip(self.trUtf8(
            'Starts the Pyramid Web server with logging'))
        self.runLoggingServerAct.setWhatsThis(self.trUtf8(
            """<b>Run Server with Logging</b>"""
            """<p>Starts the Pyramid Web server with logging using"""
            """ "pserve --log-file=server.log --reload development.ini".</p>"""
        ))
        self.runLoggingServerAct.triggered[()].connect(self.__runLoggingServer)
        self.actions.append(self.runLoggingServerAct)
        
        self.runBrowserAct = E5Action(
            self.trUtf8('Run Web-Browser'),
            self.trUtf8('Run &Web-Browser'),
            0, 0,
            self, 'pyramid_run_browser')
        self.runBrowserAct.setStatusTip(self.trUtf8(
            'Starts the default Web-Browser with the URL of the Pyramid Web'
            ' server'))
        self.runBrowserAct.setWhatsThis(self.trUtf8(
            """<b>Run Web-Browser</b>"""
            """<p>Starts the default Web-Browser with the URL of the """
            """Pyramid Web server.</p>"""
        ))
        self.runBrowserAct.triggered[()].connect(self.__runBrowser)
        self.actions.append(self.runBrowserAct)
    
        self.runPythonShellAct = E5Action(
            self.trUtf8('Start Pyramid Python Console'),
            self.trUtf8('Start Pyramid &Python Console'),
            0, 0,
            self, 'pyramid_python_console')
        self.runPythonShellAct.setStatusTip(self.trUtf8(
            'Starts an interactive Python interpreter'))
        self.runPythonShellAct.setWhatsThis(self.trUtf8(
            """<b>Start Pyramid Python Console</b>"""
            """<p>Starts an interactive Python interpreter.</p>"""
        ))
        self.runPythonShellAct.triggered[()].connect(self.__runPythonShell)
        self.actions.append(self.runPythonShellAct)
        
        ##############################
        ## setup actions below      ##
        ##############################
        
        self.setupDevelopAct = E5Action(
            self.trUtf8('Setup Development Environment'),
            self.trUtf8('Setup &Development Environment'),
            0, 0,
            self, 'pyramid_setup_development')
        self.setupDevelopAct.setStatusTip(self.trUtf8(
            'Setup the Pyramid project in development mode'))
        self.setupDevelopAct.setWhatsThis(self.trUtf8(
            """<b>Setup Development Environment</b>"""
            """<p>Setup the Pyramid project in development mode using"""
            """ "python setup.py develop".</p>"""
        ))
        self.setupDevelopAct.triggered[()].connect(self.__setupDevelop)
        self.actions.append(self.setupDevelopAct)
        
        ###############################
        ## database actions below    ##
        ###############################
        
        self.initializeDbAct = E5Action(
            self.trUtf8('Initialize Database'),
            self.trUtf8('Initialize &Database'),
            0, 0,
            self, 'pyramid_initialize_database')
        self.initializeDbAct.setStatusTip(self.trUtf8(
            'Initializes (or re-initializes) the database of the current'
            ' Pyramid project'))
        self.initializeDbAct.setWhatsThis(self.trUtf8(
            """<b>Initialize Database</b>"""
            """<p>Initializes (or re-initializes) the database of the"""
            """ current Pyramid project.</p>"""
        ))
        self.initializeDbAct.triggered[()].connect(self.__initializeDatabase)
        self.actions.append(self.initializeDbAct)
        
        ###############################
        ## show actions below        ##
        ###############################
        
        self.showViewsAct = E5Action(
            self.trUtf8('Show Matching Views'),
            self.trUtf8('Show Matching &Views'),
            0, 0,
            self, 'pyramid_show_views')
        self.showViewsAct.setStatusTip(self.trUtf8(
            'Show views matching a given URL'))
        self.showViewsAct.setWhatsThis(self.trUtf8(
            """<b>Show Matching Views</b>"""
            """<p>Show views matching a given URL.</p>"""
        ))
        self.showViewsAct.triggered[()].connect(self.__showMatchingViews)
        self.actions.append(self.showViewsAct)
        
        self.showRoutesAct = E5Action(
            self.trUtf8('Show Routes'),
            self.trUtf8('Show &Routes'),
            0, 0,
            self, 'pyramid_show_routes')
        self.showRoutesAct.setStatusTip(self.trUtf8(
            'Show all URL dispatch routes used by a Pyramid application'))
        self.showRoutesAct.setWhatsThis(self.trUtf8(
            """<b>Show Routes</b>"""
            """<p>Show all URL dispatch routes used by a Pyramid application"""
            """ in the order in which they are evaluated.</p>"""
        ))
        self.showRoutesAct.triggered[()].connect(self.__showRoutes)
        self.actions.append(self.showRoutesAct)
        
        self.showTweensAct = E5Action(
            self.trUtf8('Show Tween Objects'),
            self.trUtf8('Show &Tween Objects'),
            0, 0,
            self, 'pyramid_show_routes')
        self.showTweensAct.setStatusTip(self.trUtf8(
            'Show all implicit and explicit tween objects used by a Pyramid'
            ' application'))
        self.showTweensAct.setWhatsThis(self.trUtf8(
            """<b>Show Tween Objects</b>"""
            """<p>Show all implicit and explicit tween objects used by a"""
            """ Pyramid application.</p>"""
        ))
        self.showTweensAct.triggered[()].connect(self.__showTweens)
        self.actions.append(self.showTweensAct)
        
        ##################################
        ## distribution actions below   ##
        ##################################
        
        self.buildDistroAct = E5Action(
            self.trUtf8('Build Distribution'),
            self.trUtf8('Build &Distribution'),
            0, 0,
            self, 'pyramid_build_distribution')
        self.buildDistroAct.setStatusTip(self.trUtf8(
            'Builds a distribution file for the Pyramid project'))
        self.buildDistroAct.setWhatsThis(self.trUtf8(
            """<b>Build Distribution</b>"""
            """<p>Builds a distribution file for the Pyramid project using"""
            """ "python setup.py sdist".</p>"""
        ))
        self.buildDistroAct.triggered[()].connect(self.__buildDistribution)
        self.actions.append(self.buildDistroAct)
        
        ##################################
        ## documentation action below   ##
        ##################################
        
        self.documentationAct = E5Action(
            self.trUtf8('Documentation'),
            self.trUtf8('D&ocumentation'),
            0, 0,
            self, 'pyramid_documentation')
        self.documentationAct.setStatusTip(self.trUtf8(
            'Shows the help viewer with the Pyramid documentation'))
        self.documentationAct.setWhatsThis(self.trUtf8(
            """<b>Documentation</b>"""
            """<p>Shows the help viewer with the Pyramid documentation.</p>"""
        ))
        self.documentationAct.triggered[()].connect(self.__showDocumentation)
        self.actions.append(self.documentationAct)
       
        ##############################
        ## about action below       ##
        ##############################
        
        self.aboutPyramidAct = E5Action(
            self.trUtf8('About Pyramid'),
            self.trUtf8('About P&yramid'),
            0, 0,
            self, 'pyramid_about')
        self.aboutPyramidAct.setStatusTip(self.trUtf8(
            'Shows some information about Pyramid'))
        self.aboutPyramidAct.setWhatsThis(self.trUtf8(
            """<b>About Pyramid</b>"""
            """<p>Shows some information about Pyramid.</p>"""
        ))
        self.aboutPyramidAct.triggered[()].connect(self.__pyramidInfo)
        self.actions.append(self.aboutPyramidAct)
        
        self.__setCurrentProject(None)
    
    def initMenu(self):
        """
        Public slot to initialize the Pyramid menu.
        
        @return the menu generated (QMenu)
        """
        menu = QMenu(self.trUtf8('P&yramid'), self.__ui)
        menu.setTearOffEnabled(True)
        
        menu.addAction(self.selectProjectAct)
        menu.addSeparator()
        menu.addAction(self.runServerAct)
        menu.addAction(self.runLoggingServerAct)
        menu.addAction(self.runBrowserAct)
        menu.addSeparator()
        menu.addAction(self.createProjectAct)
        menu.addSeparator()
        menu.addAction(self.setupDevelopAct)
        menu.addSeparator()
        menu.addAction(self.initializeDbAct)
        menu.addSeparator()
        menu.addAction(self.showViewsAct)
        menu.addAction(self.showRoutesAct)
        menu.addAction(self.showTweensAct)
        menu.addSeparator()
        menu.addAction(self.runPythonShellAct)
        menu.addSeparator()
        menu.addAction(self.buildDistroAct)
        menu.addSeparator()
        menu.addAction(self.documentationAct)
        menu.addSeparator()
        menu.addAction(self.aboutPyramidAct)
        
        self.__mainMenu = menu
        return menu
    
    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")
            try:
                if editor:
                    self.__translationsBrowser.addHookMethodAndMenuEntry(
                        "open", self.openPOEditor,
                        self.trUtf8("Open with {0}").format(
                            os.path.basename(editor)))
                else:
                    self.__translationsBrowser.removeHookMethod("open")
            except KeyError:
                # ignore for older eric5 versions
                pass
    
    def projectOpenedHooks(self):
        """
        Public method to add our hook methods.
        """
        if self.__e5project.getProjectType() == "Pyramid":
            self.__formsBrowser = \
                e5App().getObject("ProjectBrowser").getProjectBrowser("forms")
            self.__formsBrowser.addHookMethodAndMenuEntry(
                "newForm", self.newForm, self.trUtf8("New template..."))
            
            if self.__e5project.getProjectLanguage() == "Python2":
                # Babel and lingua are not yet available for Python 3
                self.__e5project.projectLanguageAddedByCode.connect(
                    self.__projectLanguageAdded)
                self.__translationsBrowser = \
                    e5App().getObject("ProjectBrowser")\
                    .getProjectBrowser("translations")
                self.__translationsBrowser.addHookMethodAndMenuEntry(
                    "extractMessages", self.extractMessages,
                    self.trUtf8("Extract Messages"))
                self.__translationsBrowser.addHookMethodAndMenuEntry(
                    "releaseAll", self.compileCatalogs,
                    self.trUtf8("Compile All Catalogs"))
                self.__translationsBrowser.addHookMethodAndMenuEntry(
                    "releaseSelected", self.compileSelectedCatalogs,
                    self.trUtf8("Compile Selected Catalogs"))
                self.__translationsBrowser.addHookMethodAndMenuEntry(
                    "generateAll", self.updateCatalogs,
                    self.trUtf8("Update All Catalogs"))
                self.__translationsBrowser.addHookMethodAndMenuEntry(
                    "generateSelected", self.updateSelectedCatalogs,
                    self.trUtf8("Update Selected Catalogs"))
                
                self.__hooksInstalled = True
            
                self.registerOpenHook()
    
    def projectClosedHooks(self):
        """
        Public method to remove our hook methods.
        """
        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("generateSelected")
            try:
                self.__translationsBrowser.removeHookMethod("open")
            except KeyError:
                # ignore for older eric5 versions
                pass
            self.__translationsBrowser = None
        
        self.__hooksInstalled = False
    
    def newForm(self, path):
        """
        Public method to create a new form.
        
        @param path full directory path for the new form file (string)
        """
        from .FormSelectionDialog import FormSelectionDialog
        
        dlg = FormSelectionDialog()
        if dlg.exec_() == QDialog.Accepted:
            template = dlg.getTemplateText()
            
            filter = self.trUtf8(
                "Chameleon Templates (*.pt);;"
                "Chameleon Text Templates (*.txt);;"
                "Mako Templates (*.mako);;"
                "Mako Templates (*.mak);;"
                "HTML Files (*.html);;"
                "HTML Files (*.htm);;"
                "All Files (*)")
            fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
                self.__ui,
                self.trUtf8("New Form"),
                path,
                filter,
                None,
                E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
            if fname:
                ext = QFileInfo(fname).suffix()
                if not ext:
                    ex = selectedFilter.split("(*")[1].split(")")[0]
                    if ex:
                        fname += ex
            
                if os.path.exists(fname):
                    res = E5MessageBox.yesNo(
                        self.__ui,
                        self.trUtf8("New Form"),
                        self.trUtf8("""The file already exists! Overwrite"""
                                    """ it?"""),
                        icon=E5MessageBox.Warning)
                    if not res:
                        # user selected to not overwrite
                        return
                
                try:
                    f = open(fname, "w", encoding="utf-8")
                    f.write(template)
                    f.close()
                except IOError as e:
                    E5MessageBox.critical(
                        self.__ui,
                        self.trUtf8("New Form"),
                        self.trUtf8("<p>The new form file <b>{0}</b> could"
                                    " not be created.<br/> Problem: {1}</p>")
                        .format(fname, e))
                    return
                
                self.__e5project.appendFile(fname)
                self.__formsBrowser.sourceFile.emit(fname)

    ##################################################################
    ## methods below implement general functionality
    ##################################################################
    
    def projectClosed(self):
        """
        Public method to handle the closing of a project.
        """
        if self.__serverProc is not None:
            self.__serverProcFinished()
        self.__setCurrentProject(None)
    
    def __getExecutablePaths(self, file):
        """
        Private method to build all full path of an executable file from
        the environment.
        
        @param file filename of the executable (string)
        @return list of full executable names, if the executable file is
            accessible via the searchpath defined by the PATH environment
            variable, or an empty list otherwise.
        """
        paths = []
        
        if os.path.isabs(file):
            if os.access(file, os.X_OK):
                return [file]
            else:
                return []
            
        cur_path = os.path.join(os.curdir, file)
        if os.path.exists(cur_path):
            if os.access(cur_path, os.X_OK):
                paths.append(cur_path)

        path = os.getenv('PATH')
        
        # environment variable not defined
        if path is not None:
            dirs = path.split(os.pathsep)
            for dir in dirs:
                exe = os.path.join(dir, file)
                if os.access(exe, os.X_OK) and exe not in paths:
                    paths.append(exe)
        
        return paths
    
    def supportedPythonVariants(self):
        """
        Public method to get the supported Python variants.
        
        @return list of supported Python variants (list of strings)
        """
        variants = []
        cmd = "pcreate"
        
        for variant in 'Python2', 'Python3':
            virtEnv = self.__getVirtualEnvironment(variant)
            if virtEnv:
                fullCmd = self.getPyramidCommand(cmd, variant)
                if fullCmd != cmd:
                    variants.append(variant)
            else:
                try:
                    fullCmds = Utilities.getExecutablePaths(cmd)
                except AttributeError:
                    fullCmds = self.__getExecutablePaths(cmd)
                for fullCmd in fullCmds:
                    try:
                        f = open(fullCmd, 'r', encoding='utf-8')
                        l0 = f.readline()
                        f.close()
                    except (IOError, OSError):
                        l0 = ""
                    if variant.lower() in l0.lower() or \
                       "{0}.".format(variant[-1]) in l0 or \
                       (variant == "Python2" and
                        "python3" not in l0.lower() and
                            "python" in l0.lower()):
                        variants.append(variant)
                        break
        
        return variants
    
    def __getVirtualEnvironment(self, language=""):
        """
        Private method to get the path of the virtual environment.
        
        @param language Python variant to get the virtual environment
            for (string, one of '', 'Python2' or 'Python3')
        @return path of the virtual environment (string)
        """
        if not language:
            language = self.__e5project.getProjectLanguage()
        if language == "Python3":
            virtEnv = self.__plugin.getPreferences("VirtualEnvironmentPy3")
        elif language == "Python2":
            virtEnv = self.__plugin.getPreferences("VirtualEnvironmentPy2")
        else:
            virtEnv = ""
        if virtEnv and not os.path.exists(virtEnv):
            virtEnv = ""
        return virtEnv
    
    def getPyramidCommand(self, cmd, language=""):
        """
        Public method to build a Pyramid command.
        
        @param cmd command (string)
        @param language Python variant to get the virtual environment
            for (string, one of '', 'Python2' or 'Python3')
        @return full pyramid command (string)
        """
        virtualEnv = self.__getVirtualEnvironment(language)
        if virtualEnv:
            if isWindowsPlatform():
                cmd = os.path.join(virtualEnv, "Scripts", cmd)
            else:
                fullCmd = os.path.join(virtualEnv, "bin", cmd)
                if not os.path.exists(fullCmd):
                    fullCmd = os.path.join(virtualEnv, "local", "bin", cmd)
                    if not os.path.exists(fullCmd):
                        # fall back to just cmd
                        fullCmd = cmd
                cmd = fullCmd
        return cmd
    
    def getPythonCommand(self):
        """
        Public method to build the Python command.
        
        @return python command (string)
        """
        python = "python"
        if isWindowsPlatform():
            python += ".exe"
        else:
            language = self.__e5project.getProjectLanguage()
            if language == "Python3":
                python = "python3"
            elif language == "Python2":
                python = "python2"
        virtualEnv = self.__getVirtualEnvironment()
        if virtualEnv:
            if isWindowsPlatform():
                python = os.path.join(virtualEnv, "Scripts", python)
                if not os.path.exists(python):
                    python = os.path.join(virtualEnv, python)
            else:
                python = os.path.join(virtualEnv, "bin", python)
                if not os.path.exists(python):
                    python = python[:-1]    # omit the version character
        
        return python
    
    def __pyramidInfo(self):
        """
        Private slot to show some info about Pyramid.
        """
        version = self.getPyramidVersion()
        url = "http://www.pylonsproject.org/projects/pyramid/about"
        
        msgBox = E5MessageBox.E5MessageBox(
            E5MessageBox.Question,
            self.trUtf8("About Pyramid"),
            self.trUtf8(
                "<p>Pyramid is a high-level Python Web framework that"
                " encourages rapid development and clean, pragmatic"
                " design.</p>"
                "<p><table>"
                "<tr><td>Version:</td><td>{0}</td></tr>"
                "<tr><td>URL:</td><td><a href=\"{1}\">"
                "{1}</a></td></tr>"
                "</table></p>"
            ).format(version, url),
            modal=True,
            buttons=E5MessageBox.Ok)
        msgBox.setIconPixmap(UI.PixmapCache.getPixmap(
            os.path.join("ProjectPyramid", "icons", "pyramid64.png")))
        msgBox.exec_()
    
    def getPyramidVersion(self):
        """
        Public method to get the Pyramid version.
        
        @return Pyramid version (string)
        """
        if not self.__pyramidVersion:
            cmd = self.getPyramidCommand("pcreate")
            try:
                f = open(cmd, 'r', encoding="utf-8")
                lines = f.read().splitlines()
                f.close()
                for line in lines:
                    if line.startswith("__requires__"):
                        # sample: __requires__ = 'pyramid==1.4'
                        vers = line.strip().split()[-1][1:-1].split("==")[1]
                        self.__pyramidVersion = vers
            except (IOError, OSError):
                self.__pyramidVersion = ""
        
        return self.__pyramidVersion
    
    def isSpawningConsole(self, consoleCmd):
        """
        Public method to check, if the given console is a spawning console.
        
        @param consoleCmd console command (string)
        @return tuple of two entries giving an indication, if the console
            is spawning (boolean) and the (possibly) cleaned console command
            (string)
        """
        if consoleCmd and consoleCmd[0] == '@':
            return (True, consoleCmd[1:])
        else:
            return (False, consoleCmd)

    ##################################################################
    ## slots below implement creation functions
    ##################################################################
    
    def __createProject(self):
        """
        Private slot to create a new Pyramid project.
        """
        from .CreateParametersDialog import CreateParametersDialog
        
        dlg = CreateParametersDialog(self)
        if dlg.exec_() == QDialog.Accepted:
            scaffold, project, overwrite, simulate = dlg.getData()
            
            cmd = self.getPyramidCommand("pcreate")
            args = []
            if overwrite:
                args.append("--overwrite")
            else:
                args.append("--interactive")
            if simulate:
                args.append("--simulate")
            args.append("--scaffold={0}".format(scaffold))
            args.append(project)
            dlg = PyramidDialog(self.trUtf8("Create Pyramid Project"),
                                linewrap=False, parent=self.__ui)
            if dlg.startProcess(cmd, args, self.__e5project.getProjectPath()):
                dlg.exec_()
                if dlg.normalExit() and not simulate:
                    # search for files created by pcreate and add them to the
                    # project
                    projectPath = os.path.join(
                        self.__e5project.getProjectPath(), project)
                    for entry in os.walk(projectPath):
                        for fileName in entry[2]:
                            fullName = os.path.join(entry[0], fileName)
                            self.__e5project.appendFile(fullName)
                    
                    # create the base directory for translations
                    i18nPath = os.path.join(
                        projectPath, project.lower(), "i18n")
                    if not os.path.exists(i18nPath):
                        os.makedirs(i18nPath)
                    self.__e5project.setDirty(True)
                    
                    self.__setCurrentProject(project)
    
    ##################################################################
    ## methods below implement site related functions
    ##################################################################
    
    def __findProjects(self):
        """
        Private method to determine the relative path of all Pyramid
        projects (= top level dirs).
        
        @return list of projects (list of string)
        """
        projects = []
        ppath = self.__e5project.getProjectPath()
        for entry in os.listdir(ppath):
            if entry[0] not in "._" and \
               os.path.isdir(os.path.join(ppath, entry)):
                projects.append(entry)
        return projects
    
    def __selectProject(self):
        """
        Private method to select a Pyramid project to work with.
        """
        projects = self.__findProjects()
        if len(projects) == 0:
            project = None
        elif len(projects) == 1:
            project = projects[0]
        else:
            if self.__currentProject is not None:
                try:
                    cur = projects.index(self.__currentProject)
                except ValueError:
                    cur = 0
            else:
                cur = 0
            project, ok = QInputDialog.getItem(
                self.__ui,
                self.trUtf8("Select Pyramid Project"),
                self.trUtf8("Select the Pyramid project to work with."),
                projects,
                cur, False)
            if not ok:
                projects = None
        self.__setCurrentProject(project)
    
    def __projectPath(self):
        """
        Private method to calculate the full path of the Pyramid project.
        
        @exception PyramidNoProjectSelectedException raised, if no project is
            selected
        @return path of the project (string)
        """
        if self.__currentProject is None:
            self.__selectProject()
        
        if self.__currentProject is None:
            raise PyramidNoProjectSelectedException
        else:
            return os.path.join(self.__e5project.getProjectPath(),
                                self.__currentProject)
    
    def __setCurrentProject(self, project):
        """
        Private slot to set the current project.
        
        @param project name of the project (string)
        """
        if project is not None and len(project) == 0:
            self.__currentProject = None
        else:
            self.__currentProject = project
        
        if self.__currentProject is None:
            curProject = self.trUtf8("None")
        else:
            curProject = self.__currentProject
        self.selectProjectAct.setText(
            self.trUtf8('&Current Pyramid Project ({0})').format(curProject))
        
        if self.__currentProject is None:
            self.__e5project.pdata["TRANSLATIONPATTERN"] = []
        else:
            config = configparser.ConfigParser()
            config.read(os.path.join(self.__projectPath(), "setup.cfg"))
            outputDir = config.get("init_catalog", "output_dir")
            domain = config.get("init_catalog", "domain")
            self.__e5project.pdata["TRANSLATIONPATTERN"] = [
                os.path.join(project, outputDir, "%language%",
                             "LC_MESSAGES", "{0}.po".format(domain))
            ]
        
        if self.__currentProject is None:
            self.initializeDbAct.setEnabled(False)
        else:
            initCmd = self.__getInitDbCommand()
            self.initializeDbAct.setEnabled(os.path.exists(initCmd))
    
    def __project(self):
        """
        Private method to get the name of the current Pyramid project.
        
        @exception PyramidNoProjectSelectedException raised, if no project is
            selected
        @return name of the project (string)
        """
        if self.__currentProject is None:
            self.__selectProject()
        
        if self.__currentProject is None:
            raise PyramidNoProjectSelectedException
        else:
            return self.__currentProject
    
    ##################################################################
    ## slots below implement run functions
    ##################################################################
    
    def __runServer(self, logging=False):
        """
        Private slot to start the Pyramid Web server.
        
        @param logging flag indicating to enable logging (boolean)
        """
        consoleCmd = self.isSpawningConsole(
            self.__plugin.getPreferences("ConsoleCommand"))[1]
        if consoleCmd:
            try:
                projectPath = self.__projectPath()
            except PyramidNoProjectSelectedException:
                E5MessageBox.warning(
                    self.__ui,
                    self.trUtf8('Run Server'),
                    self.trUtf8('No current Pyramid project selected or no'
                                ' Pyramid project created yet. Aborting...'))
                return
            
            args = Utilities.parseOptionString(consoleCmd)
            args[0] = Utilities.getExecutablePath(args[0])
            args.append(self.getPyramidCommand("pserve"))
            if logging:
                args.append("--log-file=server.log")
            args.append("--reload")
            args.append(os.path.join(projectPath, "development.ini"))
            
            if isWindowsPlatform():
                serverProcStarted, pid = \
                    QProcess.startDetached(args[0], args[1:], projectPath)
            else:
                if self.__serverProc is not None:
                    self.__serverProcFinished()
                
                self.__serverProc = QProcess()
                self.__serverProc.finished.connect(self.__serverProcFinished)
                self.__serverProc.setWorkingDirectory(projectPath)
                self.__serverProc.start(args[0], args[1:])
                serverProcStarted = self.__serverProc.waitForStarted()
            if not serverProcStarted:
                E5MessageBox.critical(
                    self.__ui,
                    self.trUtf8('Process Generation Error'),
                    self.trUtf8('The Pyramid server could not be started.'))
    
    def __runLoggingServer(self):
        """
        Private slot to start the Pyramid Web server with logging.
        """
        self.__runServer(True)
    
    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 __runBrowser(self):
        """
        Private slot to start the default web browser with the server URL.
        """
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                self.trUtf8('Run Web-Browser'),
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        config = configparser.ConfigParser()
        config.read(os.path.join(projectPath, "development.ini"))
        port = config.get("server:main", "port", fallback="6543")
        url = QUrl("http://localhost:{0}".format(port))
        res = QDesktopServices.openUrl(url)
        if not res:
            E5MessageBox.critical(
                self.__ui,
                self.trUtf8('Run Web-Browser'),
                self.trUtf8('Could not start the web-browser for the URL'
                            ' "{0}".').format(url.toString()))
    
    def __runPythonShell(self):
        """
        Private slot to start a Python console for a Pyramid project.
        """
        consoleCmd = self.isSpawningConsole(
            self.__plugin.getPreferences("ConsoleCommand"))[1]
        if consoleCmd:
            try:
                projectPath = self.__projectPath()
            except PyramidNoProjectSelectedException:
                E5MessageBox.warning(
                    self.__ui,
                    self.trUtf8('Start Pyramid Python Console'),
                    self.trUtf8('No current Pyramid project selected or no'
                                ' Pyramid project created yet. Aborting...'))
                return
            
            args = Utilities.parseOptionString(consoleCmd)
            args[0] = Utilities.getExecutablePath(args[0])
            args.append(self.getPyramidCommand("pshell"))
            language = self.__e5project.getProjectLanguage()
            if language == "Python2":
                consoleType = self.__plugin.getPreferences(
                    "Python2ConsoleType")
            else:
                consoleType = self.__plugin.getPreferences(
                    "Python3ConsoleType")
            args.append("--python-shell={0}".format(consoleType))
            args.append(os.path.join(projectPath, "development.ini"))
            
            started, pid = QProcess.startDetached(
                args[0], args[1:], projectPath)
            if not started:
                E5MessageBox.critical(
                    self.__ui,
                    self.trUtf8('Process Generation Error'),
                    self.trUtf8('The Pyramid Shell process could not be'
                                ' started.'))

    ##################################################################
    ## slots below implement setup functions
    ##################################################################
    
    def __setupDevelop(self):
        """
        Private slot to set up the development environment for the current
        project.
        """
        title = self.trUtf8("Setup Development Environment")
        try:
            wd = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.getPythonCommand()
        args = []
        args.append("setup.py")
        args.append("develop")
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("Pyramid development environment setup"
                                   " successfully."))
        res = dia.startProcess(cmd, args, wd)
        if res:
            dia.exec_()
    
    ##################################################################
    ## slots below implement distribution functions
    ##################################################################
    
    def __buildDistribution(self):
        """
        Private slot to build a distribution file for the current Pyramid
        project.
        """
        title = self.trUtf8("Build Distribution File")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        from .DistributionTypeSelectionDialog import \
            DistributionTypeSelectionDialog
        
        dlg = DistributionTypeSelectionDialog(self, projectPath, self.__ui)
        if dlg.exec_() == QDialog.Accepted:
            formats = dlg.getFormats()
            cmd = self.getPythonCommand()
            args = []
            args.append("setup.py")
            args.append("sdist")
            if formats:
                args.append("--formats={0}".format(','.join(formats)))
            
            dia = PyramidDialog(
                title,
                msgSuccess=self.trUtf8("Python distribution file built"
                                       " successfully."))
            res = dia.startProcess(cmd, args, projectPath)
            if res:
                dia.exec_()
    
    ##################################################################
    ## slots below implement database functions
    ##################################################################
    
    def __getInitDbCommand(self):
        """
        Private method to create the path to the initialization script.
        
        @return path to the initialization script (string)
        """
        try:
            cmd = "initialize_{0}_db".format(self.__project())
            return self.getPyramidCommand(cmd)
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                self.trUtf8("Initialize Database"),
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return ""
    
    def __initializeDatabase(self):
        """
        Private slot to initialize the database of the Pyramid project.
        """
        title = self.trUtf8("Initialize Database")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.__getInitDbCommand()
        args = []
        args.append("development.ini")
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("Database initialized successfully."))
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
    
    ##################################################################
    ## slots below implement various debugging functions
    ##################################################################
    
    def __showMatchingViews(self):
        """
        Private slot showing all views that would match a given URL.
        """
        title = self.trUtf8("Show Matching Views")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        url, ok = QInputDialog.getText(
            self.__ui,
            self.trUtf8("Show Matching Views"),
            self.trUtf8("Enter the URL to be matched:"),
            QLineEdit.Normal,
            "/")
        if not ok or url == "":
            return
        
        cmd = self.getPyramidCommand("pviews")
        args = []
        args.append("development.ini")
        args.append(url)
        
        dia = PyramidDialog(title, fixed=True, linewrap=False)
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
    
    def __showRoutes(self):
        """
        Private slot showing all URL dispatch routes.
        """
        title = self.trUtf8("Show Routes")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        from .PyramidRoutesDialog import PyramidRoutesDialog
        
        dia = PyramidRoutesDialog(self)
        res = dia.start(projectPath)
        if res:
            dia.exec_()
    
    def __showTweens(self):
        """
        Private slot showing all implicit and explicit tween objects.
        """
        title = self.trUtf8("Show Tween Objects")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.getPyramidCommand("ptweens")
        args = []
        args.append("development.ini")

        dia = PyramidDialog(title, fixed=True, linewrap=False)
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
    
    ##################################################################
    ## slots below implement documentation functions
    ##################################################################
    
    def __showDocumentation(self):
        """
        Private slot to show the helpviewer with the Pyramid documentation.
        """
        page = self.__plugin.getPreferences("PyramidDocUrl")
        self.__ui.launchHelpViewer(page)
    
    ##################################################################
    ## slots below implement translation functions
    ##################################################################
    
    def __getLocale(self, filename):
        """
        Private method to extract the locale out of a file name.
        
        @param filename name of the file used for extraction (string)
        @return extracted locale (string) or None
        """
        if self.__e5project.pdata["TRANSLATIONPATTERN"]:
            pattern = self.__e5project.pdata["TRANSLATIONPATTERN"][0]\
                .replace("%language%", "(.*?)")
            match = re.search(pattern, filename)
            if match is not None:
                loc = match.group(1)
                return loc
            else:
                loc = None
        else:
            loc = None
        
        return loc
    
    def __normalizeList(self, filenames):
        """
        Private method to normalize a list of file names.
        
        @param filenames list of file names to normalize (list of string)
        @return normalized file names (list of string)
        """
        nfilenames = []
        for filename in filenames:
            if filename.endswith(".mo"):
                filename = filename.replace(".mo", ".po")
            if filename not in nfilenames:
                nfilenames.append(filename)
        
        return nfilenames
    
    def __projectFilteredList(self, filenames):
        """
        Private method to filter a list of file names by Pyramid project.
        
        @param filenames list of file names to be filtered (list of string)
        @return file names belonging to the current site (list of string)
        """
        project = self.__project()
        nfilenames = []
        for filename in filenames:
            if filename.startswith(project + os.sep):
                nfilenames.append(filename)
        
        return nfilenames
    
    def extractMessages(self):
        """
        Public method to extract the messages catalog template file.
        """
        title = self.trUtf8("Extract messages")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.getPythonCommand()
        args = []
        args.append("setup.py")
        args.append("extract_messages")
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("\nMessages extracted successfully."))
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
            
            config = configparser.ConfigParser()
            config.read(os.path.join(projectPath, "setup.cfg"))
            potFile = config.get("extract_messages", "output_file")
            if potFile:
                self.__e5project.appendFile(os.path.join(projectPath, potFile))
            else:
                try:
                    lowerProject = self.__project().lower()
                except PyramidNoProjectSelectedException:
                    E5MessageBox.warning(
                        self.__ui,
                        title,
                        self.trUtf8('No current Pyramid project selected or no'
                                    ' Pyramid project created yet.'
                                    ' Aborting...'))
                    return
                
                self.__e5project.appendFile(os.path.join(
                    projectPath, lowerProject, "locale",
                    "{0}.pot".format(lowerProject)))
    
    def __projectLanguageAdded(self, code):
        """
        Private slot handling the addition of a new language.
        
        @param code language code of the new language (string)
        """
        title = self.trUtf8("Initializing message catalog for '{0}'")\
            .format(code)
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.getPythonCommand()
        args = []
        args.append("setup.py")
        args.append("init_catalog")
        args.append("-l")
        args.append(code)
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("\nMessage catalog initialized"
                                   " successfully."))
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
            
            langFile = self.__e4project.pdata["TRANSLATIONPATTERN"][0]\
                .replace("%language%", code)
            self.__e5project.appendFile(langFile)
    
    def compileCatalogs(self, filenames):
        """
        Public method to compile the message catalogs.
        
        @param filenames list of filenames (not used)
        """
        title = self.trUtf8("Compiling message catalogs")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.getPythonCommand()
        args = []
        args.append("setup.py")
        args.append("compile_catalog")
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("\nMessage catalogs compiled"
                                   " successfully."))
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
        
            for entry in os.walk(projectPath):
                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 (list of string)
        """
        title = self.trUtf8("Compiling message catalogs")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        argsLists = []
        
        for filename in self.__normalizeList(
                self.__projectFilteredList(filenames)):
            locale = self.__getLocale(filename)
            if locale:
                args = []
                args.append(self.getPythonCommand())
                args.append("setup.py")
                args.append("compile_catalog")
                args.append("-l")
                args.append(locale)
                argsLists.append(args)
        
        if len(argsLists) == 0:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No locales detected. Aborting...'))
            return
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("\nMessage catalogs compiled"
                                   " successfully."))
        res = dia.startBatchProcesses(argsLists, projectPath)
        if res:
            dia.exec_()
            
            for entry in os.walk(self.__sitePath()):
                for fileName in entry[2]:
                    fullName = os.path.join(entry[0], fileName)
                    if fullName.endswith('.mo'):
                        self.__e5project.appendFile(fullName)
    
    def updateCatalogs(self, filenames):
        """
        Public method to update the message catalogs.
        
        @param filenames list of filenames (not used)
        """
        title = self.trUtf8("Updating message catalogs")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        cmd = self.getPythonCommand()
        args = []
        args.append("setup.py")
        args.append("update_catalog")
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("\nMessage catalogs updated successfully."))
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec_()
    
    def updateSelectedCatalogs(self, filenames):
        """
        Public method to update the message catalogs.
        
        @param filenames list of filenames
        """
        title = self.trUtf8("Updating message catalogs")
        try:
            projectPath = self.__projectPath()
        except PyramidNoProjectSelectedException:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No current Pyramid project selected or no Pyramid'
                            ' project created yet. Aborting...'))
            return
        
        argsLists = []
        
        for filename in self.__normalizeList(
                self.__projectFilteredList(filenames)):
            locale = self.__getLocale(filename)
            if locale:
                args = []
                args.append(self.getPythonCommand())
                args.append("setup.py")
                args.append("update_catalog")
                args.append("-l")
                args.append(locale)
                argsLists.append(args)
        
        if len(argsLists) == 0:
            E5MessageBox.warning(
                self.__ui,
                title,
                self.trUtf8('No locales detected. Aborting...'))
            return
        
        dia = PyramidDialog(
            title,
            msgSuccess=self.trUtf8("\nMessage catalogs updated successfully."))
        res = dia.startBatchProcesses(argsLists, projectPath)
        if res:
            dia.exec_()
    
    def openPOEditor(self, poFile):
        """
        Public method to edit the given file in an external .po editor.
        
        @param poFile name of the .po file (string)
        """
        editor = self.__plugin.getPreferences("TranslationsEditor")
        if poFile.endswith(".po") and editor:
            try:
                wd = self.__projectPath()
            except PyramidNoProjectSelectedException:
                wd = ""
            started, pid = QProcess.startDetached(editor, [poFile], wd)
            if not started:
                E5MessageBox.critical(
                    None,
                    self.trUtf8('Process Generation Error'),
                    self.trUtf8('The translations editor process ({0}) could'
                                ' not be started.').format(
                        os.path.basename(editor)))

eric ide

mercurial