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