ProjectPyramid/Project.py

Mon, 28 Oct 2024 17:35:47 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 28 Oct 2024 17:35:47 +0100
branch
eric7
changeset 169
0405f4f83719
parent 168
fee59283137b
child 171
4a8bf0845603
permissions
-rw-r--r--

- changed to the new style header
- ensured proper parent relationship of modal dialogs
- included compiled form files

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

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

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

import configparser
import contextlib
import glob
import os
import re
import subprocess  # secok
import sys

from PyQt6.QtCore import (
    QFileInfo,
    QIODeviceBase,
    QObject,
    QProcess as QProcessPyQt,
    QTimer,
    QUrl,
    pyqtSlot,
)
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWidgets import QDialog, QInputDialog, QLineEdit, QMenu

from eric7 import Preferences, Utilities
from eric7.EricGui.EricAction import EricAction
from eric7.EricWidgets import EricFileDialog, EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp

try:
    from eric7.SystemUtilities.FileSystemUtilities import (
        getExecutablePath,
        getExecutablePaths,
    )
except ImportError:
    # imports for eric < 23.1
    from eric7.Utilities import getExecutablePath, getExecutablePaths
try:
    from eric7.SystemUtilities.OSUtilities import isWindowsPlatform
except ImportError:
    # imports for eric < 23.1
    from eric7.Globals import isWindowsPlatform

try:
    from eric7.EricGui import EricPixmapCache
except ImportError:
    from UI import PixmapCache as EricPixmapCache

from .PyramidDialog import PyramidDialog


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

    pass


class QProcess(QProcessPyQt):
    """
    Class transforming the call arguments in case of gnome-terminal.
    """

    def start(self, cmd, args=None, mode=QIODeviceBase.OpenModeFlag.ReadWrite):
        """
        Public method to start the given program (cmd) in a new process, if
        none is already running, passing the command line arguments in args.

        @param cmd start the given program cmd
        @type str
        @param args list of parameters
        @type list of str
        @param mode access mode
        @type QIODeviceBase.OpenMode
        """
        if args is None:
            args = []

        if (
            cmd.endswith(
                ("gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal")
            )
            and "-e" in args
        ):
            index = args.index("-e") + 1
            cargs = " ".join(args[index:])
            args[index:] = [cargs]

        super().start(cmd, args, mode)

    @staticmethod
    def startDetached(cmd, args=None, path=""):
        """
        Public static method to start the given program (cmd) in a new process,
        if none is already running, passing the command line arguments in args.

        @param cmd start the given program cmd
        @type str
        @param args list of parameters
        @type list of str
        @param path new working directory
        @type str
        @return tuple of successful start and process id
        @rtype tuple of (bool, int)
        """
        if args is None:
            args = []

        if (
            cmd.endswith(
                ("gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal")
            )
            and "-e" in args
        ):
            index = args.index("-e") + 1
            cargs = " ".join(args[index:])
            args[index:] = [cargs]

        return QProcessPyQt.startDetached(cmd, args, path)


class Project(QObject):
    """
    Class implementing the Pyramid project support.
    """

    def __init__(self, plugin, iconSuffix, parent=None):
        """
        Constructor

        @param plugin reference to the plugin object
        @type ProjectPyramidPlugin
        @param iconSuffix suffix for the icons
        @type str
        @param parent parent
        @type QObject
        """
        super().__init__(parent)

        self.__plugin = plugin
        self.__iconSuffix = iconSuffix
        self.__ui = parent
        self.__ericProject = ericApp().getObject("Project")
        self.__virtualEnvManager = ericApp().getObject("VirtualEnvManager")
        self.__hooksInstalled = False

        self.__menus = {}  # dictionary with references to menus

        self.__serverProc = None

        self.__pyramidVersion = ""

        self.__migrationSummaryDialog = None

    def initActions(self):
        """
        Public method to define the Pyramid actions.
        """
        self.actions = []

        self.selectProjectAct = EricAction(
            self.tr("Current Pyramid Project"),
            "",
            0,
            0,
            self,
            "pyramid_current_project",
        )
        self.selectProjectAct.setStatusTip(
            self.tr("Selects the current Pyramid project")
        )
        self.selectProjectAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Create Pyramid Project"),
            self.tr("Create Pyramid &Project"),
            0,
            0,
            self,
            "pyramid_create_project",
        )
        self.createProjectAct.setStatusTip(self.tr("Creates a new Pyramid project"))
        self.createProjectAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Run Server"),
            self.tr("Run &Server"),
            0,
            0,
            self,
            "pyramid_run_server",
        )
        self.runServerAct.setStatusTip(self.tr("Starts the Pyramid Web server"))
        self.runServerAct.setWhatsThis(
            self.tr(
                """<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.runBrowserAct = EricAction(
            self.tr("Run Web-Browser"),
            self.tr("Run &Web-Browser"),
            0,
            0,
            self,
            "pyramid_run_browser",
        )
        self.runBrowserAct.setStatusTip(
            self.tr(
                "Starts the default Web-Browser with the URL of the Pyramid Web"
                " server"
            )
        )
        self.runBrowserAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Start Pyramid Python Console"),
            self.tr("Start Pyramid &Python Console"),
            0,
            0,
            self,
            "pyramid_python_console",
        )
        self.runPythonShellAct.setStatusTip(
            self.tr("Starts an interactive Python interpreter")
        )
        self.runPythonShellAct.setWhatsThis(
            self.tr(
                """<b>Start Pyramid Python Console</b>"""
                """<p>Starts an interactive Python interpreter.</p>"""
            )
        )
        self.runPythonShellAct.triggered.connect(self.__runPythonShell)
        self.actions.append(self.runPythonShellAct)

        ###############################
        ## show actions below        ##
        ###############################

        self.showViewsAct = EricAction(
            self.tr("Show Matching Views"),
            self.tr("Show Matching &Views"),
            0,
            0,
            self,
            "pyramid_show_views",
        )
        self.showViewsAct.setStatusTip(self.tr("Show views matching a given URL"))
        self.showViewsAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Show Routes"),
            self.tr("Show &Routes"),
            0,
            0,
            self,
            "pyramid_show_routes",
        )
        self.showRoutesAct.setStatusTip(
            self.tr("Show all URL dispatch routes used by a Pyramid application")
        )
        self.showRoutesAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Show Tween Objects"),
            self.tr("Show &Tween Objects"),
            0,
            0,
            self,
            "pyramid_show_routes",
        )
        self.showTweensAct.setStatusTip(
            self.tr(
                "Show all implicit and explicit tween objects used by a Pyramid"
                " application"
            )
        )
        self.showTweensAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Build Distribution"),
            self.tr("Build &Distribution"),
            0,
            0,
            self,
            "pyramid_build_distribution",
        )
        self.buildDistroAct.setStatusTip(
            self.tr("Builds a distribution file for the Pyramid project")
        )
        self.buildDistroAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("Documentation"),
            self.tr("D&ocumentation"),
            0,
            0,
            self,
            "pyramid_documentation",
        )
        self.documentationAct.setStatusTip(
            self.tr("Shows the help viewer with the Pyramid documentation")
        )
        self.documentationAct.setWhatsThis(
            self.tr(
                """<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 = EricAction(
            self.tr("About Pyramid"),
            self.tr("About P&yramid"),
            0,
            0,
            self,
            "pyramid_about",
        )
        self.aboutPyramidAct.setStatusTip(
            self.tr("Shows some information about Pyramid")
        )
        self.aboutPyramidAct.setWhatsThis(
            self.tr(
                """<b>About Pyramid</b>"""
                """<p>Shows some information about Pyramid.</p>"""
            )
        )
        self.aboutPyramidAct.triggered.connect(self.__pyramidInfo)
        self.actions.append(self.aboutPyramidAct)

        self.__initDatabaseActions()

        self.__setCurrentProject(None)

    def __initDatabaseActions(self):
        """
        Private method to initialize the database related actions.
        """
        self.initializeDbAct = EricAction(
            self.tr("Initialize Database"),
            self.tr("Initialize &Database"),
            0,
            0,
            self,
            "pyramid_initialize_database",
        )
        self.initializeDbAct.setStatusTip(
            self.tr(
                "Initializes (or re-initializes) the database of the current"
                " Pyramid project"
            )
        )
        self.initializeDbAct.setWhatsThis(
            self.tr(
                """<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)

        #########################################################
        ## action to create a new database migration
        #########################################################

        self.migrateCreateAct = EricAction(
            self.tr("Create Migration"),
            self.tr("&Create Migration"),
            0,
            0,
            self,
            "flask_create_migration",
        )
        self.migrateCreateAct.setStatusTip(
            self.tr("Create a new migration for the current database")
        )
        self.migrateCreateAct.setWhatsThis(
            self.tr(
                """<b>Create Migration</b>"""
                """<p>Creates a new migration for the current database"""
                """ and stores it  in the configured migrations directory.</p>"""
            )
        )
        self.migrateCreateAct.triggered.connect(self.__createMigration)
        self.actions.append(self.migrateCreateAct)

        #########################################################
        ## action to up- and downgrade a databse
        #########################################################

        self.upgradeDatabaseAct = EricAction(
            self.tr("Upgrade Database"),
            self.tr("&Upgrade Database"),
            0,
            0,
            self,
            "flask_upgrade_database",
        )
        self.upgradeDatabaseAct.setStatusTip(
            self.tr("Upgrade the database to the current migration")
        )
        self.upgradeDatabaseAct.setWhatsThis(
            self.tr(
                """<b>Upgrade Database</b>"""
                """<p>Upgrades the database to the current migration.</p>"""
            )
        )
        self.upgradeDatabaseAct.triggered.connect(self.upgradeDatabase)
        self.actions.append(self.upgradeDatabaseAct)

        self.downgradeDatabaseAct = EricAction(
            self.tr("Downgrade Database"),
            self.tr("&Downgrade Database"),
            0,
            0,
            self,
            "flask_downgrade_database",
        )
        self.downgradeDatabaseAct.setStatusTip(
            self.tr("Downgrade the database to the previous version")
        )
        self.downgradeDatabaseAct.setWhatsThis(
            self.tr(
                """<b>Downgrade Database</b>"""
                """<p>Downgrades the database to the previous version.</p>"""
            )
        )
        self.downgradeDatabaseAct.triggered.connect(self.downgradeDatabase)
        self.actions.append(self.downgradeDatabaseAct)

        #########################################################
        ## actions to show migrations history information
        #########################################################

        self.migrationSummaryAct = EricAction(
            self.tr("Show Migrations Summary"),
            self.tr("Show Migrations &Summary"),
            0,
            0,
            self,
            "flask_show_migrations_summary",
        )
        self.migrationSummaryAct.setStatusTip(
            self.tr("Show a summary of the created database migrations")
        )
        self.migrationSummaryAct.setWhatsThis(
            self.tr(
                """<b>Show Migrations Summary</b>"""
                """<p>Shows a summary list of the created database"""
                """ migrations.</p>"""
            )
        )
        self.migrationSummaryAct.triggered.connect(self.__showMigrationsSummary)
        self.actions.append(self.migrationSummaryAct)

        self.migrationHistoryAct = EricAction(
            self.tr("Show Migrations History"),
            self.tr("Show Migrations &History"),
            0,
            0,
            self,
            "flask_show_migrations_history",
        )
        self.migrationHistoryAct.setStatusTip(
            self.tr("Show the full history of the created database migrations")
        )
        self.migrationHistoryAct.setWhatsThis(
            self.tr(
                """<b>Show Migrations History</b>"""
                """<p>Shows the full history of the created database"""
                """ migrations.</p>"""
            )
        )
        self.migrationHistoryAct.triggered.connect(self.__showMigrationsHistory)
        self.actions.append(self.migrationHistoryAct)

    def initMenu(self):
        """
        Public slot to initialize the Pyramid menu.

        @return the menu generated
        @rtype QMenu
        """
        self.__menus = {}  # clear menus references

        # Database menu
        dbMenu = QMenu(self.tr("Database"))
        dbMenu.setTearOffEnabled(True)

        dbMenu.addAction(self.initializeDbAct)
        dbMenu.addSeparator()
        dbMenu.addAction(self.migrateCreateAct)
        dbMenu.addSeparator()
        dbMenu.addAction(self.upgradeDatabaseAct)
        dbMenu.addAction(self.downgradeDatabaseAct)
        dbMenu.addSeparator()
        dbMenu.addAction(self.migrationSummaryAct)
        dbMenu.addAction(self.migrationHistoryAct)

        # main Pyramid menu
        menu = QMenu(self.tr("P&yramid"), self.__ui)
        menu.setTearOffEnabled(True)

        menu.addAction(self.selectProjectAct)
        menu.addSeparator()
        menu.addAction(self.runServerAct)
        menu.addAction(self.runBrowserAct)
        menu.addSeparator()
        menu.addAction(self.runPythonShellAct)
        menu.addSeparator()
        menu.addAction(self.createProjectAct)
        menu.addSeparator()
        menu.addMenu(dbMenu)
        menu.addSeparator()
        menu.addAction(self.showViewsAct)
        menu.addAction(self.showRoutesAct)
        menu.addAction(self.showTweensAct)
        menu.addSeparator()
        menu.addAction(self.buildDistroAct)
        menu.addSeparator()
        menu.addAction(self.documentationAct)
        menu.addSeparator()
        menu.addAction(self.aboutPyramidAct)

        self.__menus["main"] = menu
        self.__menus["database"] = dbMenu

        self.__setCurrentProject(None)

        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
        """
        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())

    def registerOpenHook(self):
        """
        Public method to register the open hook to open a translations file
        in a translations editor.
        """
        if self.__hooksInstalled:
            editor = self.__plugin.getPreferences("TranslationsEditor")
            if editor:
                self.__translationsBrowser.addHookMethodAndMenuEntry(
                    "open",
                    self.openPOEditor,
                    self.tr("Open with {0}").format(os.path.basename(editor)),
                )
            else:
                self.__translationsBrowser.removeHookMethod("open")

    def projectOpenedHooks(self):
        """
        Public method to add our hook methods.
        """
        if self.__ericProject.getProjectType() == "Pyramid":
            self.__formsBrowser = (
                ericApp().getObject("ProjectBrowser").getProjectBrowser("forms")
            )
            self.__formsBrowser.addHookMethodAndMenuEntry(
                "newForm", self.newForm, self.tr("New template...")
            )

            self.__ericProject.projectLanguageAddedByCode.connect(
                self.__projectLanguageAdded
            )
            self.__translationsBrowser = (
                ericApp().getObject("ProjectBrowser").getProjectBrowser("translations")
            )
            self.__translationsBrowser.addHookMethodAndMenuEntry(
                "extractMessages", self.extractMessages, self.tr("Extract Messages")
            )
            self.__translationsBrowser.addHookMethodAndMenuEntry(
                "releaseAll", self.compileCatalogs, self.tr("Compile All Catalogs")
            )
            self.__translationsBrowser.addHookMethodAndMenuEntry(
                "releaseSelected",
                self.compileSelectedCatalogs,
                self.tr("Compile Selected Catalogs"),
            )
            self.__translationsBrowser.addHookMethodAndMenuEntry(
                "generateAll", self.updateCatalogs, self.tr("Update All Catalogs")
            )
            self.__translationsBrowser.addHookMethodAndMenuEntry(
                "generateSelected",
                self.updateSelectedCatalogs,
                self.tr("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.__ericProject.projectLanguageAddedByCode.disconnect(
                self.__projectLanguageAdded
            )
            self.__translationsBrowser.removeHookMethod("extractMessages")
            self.__translationsBrowser.removeHookMethod("releaseAll")
            self.__translationsBrowser.removeHookMethod("releaseSelected")
            self.__translationsBrowser.removeHookMethod("generateAll")
            self.__translationsBrowser.removeHookMethod("generateSelected")
            self.__translationsBrowser.removeHookMethod("open")
            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
        @type str
        """
        from .FormSelectionDialog import FormSelectionDialog

        dlg = FormSelectionDialog(parent=self.__ui)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            template = dlg.getTemplateText()

            fileFilters = self.tr(
                "Chameleon Templates (*.pt);;"
                "Chameleon Text Templates (*.txt);;"
                "Mako Templates (*.mako);;"
                "Mako Templates (*.mak);;"
                "HTML Files (*.html);;"
                "HTML Files (*.htm);;"
                "All Files (*)"
            )
            fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
                self.__ui,
                self.tr("New Form"),
                path,
                fileFilters,
                None,
                EricFileDialog.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 = EricMessageBox.yesNo(
                        self.__ui,
                        self.tr("New Form"),
                        self.tr("""The file already exists! Overwrite it?"""),
                        icon=EricMessageBox.Warning,
                    )
                    if not res:
                        # user selected to not overwrite
                        return

                try:
                    with open(fname, "w", encoding="utf-8") as f:
                        f.write(template)
                except OSError as e:
                    EricMessageBox.critical(
                        self.__ui,
                        self.tr("New Form"),
                        self.tr(
                            "<p>The new form file <b>{0}</b> could"
                            " not be created.<br/> Problem: {1}</p>"
                        ).format(fname, e),
                    )
                    return

                self.__ericProject.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)

        for dlg in (self.__migrationSummaryDialog,):
            if dlg is not None:
                dlg.close()

    def __getExecutablePaths(self, file):
        """
        Private method to build all full paths of an executable file from
        the environment.

        @param file filename of the executable
        @type str
        @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.
        @rtype list of str
        """
        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) and 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 directory in dirs:
                exe = os.path.join(directory, 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
        @rtype list of str
        """
        variants = []
        cmd = "cookiecutter"

        for variant in ["Python3"]:
            virtEnv = self.__getVirtualEnvironment(variant)
            if virtEnv:
                fullCmd = self.getPyramidCommand(cmd, variant)
                if fullCmd != cmd:
                    variants.append(variant)
            else:
                fullCmd = self.getPyramidCommand(cmd, variant)
                if isWindowsPlatform():
                    if fullCmd != cmd:
                        variants.append(variant)
                else:
                    if cmd:
                        try:
                            fullCmds = getExecutablePaths(cmd)
                        except AttributeError:
                            fullCmds = self.__getExecutablePaths(cmd)
                        for fullCmd in fullCmds:
                            try:
                                with open(fullCmd, "r", encoding="utf-8") as f:
                                    l0 = f.readline()
                            except OSError:
                                l0 = ""
                            if self.__isSuitableForVariant(variant, l0):
                                variants.append(variant)
                                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 command
        @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, language=""):
        """
        Private method to get the path of the virtual environment.

        @param language Python variant to get the virtual environment
            for (one of '' or 'Python3')
        @type str
        @return path of the virtual environment
        @rtype str
        """
        if not language:
            language = self.__ericProject.getProjectLanguage()
        if self.__virtualEnvManager:
            if language == "Python3":
                venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3")
            else:
                venvName = ""
            if venvName:
                virtEnv = self.__virtualEnvManager.getVirtualenvDirectory(venvName)
                if not virtEnv:
                    virtEnv = os.path.dirname(
                        self.__virtualEnvManager.getVirtualenvInterpreter(venvName)
                    )
                    if virtEnv.endswith(("Scripts", "bin")):
                        virtEnv = os.path.dirname(virtEnv)
            else:
                virtEnv = ""

        if virtEnv and not os.path.exists(virtEnv):
            virtEnv = ""
        return virtEnv  # __IGNORE_WARNING_M834__

    def __getDebugEnvironment(self, language=""):
        """
        Private method to get the path of the debugger environment.

        @param language Python variant to get the debugger environment
            for (one of '' or 'Python3')
        @type str
        @return path of the debugger environment
        @rtype str
        """
        if not language:
            language = self.__ericProject.getProjectLanguage()
        if self.__virtualEnvManager:
            debugEnv = self.__getVirtualEnvironment(language)
            if not debugEnv:
                if language == "Python3":
                    venvName = Preferences.getDebugger("Python3VirtualEnv")
                else:
                    venvName = ""

                if venvName:
                    return self.__virtualEnvManager.getVirtualenvDirectory(venvName)

        return ""

    def getProjectVirtualEnvironment(self):
        """
        Public method to generate the path of the project virtual environment.

        @return path of the Pyramid project virtual environment
        @rtype str
        """
        return os.path.join(self.projectPath(), "env")

    def getPyramidCommand(self, cmd, language="", virtualEnv=""):
        """
        Public method to build a Pyramid command.

        @param cmd command
        @type str
        @param language Python variant to get the virtual environment
            for (one of '' or 'Python3')
        @type str
        @param virtualEnv path of the project's Python virtual environment
        @type str
        @return full pyramid command
        @rtype str
        """
        if not language:
            language = self.__ericProject.getProjectLanguage()

        if not virtualEnv:
            virtualEnv = self.__getVirtualEnvironment(language)
            if not virtualEnv:
                virtualEnv = self.__getDebugEnvironment(language)
        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),
                getExecutablePath(cmd),
                cmd,  # fall back to just cmd
            ]
            for cmd in fullCmds:
                if os.path.exists(cmd):
                    break
        return cmd

    def __assemblePyramidCommand(self, cmd, virtualEnv):
        """
        Private method to assemble the full pyramid command for a given virtual
        environment.

        @param cmd command
        @type str
        @param virtualEnv path of the project's Python virtual environment
        @type str
        @return assembled pyramid command
        @rtype str
        """
        return (
            os.path.join(virtualEnv, "Scripts", cmd + ".exe")
            if isWindowsPlatform()
            else os.path.join(virtualEnv, "bin", cmd)
        )

    def getPythonCommand(self):
        """
        Public method to build the Python command.

        @return python command
        @rtype str
        """
        language = self.__ericProject.getProjectLanguage()
        if self.__virtualEnvManager:
            if language == "Python3":
                venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3")
                if not venvName:
                    # if none configured, use the global one
                    venvName = Preferences.getDebugger("Python3VirtualEnv")
            else:
                venvName = ""
            if venvName:
                return self.__virtualEnvManager.getVirtualenvInterpreter(venvName)

        return ""

    def __pyramidInfo(self):
        """
        Private slot to show some info about Pyramid.
        """
        try:
            version = self.getPyramidVersionString()
        except PyramidNoProjectSelectedError:
            version = self.tr("not available")
        url = "http://www.pylonsproject.org/projects/pyramid/about"

        msgBox = EricMessageBox.EricMessageBox(
            EricMessageBox.Question,
            self.tr("About Pyramid"),
            self.tr(
                "<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=EricMessageBox.Ok,
            parent=self.__ui,
        )
        msgBox.setIconPixmap(
            EricPixmapCache.getPixmap(
                os.path.join(
                    "ProjectPyramid", "icons", "pyramid64-{0}".format(self.__iconSuffix)
                )
            )
        )
        msgBox.exec()

    def getPyramidVersionString(self):
        """
        Public method to get the Pyramid version as a string.

        @return Pyramid version
        @rtype str
        """
        if not self.__pyramidVersion:
            cmd = self.getPyramidCommand(
                "pdistreport", virtualEnv=self.getProjectVirtualEnvironment()
            )
            try:
                output = subprocess.check_output([cmd])  # secok
                outputLines = output.decode().splitlines()
                for line in outputLines:
                    if line.startswith("Pyramid version:"):
                        self.__pyramidVersion = line.rsplit(None, 1)[1]
                        break
            except (OSError, subprocess.CalledProcessError):
                self.__pyramidVersion = ""

        return self.__pyramidVersion

    def getPyramidVersion(self):
        """
        Public method to get the Pyramid version as a tuple.

        @return Pyramid version
        @rtype tuple of int
        """
        pyramidVersionStr = self.getPyramidVersionString()
        pyramidVersionList = []
        if pyramidVersionStr:
            for part in pyramidVersionStr.split("."):
                try:
                    pyramidVersionList.append(int(part))
                except ValueError:
                    pyramidVersionList.append(part)

        return tuple(pyramidVersionList)

    def isSpawningConsole(self, consoleCmd):
        """
        Public method to check, if the given console is a spawning console.

        @param consoleCmd console command
        @type str
        @return tuple of two entries giving an indication, if the console
            is spawning (boolean) and the (possibly) cleaned console command
        @rtype str
        """
        if consoleCmd and consoleCmd[0] == "@":
            return (True, consoleCmd[1:])
        else:
            return (False, consoleCmd)

    def __adjustWorkingDirectory(self, args, wd):
        """
        Private method to adjust the working directory in the arguments list.

        @param args list of arguments to be modified
        @type list of str
        @param wd working directory
        @type str
        """
        if args[0].endswith("konsole") and "--workdir" in args:
            index = args.index("--workdir")
            args[index + 1] = wd
        elif args[0].endswith(("gnome-terminal", "mate-terminal")):
            for index in range(len(args)):
                if args[index].startswith("--working-directory="):
                    args[index] = "--working-directory={0}".format(wd)
                    break

    ##################################################################
    ## slots below implement creation functions
    ##################################################################

    def __createProject(self):
        """
        Private slot to create a new Pyramid project.
        """
        from .CreateParametersDialog import CreateParametersDialog

        dlg = CreateParametersDialog(parent=self.__ui)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            template, version, overwrite, contextData = dlg.getData()

            cmd = self.getPyramidCommand("cookiecutter")
            args = ["--no-input"]
            if overwrite:
                args.append("--overwrite-if-exists")
            if version:
                args += ["--checkout", version]
            args.append(template)
            for context, data in contextData.items():
                args.append("{0}={1}".format(context, data))
            dlg = PyramidDialog(self.tr("Create Pyramid Project"), parent=self.__ui)
            if dlg.startProcess(cmd, args, self.__ericProject.getProjectPath()):
                dlg.exec()
                if dlg.normalExit() and "repo_name" in contextData:
                    # search for files created by cookiecutter and add them
                    # to the project
                    projectPath = os.path.join(
                        self.__ericProject.getProjectPath(), contextData["repo_name"]
                    )
                    for entry in os.walk(projectPath):
                        for fileName in entry[2]:
                            fullName = os.path.join(entry[0], fileName)
                            self.__ericProject.appendFile(fullName)

                    # create the base directory for translations
                    i18nPath = os.path.join(
                        projectPath, contextData["repo_name"].lower(), "i18n"
                    )
                    if not os.path.exists(i18nPath):
                        os.makedirs(i18nPath)
                    self.__ericProject.setDirty(True)

                    combinedOutput = False
                    argsLists = []

                    # 1. create a Python virtual environment for the project
                    argsLists.append([sys.executable, "-m", "venv", "env"])
                    # 2. upgrade packaging tools
                    python = self.__assemblePyramidCommand(
                        "python", os.path.join(projectPath, "env")
                    )
                    argsLists.append(
                        [
                            python,
                            "-m",
                            "pip",
                            "install",
                            "--upgrade",
                            "pip",
                            "setuptools",
                        ]
                    )
                    # 3. install project in editable mode with testing
                    argsLists.append(
                        [python, "-m", "pip", "install", "-e", ".[testing]"]
                    )

                    if (
                        "backend" in contextData
                        and contextData["backend"] == "sqlalchemy"
                    ):
                        # only SQLAlchemy needs initialization of alembic
                        combinedOutput = True

                        # 4. initialize database
                        alembic = self.__assemblePyramidCommand(
                            "alembic", os.path.join(projectPath, "env")
                        )
                        argsLists.append(
                            [
                                alembic,
                                "-c",
                                "development.ini",
                                "revision",
                                "--autogenerate",
                                "--message",
                                "initialized database",
                            ]
                        )
                        # 5. upgrade database to initial version
                        argsLists.append(
                            [alembic, "-c", "development.ini", "upgrade", "head"]
                        )

                    dlg = PyramidDialog(
                        self.tr("Initializing Pyramid Project"),
                        combinedOutput=combinedOutput,
                        parent=self.__ui,
                    )
                    if dlg.startBatchProcesses(argsLists, workingDir=projectPath):
                        dlg.exec()

                    self.__setCurrentProject(contextData["repo_name"])

                    if (
                        "backend" in contextData
                        and contextData["backend"] == "sqlalchemy"
                    ):
                        # add the alembic files created above to the project
                        migrationsPath = self.migrationsPath()
                        for entry in os.walk(migrationsPath):
                            for fileName in entry[2]:
                                fullName = os.path.join(entry[0], fileName)
                                self.__ericProject.appendFile(fullName)

    ##################################################################
    ## 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
        @rtype list of str
        """
        projects = []
        ppath = self.__ericProject.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 sorted(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.tr("Select Pyramid Project"),
                self.tr("Select the Pyramid project to work with."),
                projects,
                cur,
                False,
            )
            if not ok:
                projects = None
        self.__setCurrentProject(project)

    def projectPath(self):
        """
        Public method to calculate the full path of the Pyramid project.

        @return path of the project
        @rtype str
        @exception PyramidNoProjectSelectedError raised, if no project is
            selected
        """
        if self.__currentProject is None:
            self.__selectProject()

        if self.__currentProject is None:
            raise PyramidNoProjectSelectedError
        else:
            return os.path.join(
                self.__ericProject.getProjectPath(), self.__currentProject
            )

    def __setCurrentProject(self, project):
        """
        Private slot to set the current project.

        @param project name of the project
        @type str
        """
        if project is not None and len(project) == 0:
            self.__currentProject = None
        else:
            self.__currentProject = project

        curProject = (
            self.tr("None") if self.__currentProject is None else self.__currentProject
        )
        self.selectProjectAct.setText(
            self.tr("&Current Pyramid Project ({0})").format(curProject)
        )

        if self.__currentProject is None:
            self.__ericProject.setTranslationPattern("")
        else:
            lowerProject = self.__project().lower()
            config = configparser.ConfigParser()
            config.read(os.path.join(self.projectPath(), "setup.cfg"))
            try:
                outputDir = config.get("init_catalog", "output_dir")
            except (configparser.NoOptionError, configparser.NoSectionError):
                outputDir = "{0}/locale".format(lowerProject)
            try:
                domain = config.get("init_catalog", "domain")
            except (configparser.NoOptionError, configparser.NoSectionError):
                domain = lowerProject
            self.__ericProject.setTranslationPattern(
                os.path.join(
                    project,
                    outputDir,
                    "%language%",
                    "LC_MESSAGES",
                    "{0}.po".format(domain),
                )
            )

        if self.__currentProject is None:
            self.initializeDbAct.setEnabled(False)
            with contextlib.suppress(KeyError):
                self.__menus["database"].setEnabled(False)
        else:
            initCmd = self.__getInitDbCommand()
            self.initializeDbAct.setEnabled(os.path.exists(initCmd))

            alembicDir = os.path.join(
                self.projectPath(), self.__currentProject, "alembic", "versions"
            )
            self.__menus["database"].setEnabled(os.path.exists(alembicDir))

        self.runServerAct.setEnabled(project is not None)
        self.runBrowserAct.setEnabled(project is not None)
        self.runPythonShellAct.setEnabled(project is not None)
        self.showViewsAct.setEnabled(project is not None)
        self.showRoutesAct.setEnabled(project is not None)
        self.showTweensAct.setEnabled(project is not None)
        self.buildDistroAct.setEnabled(project is not None)

    def __project(self):
        """
        Private method to get the name of the current Pyramid project.

        @return name of the project
        @rtype str
        @exception PyramidNoProjectSelectedError raised, if no project is
            selected
        """
        if self.__currentProject is None:
            self.__selectProject()

        if self.__currentProject is None:
            raise PyramidNoProjectSelectedError
        else:
            return self.__currentProject

    ##################################################################
    ## slots below implement run functions
    ##################################################################

    def __runServer(self):
        """
        Private slot to start the Pyramid Web server.
        """
        consoleCmd = self.isSpawningConsole(
            self.__plugin.getPreferences("ConsoleCommand")
        )[1]
        if consoleCmd:
            try:
                projectPath = self.projectPath()
            except PyramidNoProjectSelectedError:
                EricMessageBox.warning(
                    self.__ui,
                    self.tr("Run Server"),
                    self.tr(
                        "No current Pyramid project selected or no"
                        " Pyramid project created yet. Aborting..."
                    ),
                )
                return

            args = Utilities.parseOptionString(consoleCmd)
            args[0] = getExecutablePath(args[0])
            args.append(
                self.getPyramidCommand(
                    "pserve", virtualEnv=self.getProjectVirtualEnvironment()
                )
            )
            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:
                EricMessageBox.critical(
                    self.__ui,
                    self.tr("Process Generation Error"),
                    self.tr("The Pyramid server could not be started."),
                )

    def __serverProcFinished(self):
        """
        Private slot connected to the finished signal.
        """
        if (
            self.__serverProc is not None
            and self.__serverProc.state() != QProcess.ProcessState.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 PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                self.tr("Run Web-Browser"),
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return

        config = configparser.ConfigParser()
        config.read(os.path.join(projectPath, "development.ini"))
        try:
            listen = config.get("server:main", "listen")
        except (configparser.NoOptionError, configparser.NoSectionError):
            listen = "localhost:6543"
        url = "http://{0}".format(listen)
        if self.__plugin.getPreferences("UseExternalBrowser"):
            res = QDesktopServices.openUrl(QUrl(url))
            if not res:
                EricMessageBox.critical(
                    self.__ui,
                    self.tr("Run Web-Browser"),
                    self.tr(
                        'Could not start the web-browser for the URL "{0}".'
                    ).format(url.toString()),
                )
        else:
            self.__ui.launchHelpViewer(url)

    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 PyramidNoProjectSelectedError:
                EricMessageBox.warning(
                    self.__ui,
                    self.tr("Start Pyramid Python Console"),
                    self.tr(
                        "No current Pyramid project selected or no"
                        " Pyramid project created yet. Aborting..."
                    ),
                )
                return

            args = Utilities.parseOptionString(consoleCmd)
            args[0] = getExecutablePath(args[0])
            args.append(
                self.getPyramidCommand(
                    "pshell", virtualEnv=self.getProjectVirtualEnvironment()
                )
            )
            consoleType = self.__plugin.getPreferences("Python3ConsoleType")
            args.append("--python-shell={0}".format(consoleType))
            args.append(os.path.join(projectPath, "development.ini"))

            self.__adjustWorkingDirectory(args, projectPath)
            started, pid = QProcess.startDetached(args[0], args[1:], projectPath)
            if not started:
                EricMessageBox.critical(
                    self.__ui,
                    self.tr("Process Generation Error"),
                    self.tr("The Pyramid Shell process could not be started."),
                )

    ##################################################################
    ## slots below implement distribution functions
    ##################################################################

    def __buildDistribution(self):
        """
        Private slot to build a distribution file for the current Pyramid
        project.
        """
        from .DistributionTypeSelectionDialog import DistributionTypeSelectionDialog

        title = self.tr("Build Distribution File")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return

        dlg = DistributionTypeSelectionDialog(self, projectPath, parent=self.__ui)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            formats = dlg.getFormats()
            cmd = self.getPyramidCommand(
                "python", virtualEnv=self.getProjectVirtualEnvironment()
            )
            args = []
            args.append("setup.py")
            args.append("sdist")
            if formats:
                args.append("--formats={0}".format(",".join(formats)))

            dia = PyramidDialog(
                title,
                msgSuccess=self.tr("Python distribution file built successfully."),
                parent=self.__ui,
            )
            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.tr("Show Matching Views")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return

        url, ok = QInputDialog.getText(
            self.__ui,
            self.tr("Show Matching Views"),
            self.tr("Enter the URL to be matched:"),
            QLineEdit.EchoMode.Normal,
            "/",
        )
        if not ok or url == "":
            return

        cmd = self.getPyramidCommand(
            "pviews", virtualEnv=self.getProjectVirtualEnvironment()
        )
        args = []
        args.append("development.ini")
        args.append(url)

        dia = PyramidDialog(title, fixed=True, linewrap=False, parent=self.__ui)
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec()

    def __showRoutes(self):
        """
        Private slot showing all URL dispatch routes.
        """
        from .PyramidRoutesDialog import PyramidRoutesDialog

        title = self.tr("Show Routes")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return

        dia = PyramidRoutesDialog(self, parent=self.__ui)
        res = dia.start(projectPath)
        if res:
            dia.exec()

    def __showTweens(self):
        """
        Private slot showing all implicit and explicit tween objects.
        """
        title = self.tr("Show Tween Objects")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return

        cmd = self.getPyramidCommand(
            "ptweens", virtualEnv=self.getProjectVirtualEnvironment()
        )
        args = []
        args.append("development.ini")

        dia = PyramidDialog(title, fixed=True, linewrap=False, parent=self.__ui)
        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
        @type str
        @return extracted locale or None
        @rtype str
        """
        if self.__ericProject.getTranslationPattern():
            # On Windows, path typically contains backslashes. This leads
            # to an invalid search pattern '...\(' because the opening bracket
            # will be escaped.
            pattern = self.__ericProject.getTranslationPattern()
            pattern = os.path.normpath(pattern)
            pattern = pattern.replace("%language%", "(.*?)")
            pattern = pattern.replace("\\", "\\\\")
            match = re.search(pattern, filename)
            if match is not None:
                return match.group(1)

        return None

    def __normalizeList(self, filenames):
        """
        Private method to normalize a list of file names.

        @param filenames list of file names to normalize
        @type list of str
        @return normalized file names
        @rtype list of str
        """
        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
        @type list of str
        @return file names belonging to the current site
        @rtype list of str
        """
        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.tr("Extract messages")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return

        config = configparser.ConfigParser()
        config.read(os.path.join(projectPath, "setup.cfg"))
        try:
            potFile = config.get("extract_messages", "output_file")
        except configparser.NoSectionError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    'No setup.cfg found or no "extract_messages"'
                    " section found in setup.cfg."
                ),
            )
            return
        except configparser.NoOptionError:
            EricMessageBox.warning(
                self.__ui, title, self.tr('No "output_file" option found in setup.cfg.')
            )
            return

        with contextlib.suppress(OSError):
            path = os.path.join(projectPath, os.path.dirname(potFile))
            os.makedirs(path)

        cmd = self.getPythonCommand()
        args = []
        args.append("setup.py")
        args.append("extract_messages")

        dia = PyramidDialog(
            title,
            msgSuccess=self.tr("\nMessages extracted successfully."),
            parent=self.__ui,
        )
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec()
            self.__ericProject.appendFile(os.path.join(projectPath, potFile))

    def __projectLanguageAdded(self, code):
        """
        Private slot handling the addition of a new language.

        @param code language code of the new language
        @type str
        """
        title = self.tr("Initializing message catalog for '{0}'").format(code)
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "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.tr("\nMessage catalog initialized successfully."),
            parent=self.__ui,
        )
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec()

            langFile = self.__ericProject.getTranslationPattern().replace(
                "%language%", code
            )
            self.__ericProject.appendFile(langFile)

    def compileCatalogs(self, filenames):  # noqa: U100
        """
        Public method to compile the message catalogs.

        @param filenames list of filenames (not used)
        @type list of str
        """
        title = self.tr("Compiling message catalogs")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "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.tr("\nMessage catalogs compiled successfully."),
            parent=self.__ui,
        )
        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.__ericProject.appendFile(fullName)

    def compileSelectedCatalogs(self, filenames):
        """
        Public method to update the message catalogs.

        @param filenames list of file names
        @type list of str
        """
        title = self.tr("Compiling message catalogs")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "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:
            EricMessageBox.warning(
                self.__ui, title, self.tr("No locales detected. Aborting...")
            )
            return

        dia = PyramidDialog(
            title,
            msgSuccess=self.tr("\nMessage catalogs compiled successfully."),
            parent=self.__ui,
        )
        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.__ericProject.appendFile(fullName)

    def updateCatalogs(self, filenames):  # noqa: U100
        """
        Public method to update the message catalogs.

        @param filenames list of filenames (not used)
        @type list of str
        """
        title = self.tr("Updating message catalogs")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "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.tr("\nMessage catalogs updated successfully."),
            parent=self.__ui,
        )
        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
        @type list of str
        """
        title = self.tr("Updating message catalogs")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "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:
            EricMessageBox.warning(
                self.__ui, title, self.tr("No locales detected. Aborting...")
            )
            return

        dia = PyramidDialog(
            title,
            msgSuccess=self.tr("\nMessage catalogs updated successfully."),
            parent=self.__ui,
        )
        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
        @type str
        """
        editor = self.__plugin.getPreferences("TranslationsEditor")
        if poFile.endswith(".po") and editor:
            try:
                wd = self.projectPath()
            except PyramidNoProjectSelectedError:
                wd = ""
            started, pid = QProcess.startDetached(editor, [poFile], wd)
            if not started:
                EricMessageBox.critical(
                    None,
                    self.tr("Process Generation Error"),
                    self.tr(
                        "The translations editor process ({0}) could not be started."
                    ).format(os.path.basename(editor)),
                )

    #######################################################################
    ## database related methods and slots below
    #######################################################################

    def getAlembicCommand(self):
        """
        Public method to get the path to the alembic executable of the current
        Pyramid project.

        @return path to the alembic executable
        @rtype str
        """
        return self.getPyramidCommand(
            "alembic", virtualEnv=self.getProjectVirtualEnvironment()
        )

    def migrationsPath(self):
        """
        Public method to get the path to the migrations directory of the
        current Pyramid project.

        @return pathof the directory containing the migrations
        @rtype str
        """
        return os.path.join(
            self.projectPath(), self.__currentProject, "alembic", "versions"
        )

    def __getInitDbCommand(self):
        """
        Private method to create the path to the initialization script.

        @return path to the initialization script
        @rtype str
        """
        try:
            cmd = "initialize_{0}_db".format(self.__project())
            return self.getPyramidCommand(
                cmd, virtualEnv=self.getProjectVirtualEnvironment()
            )
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                self.tr("Initialize Database"),
                self.tr(
                    "No current Pyramid project selected or no Pyramid"
                    " project created yet. Aborting..."
                ),
            )
            return ""

    @pyqtSlot()
    def __initializeDatabase(self):
        """
        Private slot to initialize the database of the Pyramid project.
        """
        title = self.tr("Initialize Database")
        try:
            projectPath = self.projectPath()
        except PyramidNoProjectSelectedError:
            EricMessageBox.warning(
                self.__ui,
                title,
                self.tr(
                    "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.tr("Database initialized successfully."),
            parent=self.__ui,
        )
        res = dia.startProcess(cmd, args, projectPath)
        if res:
            dia.exec()

    @pyqtSlot()
    def __createMigration(self):
        """
        Private slot to create a new database migration.
        """
        title = self.tr("Create Migration")
        projectPath = self.projectPath()
        migrations = self.migrationsPath()

        message, ok = QInputDialog.getText(
            None,
            title,
            self.tr("Enter a short message for the migration:"),
            QLineEdit.EchoMode.Normal,
        )
        if ok:
            args = ["-c", "development.ini", "revision", "--autogenerate"]
            if migrations:
                args += ["--version-path", migrations]
            if message:
                args += ["--message", message]

            dlg = PyramidDialog(
                title,
                msgSuccess=self.tr("\nMigration created successfully."),
                linewrap=False,
                combinedOutput=True,
                parent=self.__ui,
            )
            if dlg.startProcess(self.getAlembicCommand(), args, workingDir=projectPath):
                dlg.exec()
                if dlg.normalExit():
                    versionsPattern = os.path.join(migrations, "*.py")
                    for fileName in glob.iglob(versionsPattern):
                        self.__ericProject.appendFile(fileName)

    @pyqtSlot()
    def upgradeDatabase(self, revision=None):
        """
        Public slot to upgrade the database to the head or a given version.

        @param revision migration revision to upgrade to
        @type str
        """
        title = self.tr("Upgrade Database")
        projectPath = self.projectPath()

        args = ["-c", "development.ini", "upgrade"]
        if revision:
            args.append(revision)
        else:
            args.append("head")

        dlg = PyramidDialog(
            title,
            msgSuccess=self.tr("\nDatabase upgraded successfully."),
            linewrap=False,
            combinedOutput=True,
            parent=self.__ui,
        )
        if dlg.startProcess(self.getAlembicCommand(), args, workingDir=projectPath):
            dlg.exec()

    @pyqtSlot()
    def downgradeDatabase(self, revision=None):
        """
        Public slot to downgrade the database to the previous or a given
        version.

        @param revision migration revision to downgrade to
        @type str
        """
        title = self.tr("Downgrade Database")
        projectPath = self.projectPath()

        args = ["-c", "development.ini", "downgrade"]
        if revision:
            args.append(revision)
        else:
            args.append("-1")

        dlg = PyramidDialog(
            title,
            msgSuccess=self.tr("\nDatabase downgraded successfully."),
            linewrap=False,
            combinedOutput=True,
            parent=self.__ui,
        )
        if dlg.startProcess(self.getAlembicCommand(), args, workingDir=projectPath):
            dlg.exec()

    @pyqtSlot()
    def __showMigrationsSummary(self):
        """
        Private slot to show a migrations history summary.
        """
        from .MigrateSummaryDialog import MigrateSummaryDialog

        if self.__migrationSummaryDialog is None:
            self.__migrationSummaryDialog = MigrateSummaryDialog(self, parent=self.__ui)

        self.__migrationSummaryDialog.showSummary()

    @pyqtSlot()
    def __showMigrationsHistory(self):
        """
        Private slot to show the full migrations history.
        """
        title = self.tr("Migrations History")
        projectPath = self.projectPath()

        args = ["-c", "development.ini", "history", "--indicate-current", "--verbose"]

        dlg = PyramidDialog(
            title, linewrap=False, combinedOutput=True, parent=self.__ui
        )
        if dlg.startProcess(self.getAlembicCommand(), args, workingDir=projectPath):
            dlg.exec()

eric ide

mercurial