Wed, 16 Sep 2015 19:36:25 +0200
Started implementing the maintainability index stuff.
# -*- 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, dict) 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, dict) 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) try: 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.hasBatch = True except TypeError: self.backgroundService.serviceConnect( 'radon', 'Python2', path, 'CodeMetricsCalculator', lambda *args: self.metricsDone.emit(*args), onErrorCallback=self.serviceErrorPy2) self.backgroundService.serviceConnect( 'radon', 'Python3', path, 'CodeMetricsCalculator', lambda *args: self.metricsDone.emit(*args), onErrorCallback=self.serviceErrorPy3) self.hasBatch = False 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.__projectMIAct = None self.__projectMIDialog = None self.__projectSeparatorActs = [] self.__projectBrowserMenu = None self.__projectBrowserRawMetricsAct = None self.__projectBrowserRawMetricsDialog = None self.__projectBrowserMIAct = None self.__projectBrowserMIDialog = None self.__projectBrowserSeparatorActs = [] self.__editors = [] self.__editorRawMetricsAct = None self.__editorRawMetricsDialog = None self.__editorMIAct = None self.__editorMIDialog = 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 cancelRawMetricsBatch(self): """ Public method to cancel all batch jobs. """ for lang in ['Python2', 'Python3']: self.backgroundService.requestCancel('batch_radon', 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', lang, filename, [source, 'mi']) 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, 'mi')) 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 cancelMaintainabilityIndexBatch(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) self.__projectMIAct = E5Action( self.tr('Maintainability Index'), self.tr('Maintainability &Index...'), 0, 0, self, 'project_show_radon_mi') self.__projectMIAct.setStatusTip( self.tr('Show the maintainability index for Python files.')) self.__projectMIAct.setWhatsThis(self.tr( """<b>Maintainability Index...</b>""" """<p>This calculates the maintainability index of Python""" """ files and shows it together with a ranking.</p>""" )) self.__projectMIAct.triggered.connect( self.__projectMaintainabilityIndex) menu.addAction(self.__projectMIAct) act = menu.addSeparator() self.__projectSeparatorActs.append(act) e5App().getObject("Project").addE5Actions([ self.__projectRawMetricsAct, self.__projectMIAct ]) 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) self.__editorMIAct = E5Action( self.tr('Maintainability Index'), self.tr('Maintainability &Index...'), 0, 0, self, "") self.__editorMIAct.setStatusTip( self.tr('Show the maintainability index for Python files.')) self.__projectMIAct.setWhatsThis(self.tr( """<b>Maintainability Index...</b>""" """<p>This calculates the maintainability index of Python""" """ files and shows it together with a ranking.</p>""" )) self.__editorMIAct.triggered.connect( self.__editorMaintainabilityIndex) 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) menu.removeAction(self.__projectMIAct) e5App().getObject("Project").removeE5Actions([ self.__projectRawMetricsAct, self.__projectMIAct ]) if self.__projectBrowserMenu: for sep in self.__projectBrowserSeparatorActs: self.__projectBrowserMenu.removeAction(sep) if self.__projectBrowserRawMetricsAct: self.__projectBrowserMenu.removeAction( self.__projectBrowserRawMetricsAct) if self.__projectBrowserMIAct: self.__projectBrowserMenu.removeAction( self.__projectBrowserMIAct) 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) menu.removeAction(self.__editorMIAct) 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, self.__projectMIAct]: 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) self.__projectBrowserMIAct = E5Action( self.tr('Maintainability Index'), self.tr('Maintainability &Index...'), 0, 0, self, 'project_show_radon_mi') self.__projectBrowserMIAct.setStatusTip( self.tr('Show the maintainability index for Python' ' files.')) self.__projectBrowserMIAct.setWhatsThis(self.tr( """<b>Maintainability Index...</b>""" """<p>This calculates the maintainability index of""" """ Python files and shows it together with a ranking.""" """</p>""" )) self.__projectBrowserMIAct.triggered.connect( self.__projectBrowserMaintainabilityIndex) menu.addAction(self.__projectBrowserMIAct) 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.__editorMIAct) 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() self.__editorRawMetricsAct.setEnabled(enable) self.__editorMIAct.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.pdata["SOURCES"] if file.endswith( tuple(Preferences.getPython("Python3Extensions")) + tuple(Preferences.getPython("PythonExtensions")))] 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() 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: 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.pdata["SOURCES"] if file.endswith( tuple(Preferences.getPython("Python3Extensions")) + tuple(Preferences.getPython("PythonExtensions")))] 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() 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: from RadonMetrics.MaintainabilityIndexDialog import \ MaintainabilityIndexDialog self.__editorMIDialog = MaintainabilityIndexDialog(self) self.__editorMIDialog.show() self.__editorMIDialog.start(editor.getFileName())