PluginMetricsRadon.py

changeset 2
1ad320a50a01
parent 0
765bb3e711d6
child 3
7150ed890fd5
--- a/PluginMetricsRadon.py	Sun Sep 13 17:56:31 2015 +0200
+++ b/PluginMetricsRadon.py	Sun Sep 13 17:56:57 2015 +0200
@@ -0,0 +1,505 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015 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 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 = "0.1.0"
+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."""
+)
+needsRestart = False
+pyqtApi = 2
+python2Compatible = True
+# End-Of-Header
+
+error = ""
+    
+
+class RadonMetricsPlugin(QObject):
+    """
+    Class implementing the radon code metrics plug-in.
+    
+    @signal metricsDone(str, list) emitted when the code metrics were
+        determined for a file
+    @signal metricsError(str, str) emitted in case of an error
+    @signal batchFinished() emitted when a code metrics batch is done
+    """
+    metricsDone = pyqtSignal(str, list)  # TODO: adjust this
+    metricsError = pyqtSignal(str, str)
+    batchFinished = pyqtSignal()
+    
+    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, 'Tabnanny')
+        self.backgroundService.serviceConnect(
+            'radon', 'Python2', path, 'CodeMetricsCalculator',
+            lambda *args: self.metricsDone.emit(*args),
+            onErrorCallback=self.serviceErrorPy2,
+            onBatchDone=self.batchJobDone)
+        self.backgroundService.serviceConnect(
+            'radon', 'Python3', path, 'CodeMetricsCalculator',
+            lambda *args: self.metricsDone.emit(*args),
+            onErrorCallback=self.serviceErrorPy3,
+            onBatchDone=self.batchJobDone)
+        
+        self.queuedBatches = []
+        self.batchesFinished = True
+        
+        self.__translator = None
+        self.__loadTranslator()
+    
+    def __serviceError(self, fn, msg):
+        """
+        Private slot handling service errors.
+        
+        @param fn file name
+        @type str
+        @param msg message text
+        @type str
+        """
+        self.metricsError.emit(fn, msg)
+    
+    def serviceErrorPy2(self, fx, lang, fn, msg):
+        """
+        Public slot handling service errors for Python 2.
+        
+        @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', 'batch_radon'] and lang == 'Python2':
+            if fx == 'radon':
+                self.__serviceError(fn, msg)
+            else:
+                self.__serviceError(self.tr("Python 2 batch job"), msg)
+                self.batchJobDone(fx, lang)
+    
+    def serviceErrorPy3(self, fx, lang, fn, msg):
+        """
+        Public slot handling service errors for Python 3.
+        
+        @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', 'batch_radon'] and lang == 'Python3':
+            if fx == 'radon':
+                self.__serviceError(fn, msg)
+            else:
+                self.__serviceError(self.tr("Python 3 batch job"), msg)
+                self.batchJobDone(fx, lang)
+    
+    def batchJobDone(self, fx, lang):
+        """
+        Public slot handling the completion of a batch job.
+        
+        @param fx service name
+        @type str
+        @param lang language
+        @type str
+        """
+        if fx in ['radon', 'batch_radon']:
+            if lang in self.queuedBatches:
+                self.queuedBatches.remove(lang)
+            # prevent sending the signal multiple times
+            if len(self.queuedBatches) == 0 and not self.batchesFinished:
+                self.batchFinished.emit()
+                self.batchesFinished = True
+    
+    def __initialize(self):
+        """
+        Private slot to (re)initialize the plugin.
+        """
+        self.__projectRawMetricsAct = None
+        self.__projectRawMetricsDialog = None
+        self.__projectSeparatorActs = []
+        
+        self.__projectBrowserMenu = None
+        self.__projectBrowserRawMetricsAct = None
+        self.__projectBrowserRawMetricsDialog = None
+        self.__projectBrowserSeparatorActs = []
+        
+        self.__editors = []
+        self.__editorRawMetricsAct = None
+        self.__editorRawMetricsDialog = None
+        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', lang, filename, [source, 'raw'])
+
+    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, 'raw'))
+        
+        self.queuedBatches = []
+        for lang in ['Python2', 'Python3']:
+            if data[lang]:
+                self.queuedBatches.append(lang)
+                self.backgroundService.enqueueRequest('batch_radon', lang, "",
+                                                      data[lang])
+                self.batchesFinished = False
+    
+    def cancelIndentBatchCheck(self):
+        """
+        Public method to cancel all batch jobs.
+        """
+        for lang in ['Python2', 'Python3']:
+            self.backgroundService.requestCancel('batch_radon', 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
+        
+        menu = e5App().getObject("Project").getMenu("Show")
+        if menu:
+            if not menu.isEmpty():
+                act = menu.addSeparator()
+                act.setText(self.tr("Radon"))
+                self.__projectSeparatorActs.append(act)
+            self.__projectRawMetricsAct = E5Action(
+                self.tr('Code Metrics'),
+                self.tr('Code &Metrics...'), 0, 0,
+                self, 'project_show_radon_raw')
+            self.__projectRawMetricsAct.setStatusTip(
+                self.tr('Show raw code metrics.'))
+            self.__projectRawMetricsAct.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>"""
+            ))
+            self.__projectRawMetricsAct.triggered.connect(
+                self.__projectRawMetrics)
+            menu.addAction(self.__projectRawMetricsAct)
+            act = menu.addSeparator()
+            self.__projectSeparatorActs.append(act)
+            
+            e5App().getObject("Project").addE5Actions([
+                self.__projectRawMetricsAct,
+            ])
+        
+        act = QAction("Radon", self)
+        act.setSeparator(True)
+        self.__editorSeparatorActs.append(act)
+        act = QAction(self)
+        act.setSeparator(True)
+        self.__editorSeparatorActs.append(act)
+        
+        self.__editorRawMetricsAct = E5Action(
+            self.tr('Code Metrics'),
+            self.tr('Code &Metrics...'), 0, 0,
+            self, "")
+        self.__editorRawMetricsAct.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>"""
+        ))
+        self.__editorRawMetricsAct.triggered.connect(self.__editorRawMetrics)
+        
+        e5App().getObject("Project").showMenu.connect(self.__projectShowMenu)
+        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("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)
+            menu.removeAction(self.__projectRawMetricsAct)
+            e5App().getObject("Project").removeE5Actions(
+                [self.__projectRawMetricsAct])
+        
+        if self.__projectBrowserMenu:
+            for sep in self.__projectBrowserSeparatorActs:
+                self.__projectBrowserMenu.removeAction(sep)
+            if self.__projectBrowserRawMetricsAct:
+                self.__projectBrowserMenu.removeAction(
+                    self.__projectBrowserRawMetricsAct)
+        
+        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)
+                menu.removeAction(self.__editorRawMetricsAct)
+        
+        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.__projectRawMetricsAct]:
+                if act is not None:
+                    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()
+                act.setText(self.tr("Radon"))
+                self.__projectBrowserSeparatorActs.append(act)
+                
+                self.__projectBrowserRawMetricsAct = E5Action(
+                    self.tr('Code Metrics'),
+                    self.tr('Code &Metrics...'), 0, 0,
+                        self, '')
+                self.__projectBrowserRawMetricsAct.setStatusTip(
+                    self.tr('Show raw code metrics.'))
+                self.__projectBrowserRawMetricsAct.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>"""
+                ))
+                self.__projectBrowserRawMetricsAct.triggered.connect(
+                    self.__projectBrowserRawMetrics)
+                menu.addAction(self.__projectBrowserRawMetricsAct)
+    
+    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.pdata["SOURCES"]
+                 if file.endswith(
+                     tuple(Preferences.getPython("Python3Extensions")) +
+                     tuple(Preferences.getPython("PythonExtensions")))]
+        
+        # TODO: implement this dialog
+        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 tabnanny 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()
+        
+        from RadonMetrics.RawMetricsDialog import RawMetricsDialog
+        self.__projectBrowserRawMetricsDialog = RawMetricsDialog(self)
+        self.__projectBrowserRawMetricsDialog.show()
+        self.__projectBrowserRawMetricsDialog.start(fn)
+    
+    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.addAction(self.__editorRawMetricsAct)
+            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":
+            self.__editorRawMetricsAct.setEnabled(editor.isPyFile())
+    
+    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:
+                from RadonMetrics.RawMetricsDialog import RawMetricsDialog
+                self.__editorRawMetricsDialog = RawMetricsDialog(self)
+                self.__editorRawMetricsDialog.show()
+                self.__editorRawMetricsDialog.start(editor.getFileName())

eric ide

mercurial