Mon, 14 Sep 2015 20:18:39 +0200
Started implementing the raw code metrics stuff.
--- a/PluginMetricsRadon.e4p Sun Sep 13 17:56:57 2015 +0200 +++ b/PluginMetricsRadon.e4p Mon Sep 14 20:18:39 2015 +0200 @@ -15,6 +15,8 @@ <Eol index="1"/> <Sources> <Source>PluginMetricsRadon.py</Source> + <Source>RadonMetrics/CodeMetricsCalculator.py</Source> + <Source>RadonMetrics/RawMetricsDialog.py</Source> <Source>RadonMetrics/__init__.py</Source> <Source>RadonMetrics/radon/__init__.py</Source> <Source>RadonMetrics/radon/complexity.py</Source> @@ -23,7 +25,9 @@ <Source>RadonMetrics/radon/visitors.py</Source> <Source>__init__.py</Source> </Sources> - <Forms/> + <Forms> + <Form>RadonMetrics/RawMetricsDialog.ui</Form> + </Forms> <Translations/> <Resources/> <Interfaces/>
--- a/PluginMetricsRadon.py Sun Sep 13 17:56:57 2015 +0200 +++ b/PluginMetricsRadon.py Mon Sep 14 20:18:39 2015 +0200 @@ -52,7 +52,7 @@ @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 + metricsDone = pyqtSignal(str, list) metricsError = pyqtSignal(str, str) batchFinished = pyqtSignal() @@ -69,8 +69,7 @@ self.backgroundService = e5App().getObject("BackgroundService") - path = os.path.join( - os.path.dirname(__file__), packageName, 'Tabnanny') + path = os.path.join(os.path.dirname(__file__), packageName) self.backgroundService.serviceConnect( 'radon', 'Python2', path, 'CodeMetricsCalculator', lambda *args: self.metricsDone.emit(*args), @@ -223,7 +222,7 @@ data[lang]) self.batchesFinished = False - def cancelIndentBatchCheck(self): + def cancelRawMetricsBatch(self): """ Public method to cancel all batch jobs. """ @@ -424,7 +423,6 @@ tuple(Preferences.getPython("Python3Extensions")) + tuple(Preferences.getPython("PythonExtensions")))] - # TODO: implement this dialog from RadonMetrics.RawMetricsDialog import RawMetricsDialog self.__projectRawMetricsDialog = RawMetricsDialog(self) self.__projectRawMetricsDialog.show()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RadonMetrics/CodeMetricsCalculator.py Mon Sep 14 20:18:39 2015 +0200 @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 Detlev Offenbach <detlev@die-offenbachs.de> +# + +from __future__ import unicode_literals + +import multiprocessing + + +def initService(): + """ + Initialize the service and return the entry point. + + @return the entry point for the background client (function) + """ + return codeMetrics + + +def initBatchService(): + """ + Initialize the batch service and return the entry point. + + @return the entry point for the background client (function) + """ + return batchCodeMetrics + + +def codeMetrics(file, text="", type_=""): + """ + Private function to calculate selected code metrics of one file. + + @param file source filename + @type str + @param text source text + @param str + @return tuple containing the filename and the result list + @rtype (str, list) + """ + if type_ == "raw": + return __rawCodeMetrics(file, text) + + # TODO: Return error indication + + +def batchCodeMetrics(argumentsList, send, fx, cancelled): + """ + Module function to calculate selected code metrics for a batch of files. + + @param argumentsList list of arguments tuples as given for check + @type list + @param send reference to send function + @type function + @param fx registered service name + @type str + @param cancelled reference to function checking for a cancellation + @type function + """ + try: + NumberOfProcesses = multiprocessing.cpu_count() + if NumberOfProcesses >= 1: + NumberOfProcesses -= 1 + except NotImplementedError: + NumberOfProcesses = 1 + + # Create queues + taskQueue = multiprocessing.Queue() + doneQueue = multiprocessing.Queue() + + # Submit tasks (initially two time number of processes + initialTasks = 2 * NumberOfProcesses + for task in argumentsList[:initialTasks]: + taskQueue.put(task) + + # Start worker processes + for i in range(NumberOfProcesses): + multiprocessing.Process(target=worker, args=(taskQueue, doneQueue))\ + .start() + + # Get and send results + endIndex = len(argumentsList) - initialTasks + for i in range(len(argumentsList)): + filename, result = doneQueue.get() + send(fx, filename, result) + if cancelled(): + # just exit the loop ignoring the results of queued tasks + break + if i < endIndex: + taskQueue.put(argumentsList[i + initialTasks]) + + # Tell child processes to stop + for i in range(NumberOfProcesses): + taskQueue.put('STOP') + + +def worker(input, output): + """ + Module function acting as the parallel worker for the style check. + + @param input input queue + @type multiprocessing.Queue + @param output output queue + @type multiprocessing.Queue + """ + for filename, source, type_ in iter(input.get, 'STOP'): + if type_ == "raw": + result = __rawCodeMetrics(filename, source) + else: + result = [] + output.put((filename, result)) + + +def __rawCodeMetrics(file, text=""): + """ + Private function to calculate the raw code metrics for one Python file. + + @param file source filename + @type str + @param text source text + @type str + @return tuple containing the result list + @rtype (list) + """ + from radon.raw import analyze + res = analyze(text) + return (res, )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RadonMetrics/RawMetricsDialog.py Mon Sep 14 20:18:39 2015 +0200 @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show raw code metrics. +""" + +from __future__ import unicode_literals + +import os +import fnmatch + +from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer +from PyQt5.QtWidgets import ( + QDialog, QDialogButtonBox, QAbstractButton, QMenu, QHeaderView, + QTreeWidgetItem, QApplication +) + +from .Ui_RawMetricsDialog import Ui_RawMetricsDialog + +import Preferences +import Utilities + + +class RawMetricsDialog(QDialog, Ui_RawMetricsDialog): + """ + Class implementing a dialog to show raw code metrics. + """ + def __init__(self, radonService, parent=None): + """ + Constructor + + @param radonService reference to the service + @type RadonMetricsPlugin + @param parent reference to the parent widget + @type QWidget + """ + super(RawMetricsDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + + self.resultList.headerItem().setText(self.resultList.columnCount(), "") + + self.radonService = radonService + self.radonService.metricsDone.connect(self.__processResult) + self.radonService.metricsError.connect(self.__processError) + self.radonService.batchFinished.connect(self.__batchFinished) + + self.cancelled = False + + self.__menu = QMenu(self) + self.__menu.addAction(self.tr("Collapse all"), + self.__resultCollapse) + self.__menu.addAction(self.tr("Expand all"), self.__resultExpand) + self.resultList.setContextMenuPolicy(Qt.CustomContextMenu) + self.resultList.customContextMenuRequested.connect( + self.__showContextMenu) + + self.__fileList = [] + self.__project = None + self.filterFrame.setVisible(False) + + def __resizeResultColumns(self): + """ + Private method to resize the list columns. + """ + self.resultList.header().resizeSections(QHeaderView.ResizeToContents) + self.resultList.header().setStretchLastSection(True) + + def __createResultItem(self, filename, values): + """ + Private slot to create a new item in the result list. + + @param parent parent of the new item + @type QTreeWidget or QTreeWidgetItem + @param values values to be displayed + @type list + @return the generated item + @rtype QTreeWidgetItem + """ + data = [filename] + for value in values: + try: + data.append("{0:5}".format(int(value))) + except ValueError: + data.append(value) + itm = QTreeWidgetItem(self.resultList, data) + for col in range(1, 6): + itm.setTextAlignment(col, Qt.Alignment(Qt.AlignRight)) + return itm + + def prepare(self, fileList, project): + """ + Public method to prepare the dialog with a list of filenames. + + @param fileList list of filenames + @type list of str + @param project reference to the project object + @type Project + """ + self.__fileList = fileList[:] + self.__project = project + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + + self.filterFrame.setVisible(True) + + self.__data = self.__project.getData( + "OTHERTOOLSPARMS", "RadonCodeMetrics") + if self.__data is None or "ExcludeFiles" not in self.__data: + self.__data = {"ExcludeFiles": ""} + self.excludeFilesEdit.setText(self.__data["ExcludeFiles"]) + + def start(self, fn): + """ + Public slot to start the code metrics determination. + + @param fn file or list of files or directory to show + the code metrics for (string or list of strings) + """ + self.cancelled = False + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + QApplication.processEvents() + + if isinstance(fn, list): + self.files = fn + elif os.path.isdir(fn): + self.files = [] + extensions = set(Preferences.getPython("PythonExtensions") + + Preferences.getPython("Python3Extensions")) + for ext in extensions: + self.files.extend( + Utilities.direntries(fn, True, '*{0}'.format(ext), 0)) + else: + self.files = [fn] + self.files.sort() + # check for missing files + for f in self.files[:]: + if not os.path.exists(f): + self.files.remove(f) + + if len(self.files) > 0: + self.checkProgress.setMaximum(len(self.files)) + self.checkProgress.setVisible(len(self.files) > 1) + self.checkProgressLabel.setVisible(len(self.files) > 1) + QApplication.processEvents() + + # now go through all the files + self.progress = 0 + self.files.sort() + if len(self.files) == 1: + self.__batch = False + self.rawMetrics() + else: + self.__batch = True + self.rawMetricsBatch() + + def rawMetrics(self, codestring=''): + """ + Public method to start a code metrics calculation for one Python file. + + The results are reported to the __processResult slot. + + @keyparam codestring optional sourcestring + @type str + """ + if not self.files: + self.checkProgressLabel.setPath("") + self.checkProgress.setMaximum(1) + self.checkProgress.setValue(1) + self.__finish() + return + + self.filename = self.files.pop(0) + self.checkProgress.setValue(self.progress) + self.checkProgressLabel.setPath(self.filename) + QApplication.processEvents() + + if self.cancelled: + return + + try: + self.source = Utilities.readEncodedFile(self.filename)[0] + self.source = Utilities.normalizeCode(self.source) + except (UnicodeError, IOError) as msg: + # TODO: adjust this + self.__createResultItem( + self.filename, 1, + "Error: {0}".format(str(msg)).rstrip()) + self.progress += 1 + # Continue with next file + self.rawMetrics() + return + + self.__finished = False + self.radonService.rawMetrics( + None, self.filename, self.source) + + def rawMetricsBatch(self): + """ + Public method to start a code metrics calculation batch job. + + The results are reported to the __processResult slot. + """ + self.__lastFileItem = None + + self.checkProgressLabel.setPath(self.tr("Preparing files...")) + progress = 0 + + argumentsList = [] + for filename in self.files: + progress += 1 + self.checkProgress.setValue(progress) + QApplication.processEvents() + + try: + source = Utilities.readEncodedFile(filename)[0] + source = Utilities.normalizeCode(source) + except (UnicodeError, IOError) as msg: + # TODO: adjust this + self.__createResultItem( + filename, 1, + "Error: {0}".format(str(msg)).rstrip()) + continue + + argumentsList.append((filename, source)) + + # reset the progress bar to the checked files + self.checkProgress.setValue(self.progress) + QApplication.processEvents() + + self.__finished = False + self.radonService.rawMetricsBatch(argumentsList) + + def __batchFinished(self): + """ + Private slot handling the completion of a batch job. + """ + self.checkProgressLabel.setPath("") + self.checkProgress.setMaximum(1) + self.checkProgress.setValue(1) + self.__finish() + + def __processError(self, fn, msg): + # TODO: implement this + print("Error", fn, msg) + + def __processResult(self, fn, result): + """ + Private slot called after perfoming a code metrics calculation on one + file. + + @param fn filename of the file + @type str + @param result result list + @type list + """ + if self.__finished: + return + + # Check if it's the requested file, otherwise ignore signal if not + # in batch mode + if not self.__batch and fn != self.filename: + return + + self.__createResultItem(fn, result) + + self.progress += 1 + + self.checkProgress.setValue(self.progress) + self.checkProgressLabel.setPath(fn) + QApplication.processEvents() + + if not self.__batch: + self.rawMetrics() + + def __finish(self): + """ + Private slot called when the action or the user pressed the button. + """ + if not self.__finished: + self.__finished = True + + self.cancelled = True + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + + self.resultList.header().resizeSections( + QHeaderView.ResizeToContents) + self.resultList.header().setStretchLastSection(True) + if qVersion() >= "5.0.0": + self.resultList.header().setSectionResizeMode( + QHeaderView.Interactive) + else: + self.resultList.header().setResizeMode(QHeaderView.Interactive) + + self.checkProgress.setVisible(False) + self.checkProgressLabel.setVisible(False) + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot called by a button of the button box clicked. + + @param button button that was clicked + @type QAbstractButton + """ + if button == self.buttonBox.button(QDialogButtonBox.Close): + self.close() + elif button == self.buttonBox.button(QDialogButtonBox.Cancel): + if self.__batch: + self.radonService.cancelRawMetricsBatch() + QTimer.singleShot(1000, self.__finish) + else: + self.__finish() + + @pyqtSlot() + def on_startButton_clicked(self): + """ + Private slot to start a code metrics run. + """ + fileList = self.__fileList[:] + + filterString = self.excludeFilesEdit.text() + if "ExcludeFiles" not in self.__data or \ + filterString != self.__data["ExcludeFiles"]: + self.__data["ExcludeFiles"] = filterString + self.__project.setData( + "OTHERTOOLSPARMS", "RadonCodeMetrics", self.__data) + filterList = [f.strip() for f in filterString.split(",") + if f.strip()] + if filterList: + for filter in filterList: + fileList = \ + [f for f in fileList if not fnmatch.fnmatch(f, filter)] + + self.resultList.clear() + self.cancelled = False + self.start(fileList) + + def __showContextMenu(self, coord): + """ + Private slot to show the context menu of the result list. + + @param coord position of the mouse pointer + @type QPoint + """ + if self.resultList.topLevelItemCount() > 0: + self.__menu.popup(self.mapToGlobal(coord)) + + def __resultCollapse(self): + """ + Private slot to collapse all entries of the result list. + """ + for index in range(self.resultList.topLevelItemCount()): + self.resultList.topLevelItem(index).setExpanded(False) + + def __resultExpand(self): + """ + Private slot to expand all entries of the result list. + """ + for index in range(self.resultList.topLevelItemCount()): + self.resultList.topLevelItem(index).setExpanded(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RadonMetrics/RawMetricsDialog.ui Mon Sep 14 20:18:39 2015 +0200 @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>RawMetricsDialog</class> + <widget class="QDialog" name="RawMetricsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>832</width> + <height>587</height> + </rect> + </property> + <property name="windowTitle"> + <string>Code Metrics</string> + </property> + <property name="whatsThis"> + <string><b>Code Metrics</b> +<p>This dialog shows some code metrics.</p></string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QFrame" name="filterFrame"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Exclude Files:</string> + </property> + </widget> + </item> + <item> + <widget class="E5ClearableLineEdit" name="excludeFilesEdit"> + <property name="toolTip"> + <string>Enter filename patterns of files to be excluded separated by a comma</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="lineWidth"> + <number>2</number> + </property> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="startButton"> + <property name="toolTip"> + <string>Press to start the code metrics run</string> + </property> + <property name="text"> + <string>Start</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="resultList"> + <property name="whatsThis"> + <string><b>Code metrics</b> +<p>This list shows some code metrics.</p></string> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>LOC</string> + </property> + </column> + <column> + <property name="text"> + <string>LLOC</string> + </property> + </column> + <column> + <property name="text"> + <string>SLOC</string> + </property> + </column> + <column> + <property name="text"> + <string>Comments</string> + </property> + </column> + <column> + <property name="text"> + <string>Multi</string> + </property> + </column> + <column> + <property name="text"> + <string>Empty</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="E5SqueezeLabelPath" name="checkProgressLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="checkProgress"> + <property name="toolTip"> + <string>Shows the progress of the code metrics action</string> + </property> + <property name="value"> + <number>0</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="format"> + <string>%v/%m Files</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <layoutdefault spacing="6" margin="6"/> + <pixmapfunction>qPixmapFromMimeSource</pixmapfunction> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5SqueezeLabelPath</class> + <extends>QLabel</extends> + <header>E5Gui/E5SqueezeLabels.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>startButton</tabstop> + <tabstop>excludeFilesEdit</tabstop> + <tabstop>resultList</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>