PluginMetricsRadon.py

Sun, 31 Dec 2017 16:59:09 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 31 Dec 2017 16:59:09 +0100
changeset 53
4a179abb79e6
parent 51
f2a137d4b1ea
child 55
755bc8e1485a
permissions
-rw-r--r--

Updated copyright for 2018.

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

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

"""
Module implementing the radon code metrics plug-in.
"""

from __future__ import unicode_literals

import os

from PyQt5.QtCore import pyqtSignal, QObject, QTranslator
from PyQt5.QtWidgets import QAction

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

from Project.ProjectBrowserModel import ProjectBrowserFileItem

import Preferences
from Utilities import determinePythonVersion

# Start-Of-Header
name = "Radon Metrics Plugin"
author = "Detlev Offenbach <detlev@die-offenbachs.de>"
autoactivate = True
deactivateable = True
version = "1.1.1"
className = "RadonMetricsPlugin"
packageName = "RadonMetrics"
shortDescription = "Code metrics plugin using radon package"
longDescription = (
    """This plug-in implements dialogs to show various code metrics. These"""
    """ are determined using the radon code metrics package. 'Raw code"""
    """ metrics', 'Maintainability Index' and 'McCabe Complexity' can be"""
    """ requested through different dialogs for one file or the whole"""
    """ project."""
)
needsRestart = False
pyqtApi = 2
python2Compatible = True
# End-Of-Header

error = ""


class RadonMetricsPlugin(QObject):
    """
    Class implementing the radon code metrics plug-in.
    
    @signal metricsDone(str, dict) emitted when the code metrics were
        determined for a file
    @signal maintainabilityIndexDone(str, dict) emitted when the
        maintainability index was determined for a file
    @signal complexityDone(str, dict) emitted when the
        cyclomatic complexity was determined for a file
    @signal error(str, str, str) emitted in case of an error
    @signal batchFinished(str) emitted when a code metrics batch is done
    """
    metricsDone = pyqtSignal(str, dict)
    maintainabilityIndexDone = pyqtSignal(str, dict)
    complexityDone = pyqtSignal(str, dict)
    error = pyqtSignal(str, str, str)
    batchFinished = pyqtSignal(str)
    
    def __init__(self, ui):
        """
        Constructor
        
        @param ui reference to the user interface object
        @type UI.UserInterface
        """
        super(RadonMetricsPlugin, self).__init__(ui)
        self.__ui = ui
        self.__initialize()
        
        self.backgroundService = e5App().getObject("BackgroundService")
        
        path = os.path.join(os.path.dirname(__file__), packageName)
        try:
            # raw code metrics calculation
            self.backgroundService.serviceConnect(
                'radon_raw', 'Python2', path, 'CodeMetricsCalculator',
                lambda fn, res: self.metricsCalculationDone("raw", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy2(
                    "raw", fx, lang, fn, msg),
                onBatchDone=lambda fx, lang: self.batchJobDone(
                    "raw", fx, lang))
            self.backgroundService.serviceConnect(
                'radon_raw', 'Python3', path, 'CodeMetricsCalculator',
                lambda fn, res: self.metricsCalculationDone("raw", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy3(
                    "raw", fx, lang, fn, msg),
                onBatchDone=lambda fx, lang: self.batchJobDone(
                    "raw", fx, lang))
            
            # maintainability index calculation
            self.backgroundService.serviceConnect(
                'radon_mi', 'Python2', path, 'MaintainabilityIndexCalculator',
                lambda fn, res: self.metricsCalculationDone("mi", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy2(
                    "mi", fx, lang, fn, msg),
                onBatchDone=lambda fx, lang: self.batchJobDone(
                    "mi", fx, lang))
            self.backgroundService.serviceConnect(
                'radon_mi', 'Python3', path, 'MaintainabilityIndexCalculator',
                lambda fn, res: self.metricsCalculationDone("mi", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy3(
                    "mi", fx, lang, fn, msg),
                onBatchDone=lambda fx, lang: self.batchJobDone(
                    "mi", fx, lang))
            
            # cyclomatic complexity
            self.backgroundService.serviceConnect(
                'radon_cc', 'Python2', path, 'CyclomaticComplexityCalculator',
                lambda fn, res: self.metricsCalculationDone("cc", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy2(
                    "cc", fx, lang, fn, msg),
                onBatchDone=lambda fx, lang: self.batchJobDone(
                    "c", fx, lang))
            self.backgroundService.serviceConnect(
                'radon_cc', 'Python3', path, 'CyclomaticComplexityCalculator',
                lambda fn, res: self.metricsCalculationDone("cc", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy3(
                    "cc", fx, lang, fn, msg),
                onBatchDone=lambda fx, lang: self.batchJobDone(
                    "cc", fx, lang))
            
            self.hasBatch = True
        except TypeError:
            # backward compatibility for eric 6.0
            # raw code metrics calculation
            self.backgroundService.serviceConnect(
                'radon_raw', 'Python2', path, 'CodeMetricsCalculator',
                lambda fn, res: self.metricsCalculationDone("raw", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy2(
                    "raw", fx, lang, fn, msg))
            self.backgroundService.serviceConnect(
                'radon_raw', 'Python3', path, 'CodeMetricsCalculator',
                lambda fn, res: self.metricsCalculationDone("raw", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy3(
                    "raw", fx, lang, fn, msg))
            
            # maintainability index calculation
            self.backgroundService.serviceConnect(
                'radon_mi', 'Python2', path, 'MaintainabilityIndexCalculator',
                lambda fn, res: self.metricsCalculationDone("mi", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy2(
                    "mi", fx, lang, fn, msg))
            self.backgroundService.serviceConnect(
                'radon_mi', 'Python3', path, 'MaintainabilityIndexCalculator',
                lambda fn, res: self.metricsCalculationDone("mi", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy3(
                    "mi", fx, lang, fn, msg))
            
            # cyclomatic complexity
            self.backgroundService.serviceConnect(
                'radon_cc', 'Python2', path, 'CyclomaticComplexityCalculator',
                lambda fn, res: self.metricsCalculationDone("cc", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy2(
                    "cc", fx, lang, fn, msg))
            self.backgroundService.serviceConnect(
                'radon_cc', 'Python3', path, 'CyclomaticComplexityCalculator',
                lambda fn, res: self.metricsCalculationDone("cc", fn, res),
                onErrorCallback=lambda fx, lang, fn, msg: self.serviceErrorPy3(
                    "cc", fx, lang, fn, msg))
            
            self.hasBatch = False
        
        self.queuedBatches = {
            "raw": [],
            "mi": [],
            "cc": [],
        }
        self.batchesFinished = {
            "raw": True,
            "mi": True,
            "cc": True,
        }
        
        self.__translator = None
        self.__loadTranslator()
    
    def __serviceError(self, type_, fn, msg):
        """
        Private slot handling service errors.
        
        @param type_ type of the calculated metrics
        @type str, one of ["raw", "mi", "cc"]
        @param fn file name
        @type str
        @param msg message text
        @type str
        """
        self.error.emit(type_, fn, msg)
    
    def serviceErrorPy2(self, type_, fx, lang, fn, msg):
        """
        Public slot handling service errors for Python 2.
        
        @param type_ type of the calculated metrics
        @type str, one of ["raw", "mi", "cc"]
        @param fx service name
        @type str
        @param lang language
        @type str
        @param fn file name
        @type str
        @param msg message text
        @type str
        """
        if fx in ['radon_' + type_, 'batch_radon_' + type_] and \
                lang == 'Python2':
            if fx == 'radon_' + type_:
                self.__serviceError(type_, fn, msg)
            else:
                self.__serviceError(type_, self.tr("Python 2 batch job"), msg)
                self.batchJobDone(type_, fx, lang)
    
    def serviceErrorPy3(self, type_, fx, lang, fn, msg):
        """
        Public slot handling service errors for Python 3.
        
        @param type_ type of the calculated metrics
        @type str, one of ["raw", "mi", "cc"]
        @param fx service name
        @type str
        @param lang language
        @type str
        @param fn file name
        @type str
        @param msg message text
        @type str
        """
        if fx in ['radon_' + type_, 'batch_radon_' + type_] and \
                lang == 'Python3':
            if fx == 'radon_' + type_:
                self.__serviceError(type_, fn, msg)
            else:
                self.__serviceError(type_, self.tr("Python 3 batch job"), msg)
                self.batchJobDone(type_, fx, lang)
    
    def batchJobDone(self, type_, fx, lang):
        """
        Public slot handling the completion of a batch job.
        
        @param type_ type of the calculated metrics
        @type str, one of ["raw", "mi", "cc"]
        @param fx service name
        @type str
        @param lang language
        @type str
        """
        if fx in ['radon_' + type_, 'batch_radon_' + type_]:
            if lang in self.queuedBatches[type_]:
                self.queuedBatches[type_].remove(lang)
            # prevent sending the signal multiple times
            if len(self.queuedBatches[type_]) == 0 and \
                    not self.batchesFinished[type_]:
                self.batchFinished.emit(type_)
                self.batchesFinished[type_] = True
    
    def metricsCalculationDone(self, type_, filename, result):
        """
        Public slot to dispatch the result.
        
        @param type_ type of the calculated metrics
        @type str, one of ["raw", "mi", "cc"]
        @param filename name of the file the results belong to
        @type str
        @param result result dictionary
        @type dict
        """
        if type_ == "raw":
            self.metricsDone.emit(filename, result)
        elif type_ == "mi":
            self.maintainabilityIndexDone.emit(filename, result)
        elif type_ == "cc":
            self.complexityDone.emit(filename, result)
        else:
            self.error.emit(
                type_,
                filename,
                self.tr("Unknown metrics result received ({0}).").format(
                    type_)
            )
    
    def __initialize(self):
        """
        Private slot to (re)initialize the plugin.
        """
        self.__projectRawMetricsDialog = None
        self.__projectMIDialog = None
        self.__projectCCDialog = None
        self.__projectMetricsActs = []
        self.__projectSeparatorActs = []
        
        self.__projectBrowserRawMetricsDialog = None
        self.__projectBrowserMIDialog = None
        self.__projectBrowserCCDialog = None
        self.__projectBrowserMenu = None
        self.__projectBrowserMetricsActs = []
        self.__projectBrowserSeparatorActs = []
        
        self.__editors = []
        self.__editorRawMetricsDialog = None
        self.__editorMIDialog = None
        self.__editorCCDialog = None
        self.__editorMetricsActs = []
        self.__editorSeparatorActs = []

    def rawMetrics(self, lang, filename, source):
        """
        Public method to prepare raw code metrics calculation on one Python
        source file.

        @param lang language of the file or None to determine by internal
            algorithm
        @type str or None
        @param filename source filename
        @type str
        @param source string containing the code
        @type str
        """
        if lang is None:
            lang = 'Python{0}'.format(determinePythonVersion(filename, source))
        if lang not in ['Python2', 'Python3']:
            return
        
        self.backgroundService.enqueueRequest(
            'radon_raw', lang, filename, [source])

    def rawMetricsBatch(self, argumentsList):
        """
        Public method to prepare raw code metrics calculation on multiple
        Python source files.
        
        @param argumentsList list of arguments tuples with each tuple
            containing filename and source
        @type (str, str)
        """
        data = {
            "Python2": [],
            "Python3": [],
        }
        for filename, source in argumentsList:
            lang = 'Python{0}'.format(determinePythonVersion(filename, source))
            if lang not in ['Python2', 'Python3']:
                continue
            else:
                data[lang].append((filename, source))
        
        self.queuedBatches["raw"] = []
        for lang in ['Python2', 'Python3']:
            if data[lang]:
                self.queuedBatches["raw"].append(lang)
                self.backgroundService.enqueueRequest('batch_radon_raw', lang,
                                                      "", data[lang])
                self.batchesFinished["raw"] = False
    
    def cancelRawMetricsBatch(self):
        """
        Public method to cancel all batch jobs.
        """
        for lang in ['Python2', 'Python3']:
            self.backgroundService.requestCancel('batch_radon_raw', lang)

    def maintainabilityIndex(self, lang, filename, source):
        """
        Public method to prepare maintainability index calculation on one
        Python source file.

        @param lang language of the file or None to determine by internal
            algorithm
        @type str or None
        @param filename source filename
        @type str
        @param source string containing the code
        @type str
        """
        if lang is None:
            lang = 'Python{0}'.format(determinePythonVersion(filename, source))
        if lang not in ['Python2', 'Python3']:
            return
        
        self.backgroundService.enqueueRequest(
            'radon_mi', lang, filename, [source])

    def maintainabilityIndexBatch(self, argumentsList):
        """
        Public method to prepare maintainability index calculation on multiple
        Python source files.
        
        @param argumentsList list of arguments tuples with each tuple
            containing filename and source
        @type (str, str)
        """
        data = {
            "Python2": [],
            "Python3": [],
        }
        for filename, source in argumentsList:
            lang = 'Python{0}'.format(determinePythonVersion(filename, source))
            if lang not in ['Python2', 'Python3']:
                continue
            else:
                data[lang].append((filename, source))
        
        self.queuedBatches["mi"] = []
        for lang in ['Python2', 'Python3']:
            if data[lang]:
                self.queuedBatches["mi"].append(lang)
                self.backgroundService.enqueueRequest('batch_radon_mi', lang,
                                                      "", data[lang])
                self.batchesFinished["mi"] = False
    
    def cancelMaintainabilityIndexBatch(self):
        """
        Public method to cancel all batch jobs.
        """
        for lang in ['Python2', 'Python3']:
            self.backgroundService.requestCancel('batch_radon_mi', lang)
    
    def cyclomaticComplexity(self, lang, filename, source):
        """
        Public method to prepare cyclomatic complexity calculation on one
        Python source file.

        @param lang language of the file or None to determine by internal
            algorithm
        @type str or None
        @param filename source filename
        @type str
        @param source string containing the code
        @type str
        """
        if lang is None:
            lang = 'Python{0}'.format(determinePythonVersion(filename, source))
        if lang not in ['Python2', 'Python3']:
            return
        
        self.backgroundService.enqueueRequest(
            'radon_cc', lang, filename, [source])

    def cyclomaticComplexityBatch(self, argumentsList):
        """
        Public method to prepare cyclomatic complexity calculation on multiple
        Python source files.
        
        @param argumentsList list of arguments tuples with each tuple
            containing filename and source
        @type (str, str)
        """
        data = {
            "Python2": [],
            "Python3": [],
        }
        for filename, source in argumentsList:
            lang = 'Python{0}'.format(determinePythonVersion(filename, source))
            if lang not in ['Python2', 'Python3']:
                continue
            else:
                data[lang].append((filename, source))
        
        self.queuedBatches["raw"] = []
        for lang in ['Python2', 'Python3']:
            if data[lang]:
                self.queuedBatches["cc"].append(lang)
                self.backgroundService.enqueueRequest('batch_radon_cc', lang,
                                                      "", data[lang])
                self.batchesFinished["cc"] = False
    
    def cancelComplexityBatch(self):
        """
        Public method to cancel all batch jobs.
        """
        for lang in ['Python2', 'Python3']:
            self.backgroundService.requestCancel('batch_radon_cc', lang)

    def activate(self):
        """
        Public method to activate this plug-in.
        
        @return tuple of None and activation status
        @rtype (None, bool)
        """
        global error
        error = ""     # clear previous error
        
        # Project menu actions
        menu = e5App().getObject("Project").getMenu("Show")
        if menu:
            if not menu.isEmpty():
                act = menu.addSeparator()
                self.__projectSeparatorActs.append(act)
            
            # header action
            act = QAction(self.tr("Radon"), self)
            font = act.font()
            font.setBold(True)
            act.setFont(font)
            act.triggered.connect(self.__showRadonVersion)
            menu.addAction(act)
            self.__projectMetricsActs.append(act)
            
            act = E5Action(
                self.tr('Code Metrics'),
                self.tr('Code &Metrics...'), 0, 0,
                self, 'project_show_radon_raw')
            act.setStatusTip(
                self.tr('Show raw code metrics.'))
            act.setWhatsThis(self.tr(
                """<b>Code Metrics...</b>"""
                """<p>This calculates raw code metrics of Python files"""
                """ and shows the amount of lines of code, logical lines"""
                """ of code, source lines of code, comment lines,"""
                """ multi-line strings and blank lines.</p>"""
            ))
            act.triggered.connect(self.__projectRawMetrics)
            menu.addAction(act)
            self.__projectMetricsActs.append(act)
            
            act = E5Action(
                self.tr('Maintainability Index'),
                self.tr('Maintainability &Index...'), 0, 0,
                self, 'project_show_radon_mi')
            act.setStatusTip(
                self.tr('Show the maintainability index for Python files.'))
            act.setWhatsThis(self.tr(
                """<b>Maintainability Index...</b>"""
                """<p>This calculates the maintainability index of Python"""
                """ files and shows it together with a ranking.</p>"""
            ))
            act.triggered.connect(self.__projectMaintainabilityIndex)
            menu.addAction(act)
            self.__projectMetricsActs.append(act)
            
            act = E5Action(
                self.tr('Cyclomatic Complexity'),
                self.tr('Cyclomatic &Complexity...'), 0, 0,
                self, 'project_show_radon_cc')
            act.setStatusTip(
                self.tr('Show the cyclomatic complexity for Python files.'))
            act.setWhatsThis(self.tr(
                """<b>Cyclomatic Complexity...</b>"""
                """<p>This calculates the cyclomatic complexity of Python"""
                """ files and shows it together with a ranking.</p>"""
            ))
            act.triggered.connect(self.__projectCyclomaticComplexity)
            menu.addAction(act)
            self.__projectMetricsActs.append(act)
            
            act = menu.addSeparator()
            self.__projectSeparatorActs.append(act)
            
            e5App().getObject("Project").addE5Actions(
                self.__projectMetricsActs[1:])
        
        # Editor menu actions
        act = QAction(self)
        act.setSeparator(True)
        self.__editorSeparatorActs.append(act)
        act = QAction(self)
        act.setSeparator(True)
        self.__editorSeparatorActs.append(act)
        
        # header action
        act = QAction(self.tr("Radon"), self)
        font = act.font()
        font.setBold(True)
        act.setFont(font)
        act.triggered.connect(self.__showRadonVersion)
        self.__editorMetricsActs.append(act)
        
        act = E5Action(
            self.tr('Code Metrics'),
            self.tr('Code &Metrics...'), 0, 0,
            self, "")
        act.setStatusTip(
            self.tr('Show raw code metrics.'))
        act.setWhatsThis(self.tr(
            """<b>Code Metrics...</b>"""
            """<p>This calculates raw code metrics of Python files"""
            """ and shows the amount of lines of code, logical lines"""
            """ of code, source lines of code, comment lines,"""
            """ multi-line strings and blank lines.</p>"""
        ))
        act.triggered.connect(self.__editorRawMetrics)
        self.__editorMetricsActs.append(act)
        
        act = E5Action(
            self.tr('Maintainability Index'),
            self.tr('Maintainability &Index...'), 0, 0,
            self, "")
        act.setStatusTip(
            self.tr('Show the maintainability index for Python files.'))
        act.setWhatsThis(self.tr(
            """<b>Maintainability Index...</b>"""
            """<p>This calculates the maintainability index of Python"""
            """ files and shows it together with a ranking.</p>"""
        ))
        act.triggered.connect(self.__editorMaintainabilityIndex)
        self.__editorMetricsActs.append(act)
        
        act = E5Action(
            self.tr('Cyclomatic Complexity'),
            self.tr('Cyclomatic &Complexity...'), 0, 0,
            self, '')
        act.setStatusTip(
            self.tr('Show the cyclomatic complexity for Python files.'))
        act.setWhatsThis(self.tr(
            """<b>Cyclomatic Complexity...</b>"""
            """<p>This calculates the cyclomatic complexity of Python"""
            """ files and shows it together with a ranking.</p>"""
        ))
        act.triggered.connect(self.__editorCyclomaticComplexity)
        self.__editorMetricsActs.append(act)
        
        e5App().getObject("Project").showMenu.connect(self.__projectShowMenu)
        e5App().getObject("Project").projectClosed.connect(
            self.__projectClosed)
        e5App().getObject("ProjectBrowser").getProjectBrowser("sources")\
            .showMenu.connect(self.__projectBrowserShowMenu)
        e5App().getObject("ViewManager").editorOpenedEd.connect(
            self.__editorOpened)
        e5App().getObject("ViewManager").editorClosedEd.connect(
            self.__editorClosed)
        
        for editor in e5App().getObject("ViewManager").getOpenEditors():
            self.__editorOpened(editor)
        
        return None, True
    
    def deactivate(self):
        """
        Public method to deactivate this plug-in.
        """
        e5App().getObject("Project").showMenu.disconnect(
            self.__projectShowMenu)
        e5App().getObject("Project").projectClosed.disconnect(
            self.__projectClosed)
        e5App().getObject("ProjectBrowser").getProjectBrowser("sources")\
            .showMenu.disconnect(self.__projectBrowserShowMenu)
        e5App().getObject("ViewManager").editorOpenedEd.disconnect(
            self.__editorOpened)
        e5App().getObject("ViewManager").editorClosedEd.disconnect(
            self.__editorClosed)
        
        menu = e5App().getObject("Project").getMenu("Show")
        if menu:
            for sep in self.__projectSeparatorActs:
                menu.removeAction(sep)
            for act in self.__projectMetricsActs:
                menu.removeAction(act)
            e5App().getObject("Project").removeE5Actions(
                self.__projectMetricsActs[1:])
        
        if self.__projectBrowserMenu:
            for sep in self.__projectBrowserSeparatorActs:
                self.__projectBrowserMenu.removeAction(sep)
            for act in self.__projectBrowserMetricsActs:
                self.__projectBrowserMenu.removeAction(act)
        
        for editor in self.__editors:
            editor.showMenu.disconnect(self.__editorShowMenu)
            menu = editor.getMenu("Show")
            if menu is not None:
                for sep in self.__editorSeparatorActs:
                    menu.removeAction(sep)
                for act in self.__editorMetricsActs:
                    menu.removeAction(act)
        
        self.__initialize()
    
    def __loadTranslator(self):
        """
        Private method to load the translation file.
        """
        if self.__ui is not None:
            loc = self.__ui.getLocale()
            if loc and loc != "C":
                locale_dir = os.path.join(
                    os.path.dirname(__file__), "RadonMetrics", "i18n")
                translation = "radon_{0}".format(loc)
                translator = QTranslator(None)
                loaded = translator.load(translation, locale_dir)
                if loaded:
                    self.__translator = translator
                    e5App().installTranslator(self.__translator)
                else:
                    print("Warning: translation file '{0}' could not be"
                          " loaded.".format(translation))
                    print("Using default.")
    
    def __projectShowMenu(self, menuName, menu):
        """
        Private slot called, when the the project menu or a submenu is
        about to be shown.
        
        @param menuName name of the menu to be shown
        @type str
        @param menu reference to the menu
        @type QMenu
        """
        if menuName == "Show":
            for act in self.__projectMetricsActs[1:]:
                act.setEnabled(
                    e5App().getObject("Project").getProjectLanguage() in
                    ["Python3", "Python2", "Python"])
    
    def __projectBrowserShowMenu(self, menuName, menu):
        """
        Private slot called, when the the project browser context menu or a
        submenu is about to be shown.
        
        @param menuName name of the menu to be shown (string)
        @param menu reference to the menu (QMenu)
        """
        if menuName == "Show" and \
           e5App().getObject("Project").getProjectLanguage() in \
                ["Python3", "Python2", "Python"]:
            if self.__projectBrowserMenu is None:
                self.__projectBrowserMenu = menu
                
                act = menu.addSeparator()
                self.__projectBrowserSeparatorActs.append(act)
                
                # header action
                act = QAction(self.tr("Radon"), self)
                font = act.font()
                font.setBold(True)
                act.setFont(font)
                act.triggered.connect(self.__showRadonVersion)
                menu.addAction(act)
                self.__projectBrowserMetricsActs.append(act)
                
                act = E5Action(
                    self.tr('Code Metrics'),
                    self.tr('Code &Metrics...'), 0, 0,
                    self, '')
                act.setStatusTip(self.tr(
                    'Show raw code metrics.'))
                act.setWhatsThis(self.tr(
                    """<b>Code Metrics...</b>"""
                    """<p>This calculates raw code metrics of Python files"""
                    """ and shows the amount of lines of code, logical lines"""
                    """ of code, source lines of code, comment lines,"""
                    """ multi-line strings and blank lines.</p>"""
                ))
                act.triggered.connect(self.__projectBrowserRawMetrics)
                menu.addAction(act)
                self.__projectBrowserMetricsActs.append(act)
                
                act = E5Action(
                    self.tr('Maintainability Index'),
                    self.tr('Maintainability &Index...'), 0, 0,
                    self, '')
                act.setStatusTip(self.tr(
                    'Show the maintainability index for Python files.'))
                act.setWhatsThis(self.tr(
                    """<b>Maintainability Index...</b>"""
                    """<p>This calculates the maintainability index of"""
                    """ Python files and shows it together with a ranking."""
                    """</p>"""
                ))
                act.triggered.connect(
                    self.__projectBrowserMaintainabilityIndex)
                menu.addAction(act)
                self.__projectBrowserMetricsActs.append(act)
                
                act = E5Action(
                    self.tr('Cyclomatic Complexity'),
                    self.tr('Cyclomatic &Complexity...'), 0, 0,
                    self, '')
                act.setStatusTip(self.tr(
                    'Show the cyclomatic complexity for Python files.'))
                act.setWhatsThis(self.tr(
                    """<b>Cyclomatic Complexity...</b>"""
                    """<p>This calculates the cyclomatic complexity of"""
                    """ Python files and shows it together with a ranking."""
                    """</p>"""
                ))
                act.triggered.connect(
                    self.__projectBrowserCyclomaticComplexity)
                menu.addAction(act)
                self.__projectBrowserMetricsActs.append(act)
                
                act = menu.addSeparator()
                self.__projectBrowserSeparatorActs.append(act)
    
    def __editorOpened(self, editor):
        """
        Private slot called, when a new editor was opened.
        
        @param editor reference to the new editor
        @type QScintilla.Editor
        """
        menu = editor.getMenu("Show")
        if menu is not None:
            menu.addAction(self.__editorSeparatorActs[0])
            menu.addActions(self.__editorMetricsActs)
            menu.addAction(self.__editorSeparatorActs[1])
            editor.showMenu.connect(self.__editorShowMenu)
            self.__editors.append(editor)
    
    def __editorClosed(self, editor):
        """
        Private slot called, when an editor was closed.
        
        @param editor reference to the editor (QScintilla.Editor)
        """
        try:
            self.__editors.remove(editor)
        except ValueError:
            pass
    
    def __editorShowMenu(self, menuName, menu, editor):
        """
        Private slot called, when the the editor context menu or a submenu is
        about to be shown.
        
        @param menuName name of the menu to be shown (string)
        @param menu reference to the menu (QMenu)
        @param editor reference to the editor
        """
        if menuName == "Show":
            enable = editor.isPyFile()
            for act in self.__editorMetricsActs:
                act.setEnabled(enable)
    
    ##################################################################
    ## Raw code metrics calculations
    ##################################################################
    
    def __projectRawMetrics(self):
        """
        Private slot used to calculate raw code metrics for the project.
        """
        project = e5App().getObject("Project")
        project.saveAllScripts()
        ppath = project.getProjectPath()
        files = [os.path.join(ppath, file)
                 for file in project.getSources()
                 if file.endswith(
                     tuple(Preferences.getPython("Python3Extensions")) +
                     tuple(Preferences.getPython("PythonExtensions")))]
        
        if self.__projectRawMetricsDialog is None:
            from RadonMetrics.RawMetricsDialog import RawMetricsDialog
            self.__projectRawMetricsDialog = RawMetricsDialog(self)
        self.__projectRawMetricsDialog.show()
        self.__projectRawMetricsDialog.prepare(files, project)
    
    def __projectBrowserRawMetrics(self):
        """
        Private method to handle the code metrics context menu action of the
        project sources browser.
        """
        browser = e5App().getObject("ProjectBrowser").getProjectBrowser(
            "sources")
        if browser.getSelectedItemsCount([ProjectBrowserFileItem]) > 1:
            fn = []
            for itm in browser.getSelectedItems([ProjectBrowserFileItem]):
                fn.append(itm.fileName())
        else:
            itm = browser.model().item(browser.currentIndex())
            try:
                fn = itm.fileName()
            except AttributeError:
                fn = itm.dirName()
        
        if self.__projectBrowserRawMetricsDialog is None:
            from RadonMetrics.RawMetricsDialog import RawMetricsDialog
            self.__projectBrowserRawMetricsDialog = RawMetricsDialog(self)
        self.__projectBrowserRawMetricsDialog.show()
        self.__projectBrowserRawMetricsDialog.start(fn)
    
    def __editorRawMetrics(self):
        """
        Private slot to handle the raw code metrics action of the editor show
        menu.
        """
        editor = e5App().getObject("ViewManager").activeWindow()
        if editor is not None:
            if editor.checkDirty() and editor.getFileName() is not None:
                if self.__editorRawMetricsDialog is None:
                    from RadonMetrics.RawMetricsDialog import RawMetricsDialog
                    self.__editorRawMetricsDialog = RawMetricsDialog(self)
                self.__editorRawMetricsDialog.show()
                self.__editorRawMetricsDialog.start(editor.getFileName())
    
    ##################################################################
    ## Maintainability index calculations
    ##################################################################
    
    def __projectMaintainabilityIndex(self):
        """
        Private slot used to calculate the maintainability indexes for the
        project.
        """
        project = e5App().getObject("Project")
        project.saveAllScripts()
        ppath = project.getProjectPath()
        files = [os.path.join(ppath, file)
                 for file in project.getSources()
                 if file.endswith(
                     tuple(Preferences.getPython("Python3Extensions")) +
                     tuple(Preferences.getPython("PythonExtensions")))]
        
        if self.__projectMIDialog is None:
            from RadonMetrics.MaintainabilityIndexDialog import \
                MaintainabilityIndexDialog
            self.__projectMIDialog = MaintainabilityIndexDialog(self)
        self.__projectMIDialog.show()
        self.__projectMIDialog.prepare(files, project)
    
    def __projectBrowserMaintainabilityIndex(self):
        """
        Private method to handle the maintainability index context menu action
        of the project sources browser.
        """
        browser = e5App().getObject("ProjectBrowser").getProjectBrowser(
            "sources")
        if browser.getSelectedItemsCount([ProjectBrowserFileItem]) > 1:
            fn = []
            for itm in browser.getSelectedItems([ProjectBrowserFileItem]):
                fn.append(itm.fileName())
        else:
            itm = browser.model().item(browser.currentIndex())
            try:
                fn = itm.fileName()
            except AttributeError:
                fn = itm.dirName()
        
        if self.__projectBrowserMIDialog is None:
            from RadonMetrics.MaintainabilityIndexDialog import \
                MaintainabilityIndexDialog
            self.__projectBrowserMIDialog = MaintainabilityIndexDialog(self)
        self.__projectBrowserMIDialog.show()
        self.__projectBrowserMIDialog.start(fn)
    
    def __editorMaintainabilityIndex(self):
        """
        Private slot to handle the maintainability index action of the editor
        show menu.
        """
        editor = e5App().getObject("ViewManager").activeWindow()
        if editor is not None:
            if editor.checkDirty() and editor.getFileName() is not None:
                if self.__editorMIDialog is None:
                    from RadonMetrics.MaintainabilityIndexDialog import \
                        MaintainabilityIndexDialog
                    self.__editorMIDialog = MaintainabilityIndexDialog(self)
                self.__editorMIDialog.show()
                self.__editorMIDialog.start(editor.getFileName())
    
    ##################################################################
    ## Cyclomatic complexity calculations
    ##################################################################
    
    def __projectCyclomaticComplexity(self):
        """
        Private slot used to calculate the cyclomatic complexity for the
        project.
        """
        project = e5App().getObject("Project")
        project.saveAllScripts()
        ppath = project.getProjectPath()
        files = [os.path.join(ppath, file)
                 for file in project.getSources()
                 if file.endswith(
                     tuple(Preferences.getPython("Python3Extensions")) +
                     tuple(Preferences.getPython("PythonExtensions")))]
        
        if self.__projectCCDialog is None:
            from RadonMetrics.CyclomaticComplexityDialog import \
                CyclomaticComplexityDialog
            self.__projectCCDialog = CyclomaticComplexityDialog(self)
        self.__projectCCDialog.show()
        self.__projectCCDialog.prepare(files, project)
    
    def __projectBrowserCyclomaticComplexity(self):
        """
        Private method to handle the cyclomatic complexity context menu action
        of the project sources browser.
        """
        browser = e5App().getObject("ProjectBrowser").getProjectBrowser(
            "sources")
        if browser.getSelectedItemsCount([ProjectBrowserFileItem]) > 1:
            fn = []
            for itm in browser.getSelectedItems([ProjectBrowserFileItem]):
                fn.append(itm.fileName())
        else:
            itm = browser.model().item(browser.currentIndex())
            try:
                fn = itm.fileName()
            except AttributeError:
                fn = itm.dirName()
        
        if self.__projectBrowserCCDialog is None:
            from RadonMetrics.CyclomaticComplexityDialog import \
                CyclomaticComplexityDialog
            self.__projectBrowserCCDialog = CyclomaticComplexityDialog(self)
        self.__projectBrowserCCDialog.show()
        self.__projectBrowserCCDialog.start(fn)
    
    def __editorCyclomaticComplexity(self):
        """
        Private slot to handle the cyclomatic complexity action of the editor
        show menu.
        """
        editor = e5App().getObject("ViewManager").activeWindow()
        if editor is not None:
            if editor.checkDirty() and editor.getFileName() is not None:
                if self.__editorCCDialog is None:
                    from RadonMetrics.CyclomaticComplexityDialog import \
                        CyclomaticComplexityDialog
                    self.__editorCCDialog = CyclomaticComplexityDialog(self)
                self.__editorCCDialog.show()
                self.__editorCCDialog.start(editor.getFileName())
    
    ##################################################################
    ## Radon info display
    ##################################################################
    
    def __showRadonVersion(self):
        """
        Private slot to show the version number of the used radon library.
        """
        from RadonMetrics.radon import __version__
        E5MessageBox.information(
            None,
            self.tr("Radon"),
            self.tr(
                """<p><b>Radon Version {0}</b></p>"""
                """<p>Radon is a Python tool that computes various metrics"""
                """ from the source code. Radon can compute:"""
                """<ul>"""
                """<li><b>Raw</b> metrics (these include SLOC, comment"""
                """ lines, blank lines, multi line strings, ...)</li>"""
                """<li><b>Maintainability Index</b> (the one used in Visual"""
                """ Studio)</li>"""
                """<li><b>McCabe's complexity</b>, i.e. cyclomatic"""
                """ complexity</li>"""
                """</ul></p>"""
            ).format(__version__))
    
    ##################################################################
    ## Project handling methods
    ##################################################################
    
    def __projectClosed(self):
        """
        Private slot to handle closing a project.
        """
        self.__projectCCDialog and self.__projectCCDialog.clear()
        self.__projectMIDialog and self.__projectMIDialog.clear()
        self.__projectRawMetricsDialog and \
            self.__projectRawMetricsDialog.clear()

#
# eflag: noqa = M801

eric ide

mercurial