Fri, 04 Oct 2013 14:26:08 +0200
Continued changing the names of the various code style checkers to make them more appropriate to the broadened scope.
--- a/Documentation/Source/eric5.Plugins.PluginPep8Checker.html Wed Oct 02 18:58:13 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,216 +0,0 @@ -<!DOCTYPE html> -<html><head> -<title>eric5.Plugins.PluginPep8Checker</title> -<meta charset="UTF-8"> -<style> -body { - background: #EDECE6; - margin: 0em 1em 10em 1em; - color: black; -} - -h1 { color: white; background: #85774A; } -h2 { color: white; background: #85774A; } -h3 { color: white; background: #9D936E; } -h4 { color: white; background: #9D936E; } - -a { color: #BA6D36; } - -</style> -</head> -<body><a NAME="top" ID="top"></a> -<h1>eric5.Plugins.PluginPep8Checker</h1> -<p> -Module implementing the PEP 8 Checker plugin. -</p> -<h3>Global Attributes</h3> -<table> -<tr><td>author</td></tr><tr><td>autoactivate</td></tr><tr><td>className</td></tr><tr><td>deactivateable</td></tr><tr><td>error</td></tr><tr><td>longDescription</td></tr><tr><td>name</td></tr><tr><td>packageName</td></tr><tr><td>pyqtApi</td></tr><tr><td>shortDescription</td></tr><tr><td>version</td></tr> -</table> -<h3>Classes</h3> -<table> -<tr> -<td><a href="#Pep8CheckerPlugin">Pep8CheckerPlugin</a></td> -<td>Class implementing the PEP 8 Checker plugin.</td> -</tr> -</table> -<h3>Functions</h3> -<table> -<tr><td>None</td></tr> -</table> -<hr /><hr /> -<a NAME="Pep8CheckerPlugin" ID="Pep8CheckerPlugin"></a> -<h2>Pep8CheckerPlugin</h2> -<p> - Class implementing the PEP 8 Checker plugin. -</p> -<h3>Derived from</h3> -QObject -<h3>Class Attributes</h3> -<table> -<tr><td>None</td></tr> -</table> -<h3>Class Methods</h3> -<table> -<tr><td>None</td></tr> -</table> -<h3>Methods</h3> -<table> -<tr> -<td><a href="#Pep8CheckerPlugin.__init__">Pep8CheckerPlugin</a></td> -<td>Constructor</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__editorClosed">__editorClosed</a></td> -<td>Private slot called, when an editor was closed.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__editorOpened">__editorOpened</a></td> -<td>Private slot called, when a new editor was opened.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__editorPep8Check">__editorPep8Check</a></td> -<td>Private slot to handle the PEP 8 check context menu action of the editors.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__editorShowMenu">__editorShowMenu</a></td> -<td>Private slot called, when the the editor context menu or a submenu is about to be shown.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__initialize">__initialize</a></td> -<td>Private slot to (re)initialize the plugin.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__projectBrowserPep8Check">__projectBrowserPep8Check</a></td> -<td>Private method to handle the PEP 8 check context menu action of the project sources browser.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__projectBrowserShowMenu">__projectBrowserShowMenu</a></td> -<td>Private slot called, when the the project browser menu or a submenu is about to be shown.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__projectPep8Check">__projectPep8Check</a></td> -<td>Public slot used to check the project files for PEP 8 compliance.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.__projectShowMenu">__projectShowMenu</a></td> -<td>Private slot called, when the the project menu or a submenu is about to be shown.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.activate">activate</a></td> -<td>Public method to activate this plugin.</td> -</tr><tr> -<td><a href="#Pep8CheckerPlugin.deactivate">deactivate</a></td> -<td>Public method to deactivate this plugin.</td> -</tr> -</table> -<h3>Static Methods</h3> -<table> -<tr><td>None</td></tr> -</table> -<a NAME="Pep8CheckerPlugin.__init__" ID="Pep8CheckerPlugin.__init__"></a> -<h4>Pep8CheckerPlugin (Constructor)</h4> -<b>Pep8CheckerPlugin</b>(<i>ui</i>) -<p> - Constructor -</p><dl> -<dt><i>ui</i></dt> -<dd> -reference to the user interface object (UI.UserInterface) -</dd> -</dl><a NAME="Pep8CheckerPlugin.__editorClosed" ID="Pep8CheckerPlugin.__editorClosed"></a> -<h4>Pep8CheckerPlugin.__editorClosed</h4> -<b>__editorClosed</b>(<i>editor</i>) -<p> - Private slot called, when an editor was closed. -</p><dl> -<dt><i>editor</i></dt> -<dd> -reference to the editor (QScintilla.Editor) -</dd> -</dl><a NAME="Pep8CheckerPlugin.__editorOpened" ID="Pep8CheckerPlugin.__editorOpened"></a> -<h4>Pep8CheckerPlugin.__editorOpened</h4> -<b>__editorOpened</b>(<i>editor</i>) -<p> - Private slot called, when a new editor was opened. -</p><dl> -<dt><i>editor</i></dt> -<dd> -reference to the new editor (QScintilla.Editor) -</dd> -</dl><a NAME="Pep8CheckerPlugin.__editorPep8Check" ID="Pep8CheckerPlugin.__editorPep8Check"></a> -<h4>Pep8CheckerPlugin.__editorPep8Check</h4> -<b>__editorPep8Check</b>(<i></i>) -<p> - Private slot to handle the PEP 8 check context menu action - of the editors. -</p><a NAME="Pep8CheckerPlugin.__editorShowMenu" ID="Pep8CheckerPlugin.__editorShowMenu"></a> -<h4>Pep8CheckerPlugin.__editorShowMenu</h4> -<b>__editorShowMenu</b>(<i>menuName, menu, editor</i>) -<p> - Private slot called, when the the editor context menu or a submenu is - about to be shown. -</p><dl> -<dt><i>menuName</i></dt> -<dd> -name of the menu to be shown (string) -</dd><dt><i>menu</i></dt> -<dd> -reference to the menu (QMenu) -</dd><dt><i>editor</i></dt> -<dd> -reference to the editor -</dd> -</dl><a NAME="Pep8CheckerPlugin.__initialize" ID="Pep8CheckerPlugin.__initialize"></a> -<h4>Pep8CheckerPlugin.__initialize</h4> -<b>__initialize</b>(<i></i>) -<p> - Private slot to (re)initialize the plugin. -</p><a NAME="Pep8CheckerPlugin.__projectBrowserPep8Check" ID="Pep8CheckerPlugin.__projectBrowserPep8Check"></a> -<h4>Pep8CheckerPlugin.__projectBrowserPep8Check</h4> -<b>__projectBrowserPep8Check</b>(<i></i>) -<p> - Private method to handle the PEP 8 check context menu action of - the project sources browser. -</p><a NAME="Pep8CheckerPlugin.__projectBrowserShowMenu" ID="Pep8CheckerPlugin.__projectBrowserShowMenu"></a> -<h4>Pep8CheckerPlugin.__projectBrowserShowMenu</h4> -<b>__projectBrowserShowMenu</b>(<i>menuName, menu</i>) -<p> - Private slot called, when the the project browser menu or a submenu is - about to be shown. -</p><dl> -<dt><i>menuName</i></dt> -<dd> -name of the menu to be shown (string) -</dd><dt><i>menu</i></dt> -<dd> -reference to the menu (QMenu) -</dd> -</dl><a NAME="Pep8CheckerPlugin.__projectPep8Check" ID="Pep8CheckerPlugin.__projectPep8Check"></a> -<h4>Pep8CheckerPlugin.__projectPep8Check</h4> -<b>__projectPep8Check</b>(<i></i>) -<p> - Public slot used to check the project files for PEP 8 compliance. -</p><a NAME="Pep8CheckerPlugin.__projectShowMenu" ID="Pep8CheckerPlugin.__projectShowMenu"></a> -<h4>Pep8CheckerPlugin.__projectShowMenu</h4> -<b>__projectShowMenu</b>(<i>menuName, menu</i>) -<p> - Private slot called, when the the project menu or a submenu is - about to be shown. -</p><dl> -<dt><i>menuName</i></dt> -<dd> -name of the menu to be shown (string) -</dd><dt><i>menu</i></dt> -<dd> -reference to the menu (QMenu) -</dd> -</dl><a NAME="Pep8CheckerPlugin.activate" ID="Pep8CheckerPlugin.activate"></a> -<h4>Pep8CheckerPlugin.activate</h4> -<b>activate</b>(<i></i>) -<p> - Public method to activate this plugin. -</p><dl> -<dt>Returns:</dt> -<dd> -tuple of None and activation status (boolean) -</dd> -</dl><a NAME="Pep8CheckerPlugin.deactivate" ID="Pep8CheckerPlugin.deactivate"></a> -<h4>Pep8CheckerPlugin.deactivate</h4> -<b>deactivate</b>(<i></i>) -<p> - Public method to deactivate this plugin. -</p> -<div align="right"><a href="#top">Up</a></div> -<hr /> -</body></html> \ No newline at end of file
--- a/Documentation/Source/index-eric5.Plugins.CheckerPlugins.Pep8.html Wed Oct 02 18:58:13 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -<!DOCTYPE html> -<html><head> -<title>eric5.Plugins.CheckerPlugins.Pep8</title> -<meta charset="UTF-8"> -<style> -body { - background: #EDECE6; - margin: 0em 1em 10em 1em; - color: black; -} - -h1 { color: white; background: #85774A; } -h2 { color: white; background: #85774A; } -h3 { color: white; background: #9D936E; } -h4 { color: white; background: #9D936E; } - -a { color: #BA6D36; } - -</style> -</head> -<body> -<h1>eric5.Plugins.CheckerPlugins.Pep8</h1> -<p> -Package containing the PEP 8 plugin. -</p> - - -<h3>Modules</h3> -<table> -<tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep257Checker.html">Pep257Checker</a></td> -<td>Module implementing a checker for PEP-257 documentation string conventions.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep8Checker.html">Pep8Checker</a></td> -<td>Module implementing the PEP 8 checker.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep8CodeSelectionDialog.html">Pep8CodeSelectionDialog</a></td> -<td>Module implementing a dialog to select PEP 8 message codes.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep8Dialog.html">Pep8Dialog</a></td> -<td>Module implementing a dialog to show the results of the PEP 8 check.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep8Fixer.html">Pep8Fixer</a></td> -<td>Module implementing a class to fix certain code style issues.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep8NamingChecker.html">Pep8NamingChecker</a></td> -<td>Module implementing a checker for PEP-8 naming conventions.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.Pep8StatisticsDialog.html">Pep8StatisticsDialog</a></td> -<td>Module implementing a dialog showing statistical data for the last code style checker run.</td> -</tr><tr> -<td><a href="eric5.Plugins.CheckerPlugins.Pep8.pep8.html">pep8</a></td> -<td></td> -</tr> -</table> -</body></html> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the code style checker. +""" + +import os + +from PyQt4.QtCore import QProcess, QCoreApplication + +from . import pep8 +from .NamingStyleChecker import NamingStyleChecker +from .DocStyleChecker import DocStyleChecker + +import Preferences +import Utilities + +from eric5config import getConfig + + +class CodeStyleCheckerPy2(object): + """ + Class implementing the code style checker interface for Python 2. + """ + def __init__(self, filename, lines, repeat=False, + select="", ignore="", max_line_length=79, + hang_closing=False, docType="pep257"): + """ + Constructor + + @param filename name of the file to check (string) + @param lines source of the file (list of strings) (ignored) + @keyparam repeat flag indicating to repeat message categories (boolean) + @keyparam select list of message IDs to check for + (comma separated string) + @keyparam ignore list of message IDs to ignore + (comma separated string) + @keyparam max_line_length maximum allowed line length (integer) + @keyparam hang_closing flag indicating to allow hanging closing + brackets (boolean) + @keyparam docType type of the documentation strings + (string, one of 'eric' or 'pep257') + """ + assert docType in ("eric", "pep257") + + self.errors = [] + self.counters = {} + + interpreter = Preferences.getDebugger("PythonInterpreter") + if interpreter == "" or not Utilities.isExecutable(interpreter): + self.errors.append((filename, 1, 1, + QCoreApplication.translate("CodeStyleCheckerPy2", + "Python2 interpreter not configured."))) + return + + checker = os.path.join(getConfig('ericDir'), + "UtilitiesPython2", "Pep8Checker.py") + + args = [checker] + if repeat: + args.append("-r") + if select: + args.append("-s") + args.append(select) + if ignore: + args.append("-i") + args.append(ignore) + args.append("-m") + args.append(str(max_line_length)) + if hang_closing: + args.append("-h") + args.append("-d") + args.append(docType) + args.append("-f") + args.append(filename) + + proc = QProcess() + proc.setProcessChannelMode(QProcess.MergedChannels) + proc.start(interpreter, args) + finished = proc.waitForFinished(15000) + if finished: + output = \ + str(proc.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace').splitlines() + if output[0] == "ERROR": + self.errors.append((filename, 1, 1, output[2])) + return + + if output[0] == "NO_PEP8": + return + + index = 0 + while index < len(output): + if output[index] == "PEP8_STATISTICS": + index += 1 + break + + fname = output[index + 1] + lineno = int(output[index + 2]) + position = int(output[index + 3]) + code = output[index + 4] + arglen = int(output[index + 5]) + args = [] + argindex = 0 + while argindex < arglen: + args.append(output[index + 6 + argindex]) + argindex += 1 + index += 6 + arglen + + if code in NamingStyleChecker.Codes: + text = NamingStyleChecker.getMessage(code, *args) + elif code in DocStyleChecker.Codes: + text = DocStyleChecker.getMessage(code, *args) + else: + text = pep8.getMessage(code, *args) + self.errors.append((fname, lineno, position, text)) + while index < len(output): + code, countStr = output[index].split(None, 1) + self.counters[code] = int(countStr) + index += 1 + else: + self.errors.append((filename, 1, 1, + QCoreApplication.translate("CodeStyleCheckerPy2", + "Python2 interpreter did not finish within 15s.")))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,906 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show the results of the code style check. +""" + +import os +import fnmatch + +from PyQt4.QtCore import pyqtSlot, Qt +from PyQt4.QtGui import QDialog, QTreeWidgetItem, QAbstractButton, \ + QDialogButtonBox, QApplication, QHeaderView, QIcon + +from E5Gui.E5Application import e5App + +from .Ui_CodeStyleCheckerDialog import Ui_CodeStyleCheckerDialog + +import UI.PixmapCache +import Preferences +import Utilities + +from . import pep8 +from .NamingStyleChecker import NamingStyleChecker + +# register the name checker +pep8.register_check(NamingStyleChecker, NamingStyleChecker.Codes) + +from .DocStyleChecker import DocStyleChecker + + +class CodeStyleCheckerReport(pep8.BaseReport): + """ + Class implementing a special report to be used with our dialog. + """ + def __init__(self, options): + """ + Constructor + + @param options options for the report (optparse.Values) + """ + super().__init__(options) + + self.__repeat = options.repeat + self.errors = [] + + def error_args(self, line_number, offset, code, check, *args): + """ + Public method to collect the error messages. + + @param line_number line number of the issue (integer) + @param offset position within line of the issue (integer) + @param code message code (string) + @param check reference to the checker function (function) + @param args arguments for the message (list) + @return error code (string) + """ + code = super().error_args(line_number, offset, code, check, *args) + if code and (self.counters[code] == 1 or self.__repeat): + if code in NamingStyleChecker.Codes: + text = NamingStyleChecker.getMessage(code, *args) + else: + text = pep8.getMessage(code, *args) + self.errors.append( + (self.filename, line_number, offset, text) + ) + return code + + +class CodeStyleCheckerDialog(QDialog, Ui_CodeStyleCheckerDialog): + """ + Class implementing a dialog to show the results of the code style check. + """ + filenameRole = Qt.UserRole + 1 + lineRole = Qt.UserRole + 2 + positionRole = Qt.UserRole + 3 + messageRole = Qt.UserRole + 4 + fixableRole = Qt.UserRole + 5 + codeRole = Qt.UserRole + 6 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super().__init__(parent) + self.setupUi(self) + + self.docTypeComboBox.addItem(self.trUtf8("PEP-257"), "pep257") + self.docTypeComboBox.addItem(self.trUtf8("Eric"), "eric") + + self.statisticsButton = self.buttonBox.addButton( + self.trUtf8("Statistics..."), QDialogButtonBox.ActionRole) + self.statisticsButton.setToolTip( + self.trUtf8("Press to show some statistics for the last run")) + self.statisticsButton.setEnabled(False) + self.showButton = self.buttonBox.addButton( + self.trUtf8("Show"), QDialogButtonBox.ActionRole) + self.showButton.setToolTip( + self.trUtf8("Press to show all files containing an issue")) + self.showButton.setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + + self.resultList.headerItem().setText(self.resultList.columnCount(), "") + self.resultList.header().setSortIndicator(0, Qt.AscendingOrder) + + self.checkProgress.setVisible(False) + self.checkProgressLabel.setVisible(False) + self.checkProgressLabel.setMaximumWidth(600) + + self.noResults = True + self.cancelled = False + self.__lastFileItem = None + + self.__fileOrFileList = "" + self.__project = None + self.__forProject = False + self.__data = {} + self.__statistics = {} + + self.on_loadDefaultButton_clicked() + + def __resort(self): + """ + Private method to resort the tree. + """ + self.resultList.sortItems(self.resultList.sortColumn(), + self.resultList.header().sortIndicatorOrder() + ) + + def __createResultItem(self, file, line, pos, message, fixed, autofixing): + """ + Private method to create an entry in the result list. + + @param file file name of the file (string) + @param line line number of issue (integer or string) + @param pos character position of issue (integer or string) + @param message message text (string) + @param fixed flag indicating a fixed issue (boolean) + @param autofixing flag indicating, that we are fixing issues + automatically (boolean) + @return reference to the created item (QTreeWidgetItem) + """ + from .CodeStyleFixer import FixableCodeStyleIssues + + if self.__lastFileItem is None: + # It's a new file + self.__lastFileItem = QTreeWidgetItem(self.resultList, [file]) + self.__lastFileItem.setFirstColumnSpanned(True) + self.__lastFileItem.setExpanded(True) + self.__lastFileItem.setData(0, self.filenameRole, file) + + fixable = False + code, message = message.split(None, 1) + itm = QTreeWidgetItem(self.__lastFileItem, + ["{0:6}".format(line), code, message]) + if code.startswith("W"): + itm.setIcon(1, UI.PixmapCache.getIcon("warning.png")) + elif code.startswith("N"): + itm.setIcon(1, UI.PixmapCache.getIcon("namingError.png")) + elif code.startswith("D"): + itm.setIcon(1, UI.PixmapCache.getIcon("docstringError.png")) + else: + itm.setIcon(1, UI.PixmapCache.getIcon("syntaxError.png")) + if fixed: + itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed.png")) + elif code in FixableCodeStyleIssues and not autofixing: + itm.setIcon(0, UI.PixmapCache.getIcon("issueFixable.png")) + fixable = True + + itm.setTextAlignment(0, Qt.AlignRight) + itm.setTextAlignment(1, Qt.AlignHCenter) + + itm.setTextAlignment(0, Qt.AlignVCenter) + itm.setTextAlignment(1, Qt.AlignVCenter) + itm.setTextAlignment(2, Qt.AlignVCenter) + + itm.setData(0, self.filenameRole, file) + itm.setData(0, self.lineRole, int(line)) + itm.setData(0, self.positionRole, int(pos)) + itm.setData(0, self.messageRole, message) + itm.setData(0, self.fixableRole, fixable) + itm.setData(0, self.codeRole, code) + + return itm + + def __modifyFixedResultItem(self, itm, text, fixed): + """ + Private method to modify a result list entry to show its + positive fixed state. + + @param itm reference to the item to modify (QTreeWidgetItem) + @param text text to be appended (string) + @param fixed flag indicating a fixed issue (boolean) + """ + if fixed: + message = itm.data(0, self.messageRole) + text + itm.setText(2, message) + itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed.png")) + + itm.setData(0, self.messageRole, message) + else: + itm.setIcon(0, QIcon()) + itm.setData(0, self.fixableRole, False) + + def __updateStatistics(self, statistics, fixer): + """ + Private method to update the collected statistics. + + @param statistics dictionary of statistical data with + message code as key and message count as value + @param fixer reference to the code style fixer (CodeStyleFixer) + """ + self.__statistics["_FilesCount"] += 1 + stats = {v: k for v, k in statistics.items() if v[0].isupper()} + if stats: + self.__statistics["_FilesIssues"] += 1 + for key in statistics: + if key in self.__statistics: + self.__statistics[key] += statistics[key] + else: + self.__statistics[key] = statistics[key] + if fixer: + self.__statistics["_IssuesFixed"] += fixer.fixed + + def __updateFixerStatistics(self, fixer): + """ + Private method to update the collected fixer related statistics. + + @param fixer reference to the code style fixer (CodeStyleFixer) + """ + self.__statistics["_IssuesFixed"] += fixer.fixed + + def __resetStatistics(self): + """ + Private slot to reset the statistics data. + """ + self.__statistics = {} + self.__statistics["_FilesCount"] = 0 + self.__statistics["_FilesIssues"] = 0 + self.__statistics["_IssuesFixed"] = 0 + + def prepare(self, fileList, project): + """ + Public method to prepare the dialog with a list of filenames. + + @param fileList list of filenames (list of strings) + @param project reference to the project object (Project) + """ + self.__fileOrFileList = fileList[:] + self.__project = project + self.__forProject = True + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + + self.__data = self.__project.getData("CHECKERSPARMS", "Pep8Checker") + if self.__data is None or \ + len(self.__data) < 6: + # initialize the data structure + self.__data = { + "ExcludeFiles": "", + "ExcludeMessages": pep8.DEFAULT_IGNORE, + "IncludeMessages": "", + "RepeatMessages": False, + "FixCodes": "", + "FixIssues": False, + } + if "MaxLineLength" not in self.__data: + self.__data["MaxLineLength"] = pep8.MAX_LINE_LENGTH + if "HangClosing" not in self.__data: + self.__data["HangClosing"] = False + if "NoFixCodes" not in self.__data: + self.__data["NoFixCodes"] = "E501" + if "DocstringType" not in self.__data: + self.__data["DocstringType"] = "pep257" + + self.excludeFilesEdit.setText(self.__data["ExcludeFiles"]) + self.excludeMessagesEdit.setText(self.__data["ExcludeMessages"]) + self.includeMessagesEdit.setText(self.__data["IncludeMessages"]) + self.repeatCheckBox.setChecked(self.__data["RepeatMessages"]) + self.fixIssuesEdit.setText(self.__data["FixCodes"]) + self.noFixIssuesEdit.setText(self.__data["NoFixCodes"]) + self.fixIssuesCheckBox.setChecked(self.__data["FixIssues"]) + self.lineLengthSpinBox.setValue(self.__data["MaxLineLength"]) + self.hangClosingCheckBox.setChecked(self.__data["HangClosing"]) + self.docTypeComboBox.setCurrentIndex( + self.docTypeComboBox.findData(self.__data["DocstringType"])) + + def start(self, fn, save=False, repeat=None): + """ + Public slot to start the code style check. + + @param fn file or list of files or directory to be checked + (string or list of strings) + @keyparam save flag indicating to save the given + file/file list/directory (boolean) + @keyparam repeat state of the repeat check box if it is not None + (None or boolean) + """ + if self.__project is None: + self.__project = e5App().getObject("Project") + + self.cancelled = False + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + self.statisticsButton.setEnabled(False) + self.showButton.setEnabled(False) + self.fixButton.setEnabled(False) + self.startButton.setEnabled(False) + if repeat is not None: + self.repeatCheckBox.setChecked(repeat) + self.checkProgress.setVisible(True) + QApplication.processEvents() + + self.__resetStatistics() + + if save: + self.__fileOrFileList = fn + + if isinstance(fn, list): + files = fn[:] + elif os.path.isdir(fn): + files = [] + for ext in Preferences.getPython("Python3Extensions"): + files.extend( + Utilities.direntries(fn, 1, '*{0}'.format(ext), 0)) + for ext in Preferences.getPython("PythonExtensions"): + files.extend( + Utilities.direntries(fn, 1, '*{0}'.format(ext), 0)) + else: + files = [fn] + + # filter the list depending on the filter string + if files: + filterString = self.excludeFilesEdit.text() + filterList = [f.strip() for f in filterString.split(",") + if f.strip()] + for filter in filterList: + files = \ + [f for f in files + if not fnmatch.fnmatch(f, filter.strip())] + + py3files = [f for f in files \ + if f.endswith( + tuple(Preferences.getPython("Python3Extensions")))] + py2files = [f for f in files \ + if f.endswith( + tuple(Preferences.getPython("PythonExtensions")))] + + if len(py3files) + len(py2files) > 0: + self.checkProgress.setMaximum(len(py3files) + len(py2files)) + self.checkProgressLabel.setVisible( + len(py3files) + len(py2files) > 1) + QApplication.processEvents() + + # extract the configuration values + excludeMessages = self.excludeMessagesEdit.text() + includeMessages = self.includeMessagesEdit.text() + repeatMessages = self.repeatCheckBox.isChecked() + fixCodes = self.fixIssuesEdit.text() + noFixCodes = self.noFixIssuesEdit.text() + fixIssues = self.fixIssuesCheckBox.isChecked() and repeatMessages + maxLineLength = self.lineLengthSpinBox.value() + hangClosing = self.hangClosingCheckBox.isChecked() + docType = self.docTypeComboBox.itemData( + self.docTypeComboBox.currentIndex()) + + try: + # disable updates of the list for speed + self.resultList.setUpdatesEnabled(False) + self.resultList.setSortingEnabled(False) + + # now go through all the files + progress = 0 + for file in sorted(py3files + py2files): + self.checkProgress.setValue(progress) + self.checkProgressLabel.setPath(file) + QApplication.processEvents() + + if self.cancelled: + self.__resort() + return + + self.__lastFileItem = None + + try: + source, encoding = Utilities.readEncodedFile(file) + source = source.splitlines(True) + except (UnicodeError, IOError) as msg: + self.noResults = False + self.__createResultItem(file, "1", "1", + self.trUtf8("Error: {0}").format(str(msg))\ + .rstrip()[1:-1], False, False) + progress += 1 + continue + + stats = {} + flags = Utilities.extractFlags(source) + ext = os.path.splitext(file)[1] + if fixIssues: + from .CodeStyleFixer import CodeStyleFixer + fixer = CodeStyleFixer(self.__project, file, source, + fixCodes, noFixCodes, maxLineLength, + True) # always fix in place + else: + fixer = None + if ("FileType" in flags and + flags["FileType"] in ["Python", "Python2"]) or \ + file in py2files or \ + (ext in [".py", ".pyw"] and \ + Preferences.getProject("DeterminePyFromProject") and \ + self.__project.isOpen() and \ + self.__project.isProjectFile(file) and \ + self.__project.getProjectLanguage() in ["Python", + "Python2"]): + from .CodeStyleChecker import CodeStyleCheckerPy2 + report = CodeStyleCheckerPy2(file, [], + repeat=repeatMessages, + select=includeMessages, + ignore=excludeMessages, + max_line_length=maxLineLength, + hang_closing=hangClosing, + docType=docType, + ) + errors = report.errors[:] + stats.update(report.counters) + else: + if includeMessages: + select = [s.strip() for s in includeMessages.split(',') + if s.strip()] + else: + select = [] + if excludeMessages: + ignore = [i.strip() for i in excludeMessages.split(',') + if i.strip()] + else: + ignore = [] + + # check PEP-8 + styleGuide = pep8.StyleGuide( + reporter=CodeStyleCheckerReport, + repeat=repeatMessages, + select=select, + ignore=ignore, + max_line_length=maxLineLength, + hang_closing=hangClosing, + ) + report = styleGuide.check_files([file]) + stats.update(report.counters) + + # check PEP-257 + pep257Checker = DocStyleChecker( + source, file, select, ignore, [], repeatMessages, + maxLineLength=maxLineLength, docType=docType) + pep257Checker.run() + stats.update(pep257Checker.counters) + + errors = report.errors + pep257Checker.errors + + deferredFixes = {} + for error in errors: + fname, lineno, position, text = error + if lineno > len(source): + lineno = len(source) + if "__IGNORE_WARNING__" not in Utilities.extractLineFlags( + source[lineno - 1].strip()): + self.noResults = False + if fixer: + res, msg, id_ = fixer.fixIssue(lineno, position, text) + if res == 1: + text += "\n" + \ + self.trUtf8("Fix: {0}").format(msg) + self.__createResultItem( + fname, lineno, position, text, True, True) + elif res == 0: + self.__createResultItem( + fname, lineno, position, text, False, True) + else: + itm = self.__createResultItem( + fname, lineno, position, + text, False, False) + deferredFixes[id_] = itm + else: + self.__createResultItem( + fname, lineno, position, text, False, False) + if fixer: + deferredResults = fixer.finalize() + for id_ in deferredResults: + fixed, msg = deferredResults[id_] + itm = deferredFixes[id_] + if fixed == 1: + text = "\n" + self.trUtf8("Fix: {0}").format(msg) + self.__modifyFixedResultItem(itm, text, True) + else: + self.__modifyFixedResultItem(itm, "", False) + fixer.saveFile(encoding) + self.__updateStatistics(stats, fixer) + progress += 1 + finally: + # reenable updates of the list + self.resultList.setSortingEnabled(True) + self.resultList.setUpdatesEnabled(True) + self.checkProgress.setValue(progress) + self.checkProgressLabel.setPath("") + QApplication.processEvents() + self.__resort() + else: + self.checkProgress.setMaximum(1) + self.checkProgress.setValue(1) + self.__finish() + + def __finish(self): + """ + Private slot called when the code style check finished or the user + pressed the cancel button. + """ + 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.statisticsButton.setEnabled(True) + self.showButton.setEnabled(True) + self.startButton.setEnabled(True) + + if self.noResults: + QTreeWidgetItem(self.resultList, [self.trUtf8('No issues found.')]) + QApplication.processEvents() + self.statisticsButton.setEnabled(False) + self.showButton.setEnabled(False) + self.__clearErrors() + else: + self.statisticsButton.setEnabled(True) + self.showButton.setEnabled(True) + self.resultList.header().resizeSections(QHeaderView.ResizeToContents) + self.resultList.header().setStretchLastSection(True) + + self.checkProgress.setVisible(False) + self.checkProgressLabel.setVisible(False) + + @pyqtSlot() + def on_startButton_clicked(self): + """ + Private slot to start a code style check run. + """ + if self.__forProject: + data = { + "ExcludeFiles": self.excludeFilesEdit.text(), + "ExcludeMessages": self.excludeMessagesEdit.text(), + "IncludeMessages": self.includeMessagesEdit.text(), + "RepeatMessages": self.repeatCheckBox.isChecked(), + "FixCodes": self.fixIssuesEdit.text(), + "NoFixCodes": self.noFixIssuesEdit.text(), + "FixIssues": self.fixIssuesCheckBox.isChecked(), + "MaxLineLength": self.lineLengthSpinBox.value(), + "HangClosing": self.hangClosingCheckBox.isChecked(), + "DocstringType": self.docTypeComboBox.itemData( + self.docTypeComboBox.currentIndex()), + } + if data != self.__data: + self.__data = data + self.__project.setData("CHECKERSPARMS", "Pep8Checker", + self.__data) + + self.resultList.clear() + self.noResults = True + self.cancelled = False + self.start(self.__fileOrFileList) + + def __selectCodes(self, edit, showFixCodes): + """ + Private method to select message codes via a selection dialog. + + @param edit reference of the line edit to be populated (QLineEdit) + @param showFixCodes flag indicating to show a list of fixable + issues (boolean) + """ + from .CodeStyleCodeSelectionDialog import CodeStyleCodeSelectionDialog + dlg = CodeStyleCodeSelectionDialog(edit.text(), showFixCodes, self) + if dlg.exec_() == QDialog.Accepted: + edit.setText(dlg.getSelectedCodes()) + + @pyqtSlot() + def on_excludeMessagesSelectButton_clicked(self): + """ + Private slot to select the message codes to be excluded via a + selection dialog. + """ + self.__selectCodes(self.excludeMessagesEdit, False) + + @pyqtSlot() + def on_includeMessagesSelectButton_clicked(self): + """ + Private slot to select the message codes to be included via a + selection dialog. + """ + self.__selectCodes(self.includeMessagesEdit, False) + + @pyqtSlot() + def on_fixIssuesSelectButton_clicked(self): + """ + Private slot to select the issue codes to be fixed via a + selection dialog. + """ + self.__selectCodes(self.fixIssuesEdit, True) + + @pyqtSlot() + def on_noFixIssuesSelectButton_clicked(self): + """ + Private slot to select the issue codes not to be fixed via a + selection dialog. + """ + self.__selectCodes(self.noFixIssuesEdit, True) + + @pyqtSlot(QTreeWidgetItem, int) + def on_resultList_itemActivated(self, item, column): + """ + Private slot to handle the activation of an item. + + @param item reference to the activated item (QTreeWidgetItem) + @param column column the item was activated in (integer) + """ + if self.noResults: + return + + if item.parent(): + fn = Utilities.normabspath(item.data(0, self.filenameRole)) + lineno = item.data(0, self.lineRole) + position = item.data(0, self.positionRole) + message = item.data(0, self.messageRole) + code = item.data(0, self.codeRole) + + vm = e5App().getObject("ViewManager") + vm.openSourceFile(fn, lineno=lineno, pos=position + 1) + editor = vm.getOpenEditor(fn) + + if code == "E901": + editor.toggleSyntaxError(lineno, 0, True, message, True) + else: + editor.toggleFlakesWarning( + lineno, True, message, warningType=editor.WarningStyle) + + @pyqtSlot() + def on_resultList_itemSelectionChanged(self): + """ + Private slot to change the dialog state depending on the selection. + """ + self.fixButton.setEnabled(len(self.__getSelectedFixableItems()) > 0) + + @pyqtSlot() + def on_showButton_clicked(self): + """ + Private slot to handle the "Show" button press. + """ + vm = e5App().getObject("ViewManager") + + selectedIndexes = [] + for index in range(self.resultList.topLevelItemCount()): + if self.resultList.topLevelItem(index).isSelected(): + selectedIndexes.append(index) + if len(selectedIndexes) == 0: + selectedIndexes = list(range(self.resultList.topLevelItemCount())) + for index in selectedIndexes: + itm = self.resultList.topLevelItem(index) + fn = Utilities.normabspath(itm.data(0, self.filenameRole)) + vm.openSourceFile(fn, 1) + editor = vm.getOpenEditor(fn) + editor.clearFlakesWarnings() + for cindex in range(itm.childCount()): + citm = itm.child(cindex) + lineno = citm.data(0, self.lineRole) + message = citm.data(0, self.messageRole) + editor.toggleFlakesWarning(lineno, True, message) + + # go through the list again to clear warning markers for files, + # that are ok + openFiles = vm.getOpenFilenames() + errorFiles = [] + for index in range(self.resultList.topLevelItemCount()): + itm = self.resultList.topLevelItem(index) + errorFiles.append( + Utilities.normabspath(itm.data(0, self.filenameRole))) + for file in openFiles: + if not file in errorFiles: + editor = vm.getOpenEditor(file) + editor.clearFlakesWarnings() + + @pyqtSlot() + def on_statisticsButton_clicked(self): + """ + Private slot to show the statistics dialog. + """ + from .CodeStyleStatisticsDialog import CodeStyleStatisticsDialog + dlg = CodeStyleStatisticsDialog(self.__statistics, self) + dlg.exec_() + + @pyqtSlot() + def on_loadDefaultButton_clicked(self): + """ + Private slot to load the default configuration values. + """ + self.excludeFilesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/ExcludeFilePatterns")) + self.excludeMessagesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/ExcludeMessages", pep8.DEFAULT_IGNORE)) + self.includeMessagesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/IncludeMessages")) + self.repeatCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/RepeatMessages"))) + self.fixIssuesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/FixCodes")) + self.noFixIssuesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/NoFixCodes", "E501")) + self.fixIssuesCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/FixIssues"))) + self.lineLengthSpinBox.setValue(int(Preferences.Prefs.settings.value( + "PEP8/MaxLineLength", pep8.MAX_LINE_LENGTH))) + self.hangClosingCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/HangClosing"))) + self.docTypeComboBox.setCurrentIndex(self.docTypeComboBox.findData( + Preferences.Prefs.settings.value("PEP8/DocstringType", "pep257"))) + + @pyqtSlot() + def on_storeDefaultButton_clicked(self): + """ + Private slot to store the current configuration values as + default values. + """ + Preferences.Prefs.settings.setValue("PEP8/ExcludeFilePatterns", + self.excludeFilesEdit.text()) + Preferences.Prefs.settings.setValue("PEP8/ExcludeMessages", + self.excludeMessagesEdit.text()) + Preferences.Prefs.settings.setValue("PEP8/IncludeMessages", + self.includeMessagesEdit.text()) + Preferences.Prefs.settings.setValue("PEP8/RepeatMessages", + self.repeatCheckBox.isChecked()) + Preferences.Prefs.settings.setValue("PEP8/FixCodes", + self.fixIssuesEdit.text()) + Preferences.Prefs.settings.setValue("PEP8/NoFixCodes", + self.noFixIssuesEdit.text()) + Preferences.Prefs.settings.setValue("PEP8/FixIssues", + self.fixIssuesCheckBox.isChecked()) + Preferences.Prefs.settings.setValue("PEP8/MaxLineLength", + self.lineLengthSpinBox.value()) + Preferences.Prefs.settings.setValue("PEP8/HangClosing", + self.hangClosingCheckBox.isChecked()) + Preferences.Prefs.settings.setValue("PEP8/DocstringType", + self.docTypeComboBox.itemData( + self.docTypeComboBox.currentIndex())) + + @pyqtSlot() + def on_resetDefaultButton_clicked(self): + """ + Private slot to reset the configuration values to their default values. + """ + Preferences.Prefs.settings.setValue("PEP8/ExcludeFilePatterns", "") + Preferences.Prefs.settings.setValue("PEP8/ExcludeMessages", + pep8.DEFAULT_IGNORE) + Preferences.Prefs.settings.setValue("PEP8/IncludeMessages", "") + Preferences.Prefs.settings.setValue("PEP8/RepeatMessages", False) + Preferences.Prefs.settings.setValue("PEP8/FixCodes", "") + Preferences.Prefs.settings.setValue("PEP8/NoFixCodes", "E501") + Preferences.Prefs.settings.setValue("PEP8/FixIssues", False) + Preferences.Prefs.settings.setValue("PEP8/MaxLineLength", + pep8.MAX_LINE_LENGTH) + Preferences.Prefs.settings.setValue("PEP8/HangClosing", False) + Preferences.Prefs.settings.setValue("PEP8/DocstringType", "pep257") + + @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 (QAbstractButton) + """ + if button == self.buttonBox.button(QDialogButtonBox.Close): + self.close() + elif button == self.buttonBox.button(QDialogButtonBox.Cancel): + self.__finish() + elif button == self.showButton: + self.on_showButton_clicked() + elif button == self.statisticsButton: + self.on_statisticsButton_clicked() + + def __clearErrors(self): + """ + Private method to clear all warning markers of open editors. + """ + vm = e5App().getObject("ViewManager") + openFiles = vm.getOpenFilenames() + for file in openFiles: + editor = vm.getOpenEditor(file) + editor.clearFlakesWarnings() + + @pyqtSlot() + def on_fixButton_clicked(self): + """ + Private slot to fix selected issues. + """ + from .CodeStyleFixer import CodeStyleFixer + + # build a dictionary of issues to fix + fixableItems = self.__getSelectedFixableItems() + fixesDict = {} # dictionary of lists of tuples containing + # the issue and the item + for itm in fixableItems: + filename = itm.data(0, self.filenameRole) + if filename not in fixesDict: + fixesDict[filename] = [] + fixesDict[filename].append(( + (itm.data(0, self.lineRole), + itm.data(0, self.positionRole), + "{0} {1}".format(itm.data(0, self.codeRole), + itm.data(0, self.messageRole))), + itm + )) + + # extract the configuration values + fixCodes = self.fixIssuesEdit.text() + noFixCodes = self.noFixIssuesEdit.text() + maxLineLength = self.lineLengthSpinBox.value() + + # now go through all the files + if fixesDict: + self.checkProgress.setMaximum(len(fixesDict)) + progress = 0 + for file in fixesDict: + self.checkProgress.setValue(progress) + QApplication.processEvents() + + try: + source, encoding = Utilities.readEncodedFile(file) + source = source.splitlines(True) + except (UnicodeError, IOError) as msg: + # skip silently because that should not happen + progress += 1 + continue + + deferredFixes = {} + fixer = CodeStyleFixer(self.__project, file, source, + fixCodes, noFixCodes, maxLineLength, + True) # always fix in place + errors = fixesDict[file] + errors.sort(key=lambda a: a[0][0]) + for error in errors: + (lineno, position, text), itm = error + if lineno > len(source): + lineno = len(source) + fixed, msg, id_ = fixer.fixIssue(lineno, position, text) + if fixed == 1: + text = "\n" + self.trUtf8("Fix: {0}").format(msg) + self.__modifyFixedResultItem(itm, text, True) + elif fixed == 0: + self.__modifyFixedResultItem(itm, "", False) + else: + # remember item for the deferred fix + deferredFixes[id_] = itm + deferredResults = fixer.finalize() + for id_ in deferredResults: + fixed, msg = deferredResults[id_] + itm = deferredFixes[id_] + if fixed == 1: + text = "\n" + self.trUtf8("Fix: {0}").format(msg) + self.__modifyFixedResultItem(itm, text, True) + else: + self.__modifyFixedResultItem(itm, "", False) + fixer.saveFile(encoding) + + self.__updateFixerStatistics(fixer) + progress += 1 + + self.checkProgress.setValue(progress) + QApplication.processEvents() + + def __getSelectedFixableItems(self): + """ + Private method to extract all selected items for fixable issues. + + @return selected items for fixable issues (list of QTreeWidgetItem) + """ + fixableItems = [] + for itm in self.resultList.selectedItems(): + if itm.childCount() > 0: + for index in range(itm.childCount()): + citm = itm.child(index) + if self.__itemFixable(citm) and not citm in fixableItems: + fixableItems.append(citm) + elif self.__itemFixable(itm) and not itm in fixableItems: + fixableItems.append(itm) + + return fixableItems + + def __itemFixable(self, itm): + """ + Private method to check, if an item has a fixable issue. + + @param itm item to be checked (QTreeWidgetItem) + @return flag indicating a fixable issue (boolean) + """ + return itm.data(0, self.fixableRole)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,510 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CodeStyleCheckerDialog</class> + <widget class="QDialog" name="CodeStyleCheckerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>700</height> + </rect> + </property> + <property name="windowTitle"> + <string>Code Style Check Result</string> + </property> + <property name="whatsThis"> + <string><b>Code Style Check Results</b> +<p>This dialog shows the results of the code style check. Double clicking an +entry will open an editor window and position the cursor at the respective line and position.</p></string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QFrame" name="filterFrame"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="margin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Exclude Files:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <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 row="0" column="3" rowspan="9"> + <widget class="Line" name="line"> + <property name="lineWidth"> + <number>2</number> + </property> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + <item row="0" column="4" rowspan="9"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPushButton" name="startButton"> + <property name="toolTip"> + <string>Press to start the code style check run</string> + </property> + <property name="text"> + <string>Start</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="fixButton"> + <property name="toolTip"> + <string>Press to fix the selected issues</string> + </property> + <property name="text"> + <string>Fix Selected</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>18</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="loadDefaultButton"> + <property name="toolTip"> + <string>Press to load the default values</string> + </property> + <property name="text"> + <string>Load Defaults</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="storeDefaultButton"> + <property name="toolTip"> + <string>Press to store the current values as defaults</string> + </property> + <property name="text"> + <string>Store Defaults</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="resetDefaultButton"> + <property name="toolTip"> + <string>Press to reset the default values</string> + </property> + <property name="text"> + <string>Reset Defaults</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Exclude Messages:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="E5ClearableLineEdit" name="excludeMessagesEdit"> + <property name="toolTip"> + <string>Enter message codes or categories to be excluded separated by a comma</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QToolButton" name="excludeMessagesSelectButton"> + <property name="toolTip"> + <string>Press to select the message codes from a list</string> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Included Messages:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="E5ClearableLineEdit" name="includeMessagesEdit"> + <property name="toolTip"> + <string>Enter message codes or categories to be included separated by a comma</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QToolButton" name="includeMessagesSelectButton"> + <property name="toolTip"> + <string>Press to select the message codes from a list</string> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Fix Issues:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="E5ClearableLineEdit" name="fixIssuesEdit"> + <property name="toolTip"> + <string>Enter message codes of issues to be fixed automatically (leave empty to fix all)</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QToolButton" name="fixIssuesSelectButton"> + <property name="toolTip"> + <string>Press to select the message codes from a list</string> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Don't Fix Issues:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="E5ClearableLineEdit" name="noFixIssuesEdit"> + <property name="toolTip"> + <string>Enter message codes of issues not to be fixed automatically</string> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QToolButton" name="noFixIssuesSelectButton"> + <property name="toolTip"> + <string>Press to select the message codes from a list</string> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Max. Line Length:</string> + </property> + </widget> + </item> + <item row="5" column="1" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QSpinBox" name="lineLengthSpinBox"> + <property name="statusTip"> + <string>Enter the maximum allowed line length (PEP-8: 79 characters)</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>60</number> + </property> + <property name="maximum"> + <number>119</number> + </property> + <property name="value"> + <number>79</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Docstring Type:</string> + </property> + </widget> + </item> + <item row="6" column="1" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QComboBox" name="docTypeComboBox"> + <property name="toolTip"> + <string>Select the rule set for docstrings</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="7" column="0" colspan="3"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QCheckBox" name="hangClosingCheckBox"> + <property name="toolTip"> + <string>Select to allow hanging closing brackets</string> + </property> + <property name="text"> + <string>Allow hanging closing brackets</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="8" column="0" colspan="3"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="repeatCheckBox"> + <property name="toolTip"> + <string>Select to repeat each message type</string> + </property> + <property name="text"> + <string>Repeat messages</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="fixIssuesCheckBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Select to fix some issues</string> + </property> + <property name="text"> + <string>Fix issues automatically</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + <zorder>label_2</zorder> + <zorder>excludeFilesEdit</zorder> + <zorder>line</zorder> + <zorder>label</zorder> + <zorder>excludeMessagesEdit</zorder> + <zorder>excludeMessagesSelectButton</zorder> + <zorder>label_3</zorder> + <zorder>includeMessagesEdit</zorder> + <zorder>includeMessagesSelectButton</zorder> + <zorder>label_4</zorder> + <zorder>fixIssuesEdit</zorder> + <zorder>fixIssuesSelectButton</zorder> + <zorder>label_6</zorder> + <zorder>noFixIssuesEdit</zorder> + <zorder>noFixIssuesSelectButton</zorder> + <zorder>label_5</zorder> + <zorder></zorder> + <zorder>label_7</zorder> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="resultList"> + <property name="whatsThis"> + <string><b>Result List</b> +<p>This list shows the results of the code style check. Double clicking +an entry will open this entry in an editor window and position the cursor at +the respective line and position.</p></string> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>File/Line</string> + </property> + </column> + <column> + <property name="text"> + <string>Code</string> + </property> + </column> + <column> + <property name="text"> + <string>Message</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 style check check</string> + </property> + <property name="value"> + <number>0</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </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>E5SqueezeLabelPath</class> + <extends>QLabel</extends> + <header>E5Gui/E5SqueezeLabels.h</header> + </customwidget> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>startButton</tabstop> + <tabstop>loadDefaultButton</tabstop> + <tabstop>excludeFilesEdit</tabstop> + <tabstop>excludeMessagesEdit</tabstop> + <tabstop>excludeMessagesSelectButton</tabstop> + <tabstop>includeMessagesEdit</tabstop> + <tabstop>includeMessagesSelectButton</tabstop> + <tabstop>fixIssuesEdit</tabstop> + <tabstop>fixIssuesSelectButton</tabstop> + <tabstop>noFixIssuesEdit</tabstop> + <tabstop>noFixIssuesSelectButton</tabstop> + <tabstop>lineLengthSpinBox</tabstop> + <tabstop>hangClosingCheckBox</tabstop> + <tabstop>repeatCheckBox</tabstop> + <tabstop>fixIssuesCheckBox</tabstop> + <tabstop>storeDefaultButton</tabstop> + <tabstop>resultList</tabstop> + <tabstop>fixButton</tabstop> + <tabstop>resetDefaultButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>repeatCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>fixIssuesCheckBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>121</x> + <y>153</y> + </hint> + <hint type="destinationlabel"> + <x>186</x> + <y>160</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to select code style message codes. +""" + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QDialog, QTreeWidgetItem + +from . import pep8 +from .NamingStyleChecker import NamingStyleChecker +from .DocStyleChecker import DocStyleChecker + +from .Ui_CodeStyleCodeSelectionDialog import Ui_CodeStyleCodeSelectionDialog + +import UI.PixmapCache + + +class CodeStyleCodeSelectionDialog(QDialog, Ui_CodeStyleCodeSelectionDialog): + """ + Class implementing a dialog to select code style message codes. + """ + def __init__(self, codes, showFixCodes, parent=None): + """ + Constructor + + @param codes comma separated list of selected codes (string) + @param showFixCodes flag indicating to show a list of fixable + issues (boolean) + @param parent reference to the parent widget (QWidget) + """ + super().__init__(parent) + self.setupUi(self) + + codeList = [code.strip() for code in codes.split(",") if code.strip()] + + if showFixCodes: + from .CodeStyleFixer import FixableCodeStyleIssues + selectableCodes = FixableCodeStyleIssues + else: + selectableCodes = list(pep8.pep8_messages.keys()) + selectableCodes.extend(NamingStyleChecker.Messages.keys()) + selectableCodes.extend(DocStyleChecker.Messages.keys()) + for code in sorted(selectableCodes): + if code in pep8.pep8_messages_sample_args: + message = QCoreApplication.translate( + "pep8", pep8.pep8_messages[code]).format( + *pep8.pep8_messages_sample_args[code]) + elif code in pep8.pep8_messages: + message = QCoreApplication.translate( + "pep8", pep8.pep8_messages[code]) + elif code in NamingStyleChecker.Messages: + message = QCoreApplication.translate( + "NamingStyleChecker", + NamingStyleChecker.Messages[code]) + elif code in DocStyleChecker.Messages: + message = QCoreApplication.translate( + "DocStyleChecker", DocStyleChecker.Messages[code]) + else: + continue + itm = QTreeWidgetItem(self.codeTable, [code, message]) + if code.startswith("W"): + itm.setIcon(0, UI.PixmapCache.getIcon("warning.png")) + elif code.startswith("E"): + itm.setIcon(0, UI.PixmapCache.getIcon("syntaxError.png")) + elif code.startswith("N"): + itm.setIcon(0, UI.PixmapCache.getIcon("namingError.png")) + elif code.startswith("D"): + itm.setIcon(0, UI.PixmapCache.getIcon("docstringError.png")) + if code in codeList: + itm.setSelected(True) + codeList.remove(code) + + self.__extraCodes = codeList[:] + + def getSelectedCodes(self): + """ + Public method to get a comma separated list of codes selected. + + @return comma separated list of selected codes (string) + """ + selectedCodes = [] + + for itm in self.codeTable.selectedItems(): + selectedCodes.append(itm.text(0)) + + return ", ".join(self.__extraCodes + selectedCodes)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.ui Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CodeStyleCodeSelectionDialog</class> + <widget class="QDialog" name="CodeStyleCodeSelectionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>450</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Code Style Message Codes</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Select the message codes from the list:</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="codeTable"> + <property name="toolTip"> + <string>Select the message codes from this table</string> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Code</string> + </property> + </column> + <column> + <property name="text"> + <string>Message</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>codeTable</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CodeStyleCodeSelectionDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>279</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CodeStyleCodeSelectionDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>285</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleFixer.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,2713 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to fix certain code style issues. +""" + +import os +import re +import tokenize +import io + +from PyQt4.QtCore import QObject + +from E5Gui import E5MessageBox + +from . import pep8 + +import Utilities + +FixableCodeStyleIssues = [ + "D111", "D112", "D113", "D121", "D131", "D141", + "D142", "D143", "D144", "D145", + "D221", "D222", "D231", "D242", "D243", "D244", + "D245", "D246", "D247", + "E101", "E111", "E121", "E122", "E123", "E124", + "E125", "E126", "E127", "E128", "E133", "E201", + "E202", "E203", "E211", "E221", "E222", "E223", + "E224", "E225", "E226", "E227", "E228", "E231", + "E241", "E242", "E251", "E261", "E262", "E271", + "E272", "E273", "E274", "E301", "E302", "E303", + "E304", "E401", "E501", "E502", "E701", "E702", + "E703", "E711", "E712", + "N804", "N805", "N806", + "W191", "W291", "W292", "W293", "W391", "W603", +] + + +class CodeStyleFixer(QObject): + """ + Class implementing a fixer for certain code style issues. + """ + def __init__(self, project, filename, sourceLines, fixCodes, noFixCodes, + maxLineLength, inPlace): + """ + Constructor + + @param project reference to the project object (Project) + @param filename name of the file to be fixed (string) + @param sourceLines list of source lines including eol marker + (list of string) + @param fixCodes list of codes to be fixed as a comma separated + string (string) + @param noFixCodes list of codes not to be fixed as a comma + separated string (string) + @param maxLineLength maximum allowed line length (integer) + @param inPlace flag indicating to modify the file in place (boolean) + """ + super().__init__() + + self.__project = project + self.__filename = filename + self.__origName = "" + self.__source = sourceLines[:] # save a copy + self.__fixCodes = [c.strip() for c in fixCodes.split(",") if c.strip()] + self.__noFixCodes = [ + c.strip() for c in noFixCodes.split(",") if c.strip()] + self.__maxLineLength = maxLineLength + self.fixed = 0 + + self.__reindenter = None + self.__eol = "" + self.__indentWord = self.__getIndentWord() + + if not inPlace: + self.__origName = self.__filename + self.__filename = os.path.join( + os.path.dirname(self.__filename), + "fixed_" + os.path.basename(self.__filename)) + + self.__fixes = { + "D111": self.__fixD111, + "D112": self.__fixD112, + "D113": self.__fixD112, + "D121": self.__fixD121, + "D131": self.__fixD131, + "D141": self.__fixD141, + "D142": self.__fixD142, + "D143": self.__fixD143, + "D144": self.__fixD144, + "D145": self.__fixD145, + "D221": self.__fixD221, + "D222": self.__fixD221, + "D231": self.__fixD131, + "D242": self.__fixD242, + "D243": self.__fixD243, + "D244": self.__fixD242, + "D245": self.__fixD243, + "D246": self.__fixD144, + "D247": self.__fixD247, + "E101": self.__fixE101, + "E111": self.__fixE101, + "E121": self.__fixE121, + "E122": self.__fixE122, + "E123": self.__fixE123, + "E124": self.__fixE121, + "E125": self.__fixE125, + "E126": self.__fixE126, + "E127": self.__fixE127, + "E128": self.__fixE127, + "E133": self.__fixE126, + "E201": self.__fixE201, + "E202": self.__fixE201, + "E203": self.__fixE201, + "E211": self.__fixE201, + "E221": self.__fixE221, + "E222": self.__fixE221, + "E223": self.__fixE221, + "E224": self.__fixE221, + "E225": self.__fixE221, + "E226": self.__fixE221, + "E227": self.__fixE221, + "E228": self.__fixE221, + "E231": self.__fixE231, + "E241": self.__fixE221, + "E242": self.__fixE221, + "E251": self.__fixE251, + "E261": self.__fixE261, + "E262": self.__fixE261, + "E271": self.__fixE221, + "E272": self.__fixE221, + "E273": self.__fixE221, + "E274": self.__fixE221, + "E301": self.__fixE301, + "E302": self.__fixE302, + "E303": self.__fixE303, + "E304": self.__fixE304, + "E401": self.__fixE401, + "E501": self.__fixE501, + "E502": self.__fixE502, + "E701": self.__fixE701, + "E702": self.__fixE702, + "E703": self.__fixE702, + "E711": self.__fixE711, + "E712": self.__fixE711, + "N804": self.__fixN804, + "N805": self.__fixN804, + "N806": self.__fixN806, + "W191": self.__fixE101, + "W291": self.__fixW291, + "W292": self.__fixW292, + "W293": self.__fixW291, + "W391": self.__fixW391, + "W603": self.__fixW603, + } + self.__modified = False + self.__stackLogical = [] # these need to be fixed before the file + # is saved but after all other inline + # fixes. These work with logical lines. + self.__stack = [] # these need to be fixed before the file + # is saved but after all inline fixes + + self.__multiLineNumbers = None + self.__docLineNumbers = None + + self.__lastID = 0 + + def saveFile(self, encoding): + """ + Public method to save the modified file. + + @param encoding encoding of the source file (string) + @return flag indicating success (boolean) + """ + if not self.__modified: + # no need to write + return True + + txt = "".join(self.__source) + try: + Utilities.writeEncodedFile(self.__filename, txt, encoding) + except (IOError, Utilities.CodingError, UnicodeError) as err: + E5MessageBox.critical( + self, + self.trUtf8("Fix Code Style Issues"), + self.trUtf8( + """<p>Could not save the file <b>{0}</b>.""" + """ Skipping it.</p><p>Reason: {1}</p>""") + .format(self.__filename, str(err)) + ) + return False + + return True + + def __codeMatch(self, code): + """ + Private method to check, if the code should be fixed. + + @param code to check (string) + @return flag indicating it should be fixed (boolean) + """ + def mutualStartswith(a, b): + """ + Local helper method to compare the beginnings of two strings + against each other. + + @return flag indicating that one string starts with the other + (boolean) + """ + return b.startswith(a) or a.startswith(b) + + if self.__noFixCodes: + for noFixCode in [c.strip() for c in self.__noFixCodes]: + if mutualStartswith(code.lower(), noFixCode.lower()): + return False + + if self.__fixCodes: + for fixCode in [c.strip() for c in self.__fixCodes]: + if mutualStartswith(code.lower(), fixCode.lower()): + return True + return False + + return True + + def fixIssue(self, line, pos, message): + """ + Public method to fix the fixable issues. + + @param line line number of issue (integer) + @param pos character position of issue (integer) + @param message message text (string) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + code = message.split(None, 1)[0].strip() + + if line <= len(self.__source) and \ + self.__codeMatch(code) and \ + code in self.__fixes: + res = self.__fixes[code](code, line, pos) + if res[0] == 1: + self.__modified = True + self.fixed += 1 + else: + res = (0, "", 0) + + return res + + def finalize(self): + """ + Public method to apply all deferred fixes. + + @return dictionary containing the fix results + """ + results = {} + + # step 1: do fixes operating on logical lines first + for id_, code, line, pos in self.__stackLogical: + res, msg, _ = self.__fixes[code](code, line, pos, apply=True) + if res == 1: + self.__modified = True + self.fixed += 1 + results[id_] = (res, msg) + + # step 2: do fixes that change the number of lines + for id_, code, line, pos in reversed(self.__stack): + res, msg, _ = self.__fixes[code](code, line, pos, apply=True) + if res == 1: + self.__modified = True + self.fixed += 1 + results[id_] = (res, msg) + + return results + + def __getID(self): + """ + Private method to get the ID for a deferred fix. + + @return ID for a deferred fix (integer) + """ + self.__lastID += 1 + return self.__lastID + + def __getEol(self): + """ + Private method to get the applicable eol string. + + @return eol string (string) + """ + if not self.__eol: + if self.__origName: + fn = self.__origName + else: + fn = self.__filename + + if self.__project.isOpen() and self.__project.isProjectFile(fn): + self.__eol = self.__project.getEolString() + else: + self.__eol = Utilities.linesep() + return self.__eol + + def __findLogical(self): + """ + Private method to extract the index of all the starts and ends of + lines. + + @return tuple containing two lists of integer with start and end tuples + of lines + """ + logical_start = [] + logical_end = [] + last_newline = True + sio = io.StringIO("".join(self.__source)) + parens = 0 + for t in tokenize.generate_tokens(sio.readline): + if t[0] in [tokenize.COMMENT, tokenize.DEDENT, + tokenize.INDENT, tokenize.NL, + tokenize.ENDMARKER]: + continue + if not parens and t[0] in [tokenize.NEWLINE, tokenize.SEMI]: + last_newline = True + logical_end.append((t[3][0] - 1, t[2][1])) + continue + if last_newline and not parens: + logical_start.append((t[2][0] - 1, t[2][1])) + last_newline = False + if t[0] == tokenize.OP: + if t[1] in '([{': + parens += 1 + elif t[1] in '}])': + parens -= 1 + return logical_start, logical_end + + def __getLogical(self, line, pos): + """ + Private method to get the logical line corresponding to the given + position. + + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return tuple of a tuple of two integers giving the start of the + logical line, another tuple of two integers giving the end + of the logical line and a list of strings with the original + source lines + """ + try: + (logical_start, logical_end) = self.__findLogical() + except (SyntaxError, tokenize.TokenError): + return None + + line = line - 1 + ls = None + le = None + for i in range(0, len(logical_start)): + x = logical_end[i] + if x[0] > line or (x[0] == line and x[1] > pos): + le = x + ls = logical_start[i] + break + if ls is None: + return None + + original = self.__source[ls[0]:le[0] + 1] + return ls, le, original + + def __getIndentWord(self): + """ + Private method to determine the indentation type. + + @return string to be used for an indentation (string) + """ + sio = io.StringIO("".join(self.__source)) + indentWord = " " # default in case of failure + try: + for token in tokenize.generate_tokens(sio.readline): + if token[0] == tokenize.INDENT: + indentWord = token[1] + break + except (SyntaxError, tokenize.TokenError): + pass + return indentWord + + def __getIndent(self, line): + """ + Private method to get the indentation string. + + @param line line to determine the indentation string from (string) + @return indentation string (string) + """ + return line.replace(line.lstrip(), "") + + def __multilineStringLines(self): + """ + Private method to determine the line numbers that are within multi line + strings and these which are part of a documentation string. + + @return tuple of a set of line numbers belonging to a multi line + string and a set of line numbers belonging to a multi line + documentation string (tuple of two set of integer) + """ + if self.__multiLineNumbers is None: + source = "".join(self.__source) + sio = io.StringIO(source) + self.__multiLineNumbers = set() + self.__docLineNumbers = set() + previousTokenType = '' + try: + for t in tokenize.generate_tokens(sio.readline): + tokenType = t[0] + startRow = t[2][0] + endRow = t[3][0] + + if (tokenType == tokenize.STRING and startRow != endRow): + if previousTokenType != tokenize.INDENT: + self.__multiLineNumbers |= set( + range(startRow, 1 + endRow)) + else: + self.__docLineNumbers |= set( + range(startRow, 1 + endRow)) + + previousTokenType = tokenType + except (SyntaxError, tokenize.TokenError): + pass + + return self.__multiLineNumbers, self.__docLineNumbers + + def __fixReindent(self, line, pos, logical): + """ + Private method to fix a badly indented line. + + This is done by adding or removing from its initial indent only. + + @param line line number of the issue (integer) + @param pos position inside line (integer) + @param logical logical line structure + @return flag indicating a change was done (boolean) + """ + assert logical + ls, _, original = logical + + rewrapper = IndentationWrapper(original) + valid_indents = rewrapper.pep8Expected() + if not rewrapper.rel_indent: + return False + + if line > ls[0]: + # got a valid continuation line number + row = line - ls[0] - 1 + # always pick the first option for this + valid = valid_indents[row] + got = rewrapper.rel_indent[row] + else: + return False + + line1 = ls[0] + row + # always pick the expected indent, for now. + indent_to = valid[0] + + if got != indent_to: + orig_line = self.__source[line1] + new_line = ' ' * (indent_to) + orig_line.lstrip() + if new_line == orig_line: + return False + else: + self.__source[line1] = new_line + return True + else: + return False + + def __fixWhitespace(self, line, offset, replacement): + """ + Private method to correct whitespace at the given offset. + + @param line line to be corrected (string) + @param offset offset within line (integer) + @param replacement replacement string (string) + @return corrected line + """ + left = line[:offset].rstrip(" \t") + right = line[offset:].lstrip(" \t") + if right.startswith("#"): + return line + else: + return left + replacement + right + + def __fixD111(self, code, line, pos): + """ + Private method to fix docstring enclosed in wrong quotes. + + Codes: D111 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + left, right = self.__source[line].split("'''", 1) + self.__source[line] = left + '"""' + right + while line < len(self.__source): + if self.__source[line].rstrip().endswith("'''"): + left, right = self.__source[line].rsplit("'''", 1) + self.__source[line] = left + '"""' + right + break + line += 1 + + return ( + 1, + self.trUtf8( + "Triple single quotes converted to triple double quotes."), + 0) + + def __fixD112(self, code, line, pos): + """ + Private method to fix docstring 'r' or 'u' in leading quotes. + + Codes: D112, D113 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + if code == "D112": + insertChar = "r" + elif code == "D113": + insertChar = "u" + else: + return (0, "", 0) + + newText = self.__getIndent(self.__source[line]) + \ + insertChar + self.__source[line].lstrip() + self.__source[line] = newText + return ( + 1, + self.trUtf8('Introductory quotes corrected to be {0}"""') + .format(insertChar), + 0) + + def __fixD121(self, code, line, pos, apply=False): + """ + Private method to fix a single line docstring on multiple lines. + + Codes: D121 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + if not self.__source[line].lstrip().startswith( + ('"""', 'r"""', 'u"""')): + # only correctly formatted docstrings will be fixed + return (0, "", 0) + + docstring = self.__source[line].rstrip() + \ + self.__source[line + 1].strip() + if docstring.endswith('"""'): + docstring += self.__getEol() + else: + docstring += self.__source[line + 2].lstrip() + self.__source[line + 2] = "" + + self.__source[line] = docstring + self.__source[line + 1] = "" + return ( + 1, + self.trUtf8("Single line docstring put on one line."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD131(self, code, line, pos): + """ + Private method to fix a docstring summary not ending with a + period. + + Codes: D131 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + newText = "" + if self.__source[line].rstrip().endswith(('"""', "'''")) and \ + self.__source[line].lstrip().startswith(('"""', 'r"""', 'u"""')): + # it is a one-liner + newText = self.__source[line].rstrip()[:-3].rstrip() + "." + \ + self.__source[line].rstrip()[-3:] + self.__getEol() + else: + if line < len(self.__source) - 1 and \ + (not self.__source[line + 1].strip() or + self.__source[line + 1].lstrip().startswith("@") or + (self.__source[line + 1].strip() in ('"""', "'''") and + not self.__source[line].lstrip().startswith("@"))): + newText = self.__source[line].rstrip() + "." + self.__getEol() + + if newText: + self.__source[line] = newText + return (1, self.trUtf8("Period added to summary line."), 0) + else: + return (0, "", 0) + + def __fixD141(self, code, line, pos, apply=False): + """ + Private method to fix a function/method docstring preceded by a + blank line. + + Codes: D141 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line - 1] = "" + return ( + 1, + self.trUtf8( + "Blank line before function/method docstring removed."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD142(self, code, line, pos, apply=False): + """ + Private method to fix a class docstring not preceded by a + blank line. + + Codes: D142 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line] = self.__getEol() + self.__source[line] + return ( + 1, + self.trUtf8("Blank line inserted before class docstring."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD143(self, code, line, pos, apply=False): + """ + Private method to fix a class docstring not followed by a + blank line. + + Codes: D143 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line] += self.__getEol() + return ( + 1, + self.trUtf8("Blank line inserted after class docstring."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD144(self, code, line, pos, apply=False): + """ + Private method to fix a docstring summary not followed by a + blank line. + + Codes: D144 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + if not self.__source[line].rstrip().endswith("."): + # only correct summary lines can be fixed here + return (0, "", 0) + + self.__source[line] += self.__getEol() + return ( + 1, + self.trUtf8("Blank line inserted after docstring summary."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD145(self, code, line, pos, apply=False): + """ + Private method to fix the last paragraph of a multi-line docstring + not followed by a blank line. + + Codes: D143 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line] = self.__getEol() + self.__source[line] + return ( + 1, + self.trUtf8("Blank line inserted after last paragraph" + " of docstring."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD221(self, code, line, pos, apply=False): + """ + Private method to fix leading and trailing quotes of docstring + not on separate lines. + + Codes: D221, D222 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + indent = self.__getIndent(self.__source[line]) + source = self.__source[line].strip() + if code == "D221": + # leading + if source.startswith(("r", "u")): + first, second = source[:4], source[4:].strip() + else: + first, second = source[:3], source[3:].strip() + else: + # trailing + first, second = source[:-3].strip(), source[-3:] + newText = indent + first + self.__getEol() + \ + indent + second + self.__getEol() + self.__source[line] = newText + if code == "D221": + msg = self.trUtf8("Leading quotes put on separate line.") + else: + msg = self.trUtf8("Trailing quotes put on separate line.") + return (1, msg, 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD242(self, code, line, pos, apply=False): + """ + Private method to fix a class or function/method docstring preceded + by a blank line. + + Codes: D242, D244 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line - 1] = "" + if code == "D242": + msg = self.trUtf8("Blank line before class docstring removed.") + else: + msg = self.trUtf8( + "Blank line before function/method docstring removed.") + return (1, msg, 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD243(self, code, line, pos, apply=False): + """ + Private method to fix a class or function/method docstring followed + by a blank line. + + Codes: D243, D245 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line + 1] = "" + if code == "D243": + msg = self.trUtf8("Blank line after class docstring removed.") + else: + msg = self.trUtf8( + "Blank line after function/method docstring removed.") + return (1, msg, 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixD247(self, code, line, pos, apply=False): + """ + Private method to fix a last paragraph of a docstring followed + by a blank line. + + Codes: D247 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + self.__source[line - 1] = "" + return ( + 1, + self.trUtf8("Blank line after last paragraph removed."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE101(self, code, line, pos): + """ + Private method to fix obsolete tab usage and indentation errors. + + Codes: E101, E111, W191 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if self.__reindenter is None: + self.__reindenter = Reindenter(self.__source) + self.__reindenter.run() + fixedLine = self.__reindenter.fixedLine(line - 1) + if fixedLine is not None and fixedLine != self.__source[line - 1]: + self.__source[line - 1] = fixedLine + if code in ["E101", "W191"]: + msg = self.trUtf8("Tab converted to 4 spaces.") + else: + msg = self.trUtf8( + "Indentation adjusted to be a multiple of four.") + return (1, msg, 0) + else: + return (0, "", 0) + + def __fixE121(self, code, line, pos, apply=False): + """ + Private method to fix the indentation of continuation lines and + closing brackets. + + Codes: E121, E124 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + logical = self.__getLogical(line, pos) + if logical: + # Fix by adjusting initial indent level. + changed = self.__fixReindent(line, pos, logical) + if changed: + if code == "E121": + msg = self.trUtf8( + "Indentation of continuation line corrected.") + elif code == "E124": + msg = self.trUtf8( + "Indentation of closing bracket corrected.") + return (1, msg, 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stackLogical.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE122(self, code, line, pos, apply=False): + """ + Private method to fix a missing indentation of continuation lines. + + Codes: E122 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + logical = self.__getLogical(line, pos) + if logical: + # Fix by adding an initial indent. + modified = self.__fixReindent(line, pos, logical) + if not modified: + # fall back to simple method + line = line - 1 + text = self.__source[line] + indentation = self.__getIndent(text) + self.__source[line] = indentation + \ + self.__indentWord + text.lstrip() + return ( + 1, + self.trUtf8( + "Missing indentation of continuation line corrected."), + 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stackLogical.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE123(self, code, line, pos, apply=False): + """ + Private method to fix the indentation of a closing bracket lines. + + Codes: E123 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + logical = self.__getLogical(line, pos) + if logical: + # Fix by deleting whitespace to the correct level. + logicalLines = logical[2] + row = line - 1 + text = self.__source[row] + newText = self.__getIndent(logicalLines[0]) + text.lstrip() + if newText == text: + # fall back to slower method + changed = self.__fixReindent(line, pos, logical) + else: + self.__source[row] = newText + changed = True + if changed: + return (1, self.trUtf8( + "Closing bracket aligned to opening bracket."), + 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stackLogical.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE125(self, code, line, pos, apply=False): + """ + Private method to fix the indentation of continuation lines not + distinguishable from next logical line. + + Codes: E125 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + logical = self.__getLogical(line, pos) + if logical: + # Fix by adjusting initial indent level. + modified = self.__fixReindent(line, pos, logical) + if not modified: + row = line - 1 + text = self.__source[row] + self.__source[row] = self.__getIndent(text) + \ + self.__indentWord + text.lstrip() + return (1, self.trUtf8("Indentation level changed."), 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stackLogical.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE126(self, code, line, pos, apply=False): + """ + Private method to fix over-indented/under-indented hanging + indentation. + + Codes: E126, E133 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + logical = self.__getLogical(line, pos) + if logical: + # Fix by deleting whitespace to the left. + logicalLines = logical[2] + row = line - 1 + text = self.__source[row] + newText = self.__getIndent(logicalLines[0]) + \ + self.__indentWord + text.lstrip() + if newText == text: + # fall back to slower method + changed = self.__fixReindent(line, pos, logical) + else: + self.__source[row] = newText + changed = True + if changed: + return (1, self.trUtf8( + "Indentation level of hanging indentation changed."), + 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stackLogical.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE127(self, code, line, pos, apply=False): + """ + Private method to fix over/under indented lines. + + Codes: E127, E128 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + logical = self.__getLogical(line, pos) + if logical: + # Fix by inserting/deleting whitespace to the correct level. + logicalLines = logical[2] + row = line - 1 + text = self.__source[row] + newText = text + + if logicalLines[0].rstrip().endswith('\\'): + newText = self.__getIndent(logicalLines[0]) + \ + self.__indentWord + text.lstrip() + else: + startIndex = None + for symbol in '([{': + if symbol in logicalLines[0]: + foundIndex = logicalLines[0].find(symbol) + 1 + if startIndex is None: + startIndex = foundIndex + else: + startIndex = min(startIndex, foundIndex) + + if startIndex is not None: + newText = startIndex * ' ' + text.lstrip() + + if newText == text: + # fall back to slower method + changed = self.__fixReindent(line, pos, logical) + else: + self.__source[row] = newText + changed = True + if changed: + return (1, self.trUtf8("Visual indentation corrected."), 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stackLogical.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE201(self, code, line, pos): + """ + Private method to fix extraneous whitespace. + + Codes: E201, E202, E203, E211 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + text = self.__source[line] + + if '"""' in text or "'''" in text or text.rstrip().endswith('\\'): + return (0, "", 0) + + newText = self.__fixWhitespace(text, pos, '') + if newText == text: + return (0, "", 0) + + self.__source[line] = newText + return (1, self.trUtf8("Extraneous whitespace removed."), 0) + + def __fixE221(self, code, line, pos): + """ + Private method to fix extraneous whitespace around operator or + keyword. + + Codes: E221, E222, E223, E224, E225, E226, E227, E228, E241, + E242, E271, E272, E273, E274). + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + text = self.__source[line] + + if '"""' in text or "'''" in text or text.rstrip().endswith('\\'): + return (0, "", 0) + + newText = self.__fixWhitespace(text, pos, ' ') + if newText == text: + return (0, "", 0) + + self.__source[line] = newText + if code in ["E225", "E226", "E227", "E228"]: + return (1, self.trUtf8("Missing whitespace added."), 0) + else: + return (1, self.trUtf8("Extraneous whitespace removed."), 0) + + def __fixE231(self, code, line, pos): + """ + Private method to fix missing whitespace after ',;:'. + + Codes: E231 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + pos = pos + 1 + self.__source[line] = self.__source[line][:pos] + \ + " " + self.__source[line][pos:] + return (1, self.trUtf8("Missing whitespace added."), 0) + + def __fixE251(self, code, line, pos): + """ + Private method to fix extraneous whitespace around keyword and + default parameter equals. + + Codes: E251 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + text = self.__source[line] + + # This is necessary since pep8 sometimes reports columns that goes + # past the end of the physical line. This happens in cases like, + # foo(bar\n=None) + col = min(pos, len(text) - 1) + if text[col].strip(): + newText = text + else: + newText = text[:col].rstrip() + text[col:].lstrip() + + # There could be an escaped newline + # + # def foo(a=\ + # 1) + if newText.endswith(('=\\\n', '=\\\r\n', '=\\\r')): + self.__source[line] = newText.rstrip("\n\r \t\\") + self.__source[line + 1] = self.__source[line + 1].lstrip() + else: + self.__source[line] = newText + return (1, self.trUtf8("Extraneous whitespace removed."), 0) + + def __fixE261(self, code, line, pos): + """ + Private method to fix whitespace before or after inline comment. + + Codes: E261, E262 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + text = self.__source[line] + left = text[:pos].rstrip(' \t#') + right = text[pos:].lstrip(' \t#') + newText = left + (" # " + right if right.strip() else right) + self.__source[line] = newText + return (1, self.trUtf8("Whitespace around comment sign corrected."), 0) + + def __fixE301(self, code, line, pos, apply=False): + """ + Private method to fix the need for one blank line. + + Codes: E301 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + self.__source.insert(line - 1, self.__getEol()) + return (1, self.trUtf8("One blank line inserted."), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE302(self, code, line, pos, apply=False): + """ + Private method to fix the need for two blank lines. + + Codes: E302 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + # count blank lines + index = line - 1 + blanks = 0 + while index: + if self.__source[index - 1].strip() == "": + blanks += 1 + index -= 1 + else: + break + delta = blanks - 2 + + line -= 1 + if delta < 0: + # insert blank lines (one or two) + while delta < 0: + self.__source.insert(line, self.__getEol()) + delta += 1 + changed = True + elif delta > 0: + # delete superfluous blank lines + while delta > 0: + del self.__source[line - 1] + line -= 1 + delta -= 1 + changed = True + else: + changed = False + + if changed: + if delta < 0: + msg = self.trUtf8( + "%n blank line(s) inserted.", "", -delta) + elif delta > 0: + msg = self.trUtf8( + "%n superfluous lines removed", "", delta) + else: + msg = "" + return (1, msg, 0) + return (0, "", 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE303(self, code, line, pos, apply=False): + """ + Private method to fix superfluous blank lines. + + Codes: E303 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + index = line - 3 + while index: + if self.__source[index].strip() == "": + del self.__source[index] + index -= 1 + else: + break + return (1, self.trUtf8("Superfluous blank lines removed."), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE304(self, code, line, pos, apply=False): + """ + Private method to fix superfluous blank lines after a function + decorator. + + Codes: E304 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + index = line - 2 + while index: + if self.__source[index].strip() == "": + del self.__source[index] + index -= 1 + else: + break + return (1, self.trUtf8( + "Superfluous blank lines after function decorator removed."), + 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE401(self, code, line, pos, apply=False): + """ + Private method to fix multiple imports on one line. + + Codes: E401 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + text = self.__source[line] + if not text.lstrip().startswith("import"): + return (0, "", 0) + + # pep8 (1.3.1) reports false positive if there is an import + # statement followed by a semicolon and some unrelated + # statement with commas in it. + if ';' in text: + return (0, "", 0) + + newText = text[:pos].rstrip("\t ,") + self.__getEol() + \ + self.__getIndent(text) + "import " + text[pos:].lstrip("\t ,") + self.__source[line] = newText + return (1, self.trUtf8("Imports were put on separate lines."), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE501(self, code, line, pos, apply=False): + """ + Private method to fix the long lines by breaking them. + + Codes: E501 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + multilineStringLines, docStringLines = \ + self.__multilineStringLines() + isDocString = line in docStringLines + line = line - 1 + text = self.__source[line] + if line > 0: + prevText = self.__source[line - 1] + else: + prevText = "" + if line < len(self.__source) - 1: + nextText = self.__source[line + 1] + else: + nextText = "" + shortener = LineShortener( + text, prevText, nextText, + maxLength=self.__maxLineLength, eol=self.__getEol(), + indentWord=self.__indentWord, isDocString=isDocString) + changed, newText, newNextText = shortener.shorten() + if changed: + if newText != text: + self.__source[line] = newText + if newNextText and newNextText != nextText: + if newNextText == " ": + newNextText = "" + self.__source[line + 1] = newNextText + return (1, self.trUtf8("Long lines have been shortened."), 0) + else: + return (0, "", 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE502(self, code, line, pos): + """ + Private method to fix redundant backslash within brackets. + + Codes: E502 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + self.__source[line - 1] = \ + self.__source[line - 1].rstrip("\n\r \t\\") + self.__getEol() + return (1, self.trUtf8("Redundant backslash in brackets removed."), 0) + + def __fixE701(self, code, line, pos, apply=False): + """ + Private method to fix colon-separated compound statements. + + Codes: E701 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + text = self.__source[line] + pos = pos + 1 + + newText = text[:pos] + self.__getEol() + self.__getIndent(text) + \ + self.__indentWord + text[pos:].lstrip("\n\r \t\\") + \ + self.__getEol() + self.__source[line] = newText + return (1, self.trUtf8("Compound statement corrected."), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE702(self, code, line, pos, apply=False): + """ + Private method to fix semicolon-separated compound statements. + + Codes: E702, E703 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + text = self.__source[line] + + if text.rstrip().endswith("\\"): + # normalize '1; \\\n2' into '1; 2' + self.__source[line] = text.rstrip("\n\r \t\\") + self.__source[line + 1] = self.__source[line + 1].lstrip() + elif text.rstrip().endswith(";"): + self.__source[line] = text.rstrip("\n\r \t;") + self.__getEol() + else: + first = text[:pos].rstrip("\n\r \t;") + self.__getEol() + second = text[pos:].lstrip("\n\r \t;") + self.__source[line] = first + self.__getIndent(text) + second + return (1, self.trUtf8("Compound statement corrected."), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixE711(self, code, line, pos): + """ + Private method to fix comparison with None. + + Codes: E711, E712 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + line = line - 1 + text = self.__source[line] + + rightPos = pos + 2 + if rightPos >= len(text): + return (0, "", 0) + + left = text[:pos].rstrip() + center = text[pos:rightPos] + right = text[rightPos:].lstrip() + + if not right.startswith(("None", "True", "False")): + return (0, "", 0) + + if center.strip() == "==": + center = "is" + elif center.strip() == "!=": + center = "is not" + else: + return (0, "", 0) + + self.__source[line] = " ".join([left, center, right]) + return (1, self.trUtf8("Comparison to None/True/False corrected."), 0) + + def __fixN804(self, code, line, pos, apply=False): + """ + Private method to fix a wrong first argument of normal and + class methods. + + Codes: N804, N805 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + text = self.__source[line] + if code == "N804": + arg = "cls" + else: + arg = "self" + + if text.rstrip().endswith("("): + newText = text + self.__getIndent(text) + \ + self.__indentWord + arg + "," + self.__getEol() + else: + index = text.find("(") + 1 + left = text[:index] + right = text[index:] + if right.startswith(")"): + center = arg + else: + center = arg + ", " + newText = left + center + right + self.__source[line] = newText + return (1, self.trUtf8("'{0}' argument added.").format(arg), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixN806(self, code, line, pos, apply=False): + """ + Private method to fix a wrong first argument of static methods. + + Codes: N806 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @keyparam apply flag indicating, that the fix should be applied + (boolean) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + if apply: + line = line - 1 + text = self.__source[line] + index = text.find("(") + 1 + left = text[:index] + right = text[index:] + + if right.startswith(("cls", "self")): + # cls or self are on the definition line + if right.startswith("cls"): + right = right[3:] + arg = "cls" + else: + right = right[4:] + arg = "self" + right = right.lstrip(", ") + newText = left + right + self.__source[line] = newText + else: + # they are on the next line + line = line + 1 + text = self.__source[line] + indent = self.__getIndent(text) + right = text.lstrip() + if right.startswith("cls"): + right = right[3:] + arg = "cls" + else: + right = right[4:] + arg = "self" + right = right.lstrip(", ") + if right.startswith("):"): + # merge with previous line + self.__source[line - 1] = \ + self.__source[line - 1].rstrip() + right + self.__source[line] = "" + else: + self.__source[line] = indent + right + + return (1, self.trUtf8("'{0}' argument removed.").format(arg), 0) + else: + id = self.__getID() + self.__stack.append((id, code, line, pos)) + return (-1, "", id) + + def __fixW291(self, code, line, pos): + """ + Private method to fix trailing whitespace. + + Codes: W291, W293 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + self.__source[line - 1] = re.sub(r'[\t ]+(\r?)$', r"\1", + self.__source[line - 1]) + return (1, self.trUtf8("Whitespace stripped from end of line."), 0) + + def __fixW292(self, code, line, pos): + """ + Private method to fix a missing newline at the end of file. + + Codes: W292 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + self.__source[line - 1] += self.__getEol() + return (1, self.trUtf8("newline added to end of file."), 0) + + def __fixW391(self, code, line, pos): + """ + Private method to fix trailing blank lines. + + Codes: W391 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + index = line - 1 + while index: + if self.__source[index].strip() == "": + del self.__source[index] + index -= 1 + else: + break + return (1, self.trUtf8( + "Superfluous trailing blank lines removed from end of file."), 0) + + def __fixW603(self, code, line, pos): + """ + Private method to fix the not equal notation. + + Codes: W603 + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return value indicating an applied/deferred fix (-1, 0, 1), + a message for the fix (string) and an ID for a deferred + fix (integer) + """ + self.__source[line - 1] = self.__source[line - 1].replace("<>", "!=") + return (1, self.trUtf8("'<>' replaced by '!='."), 0) + + +class Reindenter(object): + """ + Class to reindent badly-indented code to uniformly use four-space + indentation. + + Released to the public domain, by Tim Peters, 03 October 2000. + """ + def __init__(self, sourceLines): + """ + Constructor + + @param sourceLines list of source lines including eol marker + (list of string) + """ + # Raw file lines. + self.raw = sourceLines + self.after = [] + + # File lines, rstripped & tab-expanded. Dummy at start is so + # that we can use tokenize's 1-based line numbering easily. + # Note that a line is all-blank iff it's "\n". + self.lines = [line.rstrip().expandtabs() + "\n" + for line in self.raw] + self.lines.insert(0, None) + self.index = 1 # index into self.lines of next line + + # List of (lineno, indentlevel) pairs, one for each stmt and + # comment line. indentlevel is -1 for comment lines, as a + # signal that tokenize doesn't know what to do about them; + # indeed, they're our headache! + self.stats = [] + + def run(self): + """ + Public method to run the re-indenter. + + @return flag indicating that a change was done (boolean) + """ + try: + stats = self.__genStats(tokenize.generate_tokens(self.getline)) + except (SyntaxError, tokenize.TokenError): + return False + + # Remove trailing empty lines. + lines = self.lines + while lines and lines[-1] == "\n": + lines.pop() + # Sentinel. + stats.append((len(lines), 0)) + # Map count of leading spaces to # we want. + have2want = {} + # Program after transformation. + after = self.after = [] + # Copy over initial empty lines -- there's nothing to do until + # we see a line with *something* on it. + i = stats[0][0] + after.extend(lines[1:i]) + for i in range(len(stats) - 1): + thisstmt, thislevel = stats[i] + nextstmt = stats[i + 1][0] + have = self.__getlspace(lines[thisstmt]) + want = thislevel * 4 + if want < 0: + # A comment line. + if have: + # An indented comment line. If we saw the same + # indentation before, reuse what it most recently + # mapped to. + want = have2want.get(have, -1) + if want < 0: + # Then it probably belongs to the next real stmt. + for j in range(i + 1, len(stats) - 1): + jline, jlevel = stats[j] + if jlevel >= 0: + if have == self.__getlspace(lines[jline]): + want = jlevel * 4 + break + if want < 0: # Maybe it's a hanging + # comment like this one, + # in which case we should shift it like its base + # line got shifted. + for j in range(i - 1, -1, -1): + jline, jlevel = stats[j] + if jlevel >= 0: + want = \ + have + \ + self.__getlspace(after[jline - 1]) - \ + self.__getlspace(lines[jline]) + break + if want < 0: + # Still no luck -- leave it alone. + want = have + else: + want = 0 + assert want >= 0 + have2want[have] = want + diff = want - have + if diff == 0 or have == 0: + after.extend(lines[thisstmt:nextstmt]) + else: + for line in lines[thisstmt:nextstmt]: + if diff > 0: + if line == "\n": + after.append(line) + else: + after.append(" " * diff + line) + else: + remove = min(self.__getlspace(line), -diff) + after.append(line[remove:]) + return self.raw != self.after + + def fixedLine(self, line): + """ + Public method to get a fixed line. + + @param line number of the line to retrieve (integer) + @return fixed line (string) + """ + if line < len(self.after): + return self.after[line] + + def getline(self): + """ + Public method to get a line of text for tokenize. + + @return line of text (string) + """ + if self.index >= len(self.lines): + line = "" + else: + line = self.lines[self.index] + self.index += 1 + return line + + def __genStats(self, tokens): + """ + Private method to generate the re-indent statistics. + + @param tokens tokens generator (tokenize._tokenize) + @return reference to the generated statistics + """ + find_stmt = True # next token begins a fresh stmt? + level = 0 # current indent level + stats = [] + + for t in tokens: + token_type = t[0] + sline = t[2][0] + line = t[4] + + if token_type == tokenize.NEWLINE: + # A program statement, or ENDMARKER, will eventually follow, + # after some (possibly empty) run of tokens of the form + # (NL | COMMENT)* (INDENT | DEDENT+)? + self.find_stmt = True + + elif token_type == tokenize.INDENT: + find_stmt = True + level += 1 + + elif token_type == tokenize.DEDENT: + find_stmt = True + level -= 1 + + elif token_type == tokenize.COMMENT: + if find_stmt: + stats.append((sline, -1)) + # but we're still looking for a new stmt, so leave + # find_stmt alone + + elif token_type == tokenize.NL: + pass + + elif find_stmt: + # This is the first "real token" following a NEWLINE, so it + # must be the first token of the next program statement, or an + # ENDMARKER. + find_stmt = False + if line: # not endmarker + stats.append((sline, level)) + + return stats + + def __getlspace(self, line): + """ + Private method to count number of leading blanks. + + @param line line to check (string) + @return number of leading blanks (integer) + """ + i = 0 + n = len(line) + while i < n and line[i] == " ": + i += 1 + return i + + +class IndentationWrapper(object): + """ + Class used by fixers dealing with indentation. + + Each instance operates on a single logical line. + """ + SKIP_TOKENS = frozenset([ + tokenize.COMMENT, tokenize.NL, tokenize.INDENT, + tokenize.DEDENT, tokenize.NEWLINE, tokenize.ENDMARKER + ]) + + def __init__(self, physical_lines): + """ + Constructor + + @param physical_lines list of physical lines to operate on + (list of strings) + """ + self.lines = physical_lines + self.tokens = [] + self.rel_indent = None + sio = io.StringIO(''.join(physical_lines)) + for t in tokenize.generate_tokens(sio.readline): + if not len(self.tokens) and t[0] in self.SKIP_TOKENS: + continue + if t[0] != tokenize.ENDMARKER: + self.tokens.append(t) + + self.logical_line = self.__buildTokensLogical(self.tokens) + + def __buildTokensLogical(self, tokens): + """ + Private method to build a logical line from a list of tokens. + + @param tokens list of tokens as generated by tokenize.generate_tokens + @return logical line (string) + """ + # from pep8.py with minor modifications + logical = [] + previous = None + for t in tokens: + token_type, text = t[0:2] + if token_type in self.SKIP_TOKENS: + continue + if previous: + end_line, end = previous[3] + start_line, start = t[2] + if end_line != start_line: # different row + prev_text = self.lines[end_line - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + elif end != start: # different column + fill = self.lines[end_line - 1][end:start] + logical.append(fill) + logical.append(text) + previous = t + logical_line = ''.join(logical) + assert logical_line.lstrip() == logical_line + assert logical_line.rstrip() == logical_line + return logical_line + + def pep8Expected(self): + """ + Public method to replicate logic in pep8.py, to know what level to + indent things to. + + @return list of lists, where each list represents valid indent levels + for the line in question, relative from the initial indent. However, + the first entry is the indent level which was expected. + """ + # What follows is an adjusted version of + # pep8.py:continuation_line_indentation. All of the comments have been + # stripped and the 'yield' statements replaced with 'pass'. + if not self.tokens: + return + + first_row = self.tokens[0][2][0] + nrows = 1 + self.tokens[-1][2][0] - first_row + + # here are the return values + valid_indents = [list()] * nrows + indent_level = self.tokens[0][2][1] + valid_indents[0].append(indent_level) + + if nrows == 1: + # bug, really. + return valid_indents + + indent_next = self.logical_line.endswith(':') + + row = depth = 0 + parens = [0] * nrows + self.rel_indent = rel_indent = [0] * nrows + indent = [indent_level] + indent_chances = {} + last_indent = (0, 0) + last_token_multiline = None + + for token_type, text, start, end, line in self.tokens: + newline = row < start[0] - first_row + if newline: + row = start[0] - first_row + newline = (not last_token_multiline and + token_type not in (tokenize.NL, tokenize.NEWLINE)) + + if newline: + # This is where the differences start. Instead of looking at + # the line and determining whether the observed indent matches + # our expectations, we decide which type of indentation is in + # use at the given indent level, and return the offset. This + # algorithm is susceptible to "carried errors", but should + # through repeated runs eventually solve indentation for + # multiline expressions. + + if depth: + for open_row in range(row - 1, -1, -1): + if parens[open_row]: + break + else: + open_row = 0 + + # That's all we get to work with. This code attempts to + # "reverse" the below logic, and place into the valid indents + # list + vi = [] + add_second_chances = False + if token_type == tokenize.OP and text in ']})': + # this line starts with a closing bracket, so it needs to + # be closed at the same indent as the opening one. + if indent[depth]: + # hanging indent + vi.append(indent[depth]) + else: + # visual indent + vi.append(indent_level + rel_indent[open_row]) + elif depth and indent[depth]: + # visual indent was previously confirmed. + vi.append(indent[depth]) + add_second_chances = True + elif depth and True in indent_chances.values(): + # visual indent happened before, so stick to + # visual indent this time. + if depth > 1 and indent[depth - 1]: + vi.append(indent[depth - 1]) + else: + # stupid fallback + vi.append(indent_level + 4) + add_second_chances = True + elif not depth: + vi.append(indent_level + 4) + else: + # must be in hanging indent + hang = rel_indent[open_row] + 4 + vi.append(indent_level + hang) + + # about the best we can do without look-ahead + if (indent_next and vi[0] == indent_level + 4 and + nrows == row + 1): + vi[0] += 4 + + if add_second_chances: + # visual indenters like to line things up. + min_indent = vi[0] + for col, what in indent_chances.items(): + if col > min_indent and ( + what is True or + (what == str and token_type == tokenize.STRING) or + (what == text and token_type == tokenize.OP) + ): + vi.append(col) + vi = sorted(vi) + + valid_indents[row] = vi + + # Returning to original continuation_line_indentation() from + # pep8. + visual_indent = indent_chances.get(start[1]) + last_indent = start + rel_indent[row] = pep8.expand_indent(line) - indent_level + hang = rel_indent[row] - rel_indent[open_row] + + if token_type == tokenize.OP and text in ']})': + pass + elif visual_indent is True: + if not indent[depth]: + indent[depth] = start[1] + + # line altered: comments shouldn't define a visual indent + if parens[row] and not indent[depth] and token_type not in ( + tokenize.NL, tokenize.COMMENT + ): + indent[depth] = start[1] + indent_chances[start[1]] = True + elif token_type == tokenize.STRING or text in ( + 'u', 'ur', 'b', 'br' + ): + indent_chances[start[1]] = str + + if token_type == tokenize.OP: + if text in '([{': + depth += 1 + indent.append(0) + parens[row] += 1 + elif text in ')]}' and depth > 0: + prev_indent = indent.pop() or last_indent[1] + for d in range(depth): + if indent[d] > prev_indent: + indent[d] = 0 + for ind in list(indent_chances): + if ind >= prev_indent: + del indent_chances[ind] + depth -= 1 + if depth and indent[depth]: # modified + indent_chances[indent[depth]] = True + for idx in range(row, -1, -1): + if parens[idx]: + parens[idx] -= 1 + break + assert len(indent) == depth + 1 + if start[1] not in indent_chances: + indent_chances[start[1]] = text + + last_token_multiline = (start[0] != end[0]) + + return valid_indents + + +class LineShortener(object): + """ + Class used to shorten lines to a given maximum of characters. + """ + def __init__(self, curLine, prevLine, nextLine, maxLength=79, eol="\n", + indentWord=" ", isDocString=False): + """ + Constructor + + @param curLine text to work on (string) + @param prevLine line before the text to work on (string) + @param nextLine line after the text to work on (string) + @keyparam maxLength maximum allowed line length (integer) + @keyparam eol eond-of-line marker (string) + @keyparam indentWord string used for indentation (string) + @keyparam isDocString flag indicating that the line belongs to + a documentation string (boolean) + """ + self.__text = curLine + self.__prevText = prevLine + self.__nextText = nextLine + self.__maxLength = maxLength + self.__eol = eol + self.__indentWord = indentWord + self.__isDocString = isDocString + + def shorten(self): + """ + Public method to shorten the line wrapped by the class instance. + + @return tuple of a flag indicating successful shortening, the + shortened line and the changed next line (boolean, string, string) + """ + # 1. check for comment + if self.__text.lstrip().startswith('#'): + lastComment = True + if self.__nextText.lstrip().startswith('#'): + lastComment = False + + # Wrap commented lines. + newText = self.__shortenComment(lastComment) + if newText == self.__text: + return False, "", "" + else: + return True, newText, "" + elif '#' in self.__text: + pos = self.__text.rfind("#") + newText = self.__text[:pos].rstrip() + self.__eol + \ + self.__getIndent(self.__text) + self.__text[pos:] + if newText == self.__text: + return False, "", "" + else: + return True, newText, "" + + # Do multi line doc strings + if self.__isDocString: + source = self.__text.rstrip() + blank = source.rfind(" ") + while blank > self.__maxLength and blank != -1: + blank = source.rfind(" ", 0, blank) + if blank == -1: + # Cannot break + return False, "", "" + else: + first = self.__text[:blank] + second = self.__text[blank:].lstrip() + if self.__nextText.strip(): + if self.__nextText.lstrip().startswith("@"): + # eric doc comment + # create a new line and indent it + newText = first + self.__eol + \ + self.__getIndent(first) + self.__indentWord + \ + second + newNext = "" + else: + newText = first + self.__eol + newNext = self.__getIndent(self.__nextText) + \ + second.rstrip() + " " + self.__nextText.lstrip() + else: + # empty line, add a new line + newText = first + self.__eol + self.__getIndent(first) + \ + second + newNext = "" + return True, newText, newNext + + indent = self.__getIndent(self.__text) + source = self.__text[len(indent):] + assert source.lstrip() == source + sio = io.StringIO(source) + + # Check for multi line string. + try: + tokens = list(tokenize.generate_tokens(sio.readline)) + except (SyntaxError, tokenize.TokenError): + if source.rstrip().endswith("\\"): + # just join the continuation line and let the next run + # handle it once it tokenizes ok + newText = indent + source.rstrip()[:-1].rstrip() + " " + \ + self.__nextText.lstrip() + if indent: + newNext = indent + else: + newNext = " " + return True, newText, newNext + else: + multilineCandidate = self.__breakMultiline() + if multilineCandidate: + return True, multilineCandidate[0], multilineCandidate[1] + else: + return False, "", "" + + # Handle statements by putting the right hand side on a line by itself. + # This should let the next pass shorten it. + if source.startswith('return '): + newText = ( + indent + + 'return (' + + self.__eol + + indent + self.__indentWord + re.sub('^return ', '', source) + + indent + ')' + self.__eol + ) + return True, newText, "" + + candidates = self.__shortenLine(tokens, source, indent) + if candidates: + candidates = list(sorted( + set(candidates).union([self.__text]), + key=lambda x: self.__lineShorteningRank(x))) + if candidates[0] == self.__text: + return False, "", "" + return True, candidates[0], "" + + source = self.__text + rs = source.rstrip() + if rs.endswith(("'", '"')) and " " in source: + if rs.endswith(('"""', "'''")): + quote = rs[-3:] + else: + quote = rs[-1] + blank = source.rfind(" ") + maxLen = self.__maxLength - 2 - len(quote) + while blank > maxLen and blank != -1: + blank = source.rfind(" ", 0, blank) + if blank != -1: + if source[blank + 1:].startswith(quote): + first = source[:maxLen] + second = source[maxLen:] + else: + first = source[:blank] + second = source[blank + 1:] + return ( + True, + first + quote + " \\" + self.__eol + + indent + self.__indentWord + quote + second, + "") + else: + # Cannot break + return False, "", "" + + return False, "", "" + + def __shortenComment(self, isLast): + """ + Private method to shorten a comment line. + + @param isLast flag indicating, that the line is the last comment line + (boolean) + @return shortened comment line (string) + """ + if len(self.__text) <= self.__maxLength: + return self.__text + + newText = self.__text.rstrip() + + # PEP 8 recommends 72 characters for comment text. + indentation = self.__getIndent(newText) + '# ' + maxLength = min(self.__maxLength, + len(indentation) + 72) + + MIN_CHARACTER_REPEAT = 5 + if len(newText) - len(newText.rstrip(newText[-1])) >= \ + MIN_CHARACTER_REPEAT and \ + not newText[-1].isalnum(): + # Trim comments that end with things like --------- + return newText[:maxLength] + self.__eol + elif isLast and re.match(r"\s*#+\s*\w+", newText): + import textwrap + splitLines = textwrap.wrap(newText.lstrip(" \t#"), + initial_indent=indentation, + subsequent_indent=indentation, + width=maxLength, + break_long_words=False, + break_on_hyphens=False) + return self.__eol.join(splitLines) + self.__eol + else: + return newText + self.__eol + + def __breakMultiline(self): + """ + Private method to break multi line strings. + + @return tuple of the shortened line and the changed next line + (string, string) + """ + indentation = self.__getIndent(self.__text) + + # Handle special case. + for symbol in '([{': + # Only valid if symbol is not on a line by itself. + if ( + symbol in self.__text and + self.__text.strip() != symbol and + self.__text.rstrip().endswith((',', '%')) + ): + index = 1 + self.__text.find(symbol) + + if index <= len(self.__indentWord) + len(indentation): + continue + + if self.__isProbablyInsideStringOrComment( + self.__text, index - 1): + continue + + return (self.__text[:index].rstrip() + self.__eol + + indentation + self.__indentWord + + self.__text[index:].lstrip(), "") + + newText = self.__text + newNext = self.__nextText + blank = newText.rfind(" ") + while blank > self.__maxLength and blank != -1: + blank = newText.rfind(" ", 0, blank) + if blank != -1: + first = self.__text[:blank] + second = self.__text[blank:].strip() + if newNext.strip(): + newText = first + self.__eol + if second.endswith(")"): + # don't merge with next line + newText += self.__getIndent(newText) + second + self.__eol + newNext = "" + else: + newNext = self.__getIndent(newNext) + \ + second + " " + newNext.lstrip() + else: + # empty line, add a new line + newText = first + self.__eol + newNext = self.__getIndent(newNext) + \ + second + self.__eol + newNext.lstrip() + return newText, newNext + else: + return None + + def __isProbablyInsideStringOrComment(self, line, index): + """ + Private method to check, if the given string might be inside a string + or comment. + + @param line line to check (string) + @param index position inside line to check (integer) + @return flag indicating the possibility of being inside a string + or comment + """ + # Check against being in a string. + for quote in ['"', "'"]: + pos = line.find(quote) + if pos != -1 and pos <= index: + return True + + # Check against being in a comment. + pos = line.find('#') + if pos != -1 and pos <= index: + return True + + return False + + def __shortenLine(self, tokens, source, indent): + """ + Private method to shorten a line of code at an operator. + + @param tokens tokens of the line as generated by tokenize + (list of token) + @param source code string to work at (string) + @param indent indentation string of the code line (string) + @return list of candidates (list of string) + """ + candidates = [] + + for tkn in tokens: + tokenType = tkn[0] + tokenString = tkn[1] + + if ( + tokenType == tokenize.COMMENT and + not self.__prevText.rstrip().endswith('\\') + ): + # Move inline comments to previous line. + offset = tkn[2][1] + first = source[:offset] + second = source[offset:] + candidates.append( + indent + second.strip() + self.__eol + + indent + first.strip() + self.__eol) + elif tokenType == tokenize.OP and tokenString != '=': + # Don't break on '=' after keyword as this violates PEP 8. + + assert tokenType != tokenize.INDENT + + offset = tkn[2][1] + 1 + first = source[:offset] + + secondIndent = indent + if first.rstrip().endswith('('): + secondIndent += self.__indentWord + elif '(' in first: + secondIndent += ' ' * (1 + first.find('(')) + else: + secondIndent += self.__indentWord + + second = (secondIndent + source[offset:].lstrip()) + if not second.strip(): + continue + + # Do not begin a line with a comma + if second.lstrip().startswith(','): + continue + + # Do end a line with a dot + if first.rstrip().endswith('.'): + continue + + if tokenString in '+-*/,': + newText = first + ' \\' + self.__eol + second + else: + newText = first + self.__eol + second + + # Only fix if syntax is okay. + if self.__checkSyntax(self.__normalizeMultiline(newText)): + candidates.append(indent + newText) + + return candidates + + def __normalizeMultiline(self, text): + """ + Private method to remove multiline-related code that will cause syntax + error. + + @param text code line to work on (string) + @return normalized code line (string) + """ + for quote in '\'"': + dictPattern = r"^{q}[^{q}]*{q} *: *".format(q=quote) + if re.match(dictPattern, text): + if not text.strip().endswith('}'): + text += '}' + return '{' + text + + if text.startswith('def ') and text.rstrip().endswith(':'): + # Do not allow ':' to be alone. That is invalid. + splitText = [item.strip() for item in text.split(self.__eol)] + if ':' not in splitText and 'def' not in splitText: + return text[len('def'):].strip().rstrip(':') + + return text + + def __lineShorteningRank(self, candidate): + """ + Private method to rank a candidate. + + @param candidate candidate line to rank (string) + @return rank of the candidate (integer) + """ + rank = 0 + if candidate.strip(): + if candidate == self.__text: + # give the original a disadvantage + rank += 50 + + lines = candidate.split(self.__eol) + + offset = 0 + if lines[0].rstrip()[-1] not in '([{': + for symbol in '([{': + offset = max(offset, 1 + lines[0].find(symbol)) + + maxLength = max([offset + len(x.strip()) for x in lines]) + rank += maxLength + rank += len(lines) + + badStartingSymbol = { + '(': ')', + '[': ']', + '{': '}'}.get(lines[0][-1], None) + + if len(lines) > 1: + if (badStartingSymbol and + lines[1].lstrip().startswith(badStartingSymbol)): + rank += 20 + + if re.match(r".*[+\-\*/] \($", lines[0]): + # "1 * (\n" is ugly as hell. + rank += 100 + + for currentLine in lines: + for badStart in ['.', '%', '+', '-', '/']: + if currentLine.startswith(badStart): + rank += 100 + + for ending in '([{': + # Avoid lonely opening. They result in longer lines. + if currentLine.endswith(ending) and \ + len(currentLine.strip()) <= len(self.__indentWord): + rank += 100 + + if currentLine.endswith('%'): + rank -= 20 + + # Try to break list comprehensions at the "for". + if currentLine.lstrip().startswith('for'): + rank -= 50 + + rank += 10 * self.__countUnbalancedBrackets(currentLine) + else: + rank = 100000 + + return max(0, rank) + + def __countUnbalancedBrackets(self, line): + """ + Private method to determine the number of unmatched open/close + brackets. + + @param line line to work at (string) + @return number of unmatched open/close brackets (integer) + """ + count = 0 + for opening, closing in ['()', '[]', '{}']: + count += abs(line.count(opening) - line.count(closing)) + + return count + + def __getIndent(self, line): + """ + Private method to get the indentation string. + + @param line line to determine the indentation string from (string) + @return indentation string (string) + """ + # copied from CodeStyleFixer + return line.replace(line.lstrip(), "") + + def __checkSyntax(self, code): + """ + Private method to check the syntax of the given code fragment. + + @param code code fragment to check (string) + @return flag indicating syntax is ok (boolean) + """ + code = code.replace("\r\n", "\n").replace("\r", "\n") + try: + return compile(code, '<string>', 'exec') + except (SyntaxError, TypeError, UnicodeDecodeError): + return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog showing statistical data for the last code +style checker run. +""" + +from PyQt4.QtCore import Qt, QCoreApplication +from PyQt4.QtGui import QDialog, QTreeWidgetItem + +from . import pep8 +from .NamingStyleChecker import NamingStyleChecker +from .DocStyleChecker import DocStyleChecker + +from .Ui_CodeStyleStatisticsDialog import Ui_CodeStyleStatisticsDialog + +import UI.PixmapCache + + +class CodeStyleStatisticsDialog(QDialog, Ui_CodeStyleStatisticsDialog): + """ + Class implementing a dialog showing statistical data for the last + code style checker run. + """ + def __init__(self, statistics, parent=None): + """ + Constructor + + @param statistics dictionary with the statistical data + @param parent reference to the parent widget (QWidget) + """ + super().__init__(parent) + self.setupUi(self) + + stats = statistics.copy() + filesCount = stats["_FilesCount"] + filesIssues = stats["_FilesIssues"] + fixesCount = stats["_IssuesFixed"] + del stats["_FilesCount"] + del stats["_FilesIssues"] + del stats["_IssuesFixed"] + + totalIssues = 0 + + for code in sorted(stats.keys()): + if code in pep8.pep8_messages_sample_args: + message = QCoreApplication.translate( + "pep8", pep8.pep8_messages[code]).format( + *pep8.pep8_messages_sample_args[code]) + elif code in pep8.pep8_messages: + message = QCoreApplication.translate( + "pep8", pep8.pep8_messages[code]) + elif code in NamingStyleChecker.Messages: + message = QCoreApplication.translate( + "NamingStyleChecker", NamingStyleChecker.Messages[code]) + elif code in DocStyleChecker.Messages: + message = QCoreApplication.translate( + "DocStyleChecker", DocStyleChecker.Messages[code]) + else: + continue + self.__createItem(stats[code], code, message) + totalIssues += stats[code] + + self.totalIssues.setText( + self.trUtf8("%n issue(s) found", "", totalIssues)) + self.fixedIssues.setText( + self.trUtf8("%n issue(s) fixed", "", fixesCount)) + self.filesChecked.setText( + self.trUtf8("%n file(s) checked", "", filesCount)) + self.filesIssues.setText( + self.trUtf8("%n file(s) with issues found", "", filesIssues)) + + self.statisticsList.resizeColumnToContents(0) + self.statisticsList.resizeColumnToContents(1) + + def __createItem(self, count, code, message): + """ + Private method to create an entry in the result list. + + @param count occurrences of the issue (integer) + @param code of a code style issue message (string) + @param message code style issue message to be shown (string) + """ + itm = QTreeWidgetItem(self.statisticsList) + itm.setData(0, Qt.DisplayRole, count) + itm.setData(1, Qt.DisplayRole, code) + itm.setData(2, Qt.DisplayRole, message) + if code.startswith("W"): + itm.setIcon(1, UI.PixmapCache.getIcon("warning.png")) + elif code.startswith("E"): + itm.setIcon(1, UI.PixmapCache.getIcon("syntaxError.png")) + elif code.startswith("N"): + itm.setIcon(1, UI.PixmapCache.getIcon("namingError.png")) + elif code.startswith("D"): + itm.setIcon(1, UI.PixmapCache.getIcon("docstringError.png")) + + itm.setTextAlignment(0, Qt.AlignRight) + itm.setTextAlignment(1, Qt.AlignHCenter)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.ui Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,128 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CodeStyleStatisticsDialog</class> + <widget class="QDialog" name="CodeStyleStatisticsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>531</width> + <height>372</height> + </rect> + </property> + <property name="windowTitle"> + <string>Code Style Checker Statistics</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeWidget" name="statisticsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Count</string> + </property> + </column> + <column> + <property name="text"> + <string>Code</string> + </property> + </column> + <column> + <property name="text"> + <string>Message</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="filesChecked"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="filesIssues"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="totalIssues"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="fixedIssues"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>statisticsList</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CodeStyleStatisticsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CodeStyleStatisticsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/DocStyleChecker.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,1307 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a checker for PEP-257 documentation string conventions. +""" + +# +# The routines of the checker class are modeled after the ones found in +# pep257.py (version 0.2.4). +# + +try: + # Python 2 + from StringIO import StringIO # __IGNORE_EXCEPTION__ +except ImportError: + # Python 3 + from io import StringIO # __IGNORE_WARNING__ +import tokenize +import ast +import sys + +from PyQt4.QtCore import QT_TRANSLATE_NOOP, QCoreApplication + + +class DocStyleContext(object): + """ + Class implementing the source context. + """ + def __init__(self, source, startLine, contextType): + """ + Constructor + + @param source source code of the context (list of string or string) + @param startLine line number the context starts in the source (integer) + @param contextType type of the context object (string) + """ + if isinstance(source, str): + self.__source = source.splitlines(True) + else: + self.__source = source[:] + self.__start = startLine + self.__indent = "" + self.__type = contextType + + # ensure first line is left justified + if self.__source: + self.__indent = self.__source[0].replace( + self.__source[0].lstrip(), "") + self.__source[0] = self.__source[0].lstrip() + + def source(self): + """ + Public method to get the source. + + @return source (list of string) + """ + return self.__source + + def ssource(self): + """ + Public method to get the joined source lines. + + @return source (string) + """ + return "".join(self.__source) + + def start(self): + """ + Public method to get the start line number. + + @return start line number (integer) + """ + return self.__start + + def end(self): + """ + Public method to get the end line number. + + @return end line number (integer) + """ + return self.__start + len(self.__source) - 1 + + def indent(self): + """ + Public method to get the indentation of the first line. + + @return indentation string (string) + """ + return self.__indent + + def contextType(self): + """ + Public method to get the context type. + + @return context type (string) + """ + return self.__type + + +class DocStyleChecker(object): + """ + Class implementing a checker for PEP-257 documentation string conventions. + """ + Codes = [ + "D101", "D102", "D103", "D104", "D105", + "D111", "D112", "D113", + "D121", "D122", + "D131", "D132", "D133", "D134", + "D141", "D142", "D143", "D144", "D145", + + "D203", "D205", + "D221", "D222", + "D231", "D234", "D235", "D236", "D237", "D238", "D239", + "D242", "D243", "D244", "D245", "D246", "D247", + "D250", "D251", + ] + + Messages = { + "D101": QT_TRANSLATE_NOOP( + "DocStyleChecker", "module is missing a docstring"), + "D102": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "public function/method is missing a docstring"), + "D103": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "private function/method may be missing a docstring"), + "D104": QT_TRANSLATE_NOOP( + "DocStyleChecker", "public class is missing a docstring"), + "D105": QT_TRANSLATE_NOOP( + "DocStyleChecker", "private class may be missing a docstring"), + "D111": QT_TRANSLATE_NOOP( + "DocStyleChecker", 'docstring not surrounded by """'), + "D112": QT_TRANSLATE_NOOP( + "DocStyleChecker", + 'docstring containing \\ not surrounded by r"""'), + "D113": QT_TRANSLATE_NOOP( + "DocStyleChecker", + 'docstring containing unicode character not surrounded by u"""'), + "D121": QT_TRANSLATE_NOOP( + "DocStyleChecker", "one-liner docstring on multiple lines"), + "D122": QT_TRANSLATE_NOOP( + "DocStyleChecker", "docstring has wrong indentation"), + "D131": QT_TRANSLATE_NOOP( + "DocStyleChecker", "docstring summary does not end with a period"), + "D132": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring summary is not in imperative mood" + " (Does instead of Do)"), + "D133": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring summary looks like a function's/method's signature"), + "D134": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring does not mention the return value type"), + "D141": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "function/method docstring is separated by a blank line"), + "D142": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "class docstring is not preceded by a blank line"), + "D143": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "class docstring is not followed by a blank line"), + "D144": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring summary is not followed by a blank line"), + "D145": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "last paragraph of docstring is not followed by a blank line"), + + "D203": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "private function/method is missing a docstring"), + "D205": QT_TRANSLATE_NOOP( + "DocStyleChecker", "private class is missing a docstring"), + "D221": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "leading quotes of docstring not on separate line"), + "D222": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "trailing quotes of docstring not on separate line"), + "D231": QT_TRANSLATE_NOOP( + "DocStyleChecker", "docstring summary does not end with a period"), + "D234": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring does not contain a @return line but function/method" + " returns something"), + "D235": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring contains a @return line but function/method doesn't" + " return anything"), + "D236": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring does not contain enough @param/@keyparam lines"), + "D237": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring contains too many @param/@keyparam lines"), + "D238": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "keyword only arguments must be documented with @keyparam lines"), + "D239": QT_TRANSLATE_NOOP( + "DocStyleChecker", "order of @param/@keyparam lines does" + " not match the function/method signature"), + "D242": QT_TRANSLATE_NOOP( + "DocStyleChecker", "class docstring is preceded by a blank line"), + "D243": QT_TRANSLATE_NOOP( + "DocStyleChecker", "class docstring is followed by a blank line"), + "D244": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "function/method docstring is preceded by a blank line"), + "D245": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "function/method docstring is followed by a blank line"), + "D246": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring summary is not followed by a blank line"), + "D247": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "last paragraph of docstring is followed by a blank line"), + "D250": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring does not contain a @exception line but function/method" + " raises an exception"), + "D251": QT_TRANSLATE_NOOP( + "DocStyleChecker", + "docstring contains a @exception line but function/method doesn't" + " raise an exception"), + } + + def __init__(self, source, filename, select, ignore, expected, repeat, + maxLineLength=79, docType="pep257"): + """ + Constructor + + @param source source code to be checked (list of string) + @param filename name of the source file (string) + @param select list of selected codes (list of string) + @param ignore list of codes to be ignored (list of string) + @param expected list of expected codes (list of string) + @param repeat flag indicating to report each occurrence of a code + (boolean) + @keyparam maxLineLength allowed line length (integer) + @keyparam docType type of the documentation strings + (string, one of 'eric' or 'pep257') + """ + assert docType in ("eric", "pep257") + + self.__select = tuple(select) + self.__ignore = tuple(ignore) + self.__expected = expected[:] + self.__repeat = repeat + self.__maxLineLength = maxLineLength + self.__docType = docType + self.__filename = filename + self.__source = source[:] + + # statistics counters + self.counters = {} + + # collection of detected errors + self.errors = [] + + self.__lineNumber = 0 + + # caches + self.__functionsCache = None + self.__classesCache = None + self.__methodsCache = None + + self.__keywords = [ + 'moduleDocstring', 'functionDocstring', + 'classDocstring', 'methodDocstring', + 'defDocstring', 'docstring' + ] + if self.__docType == "pep257": + checkersWithCodes = { + "moduleDocstring": [ + (self.__checkModulesDocstrings, ("D101",)), + ], + "functionDocstring": [ + ], + "classDocstring": [ + (self.__checkClassDocstring, ("D104", "D105")), + (self.__checkBlankBeforeAndAfterClass, ("D142", "D143")), + ], + "methodDocstring": [ + ], + "defDocstring": [ + (self.__checkFunctionDocstring, ("D102", "D103")), + (self.__checkImperativeMood, ("D132",)), + (self.__checkNoSignature, ("D133",)), + (self.__checkReturnType, ("D134",)), + (self.__checkNoBlankLineBefore, ("D141",)), + ], + "docstring": [ + (self.__checkTripleDoubleQuotes, ("D111",)), + (self.__checkBackslashes, ("D112",)), + (self.__checkUnicode, ("D113",)), + (self.__checkOneLiner, ("D121",)), + (self.__checkIndent, ("D122",)), + (self.__checkEndsWithPeriod, ("D131",)), + (self.__checkBlankAfterSummary, ("D144",)), + (self.__checkBlankAfterLastParagraph, ("D145",)), + ], + } + elif self.__docType == "eric": + checkersWithCodes = { + "moduleDocstring": [ + (self.__checkModulesDocstrings, ("D101",)), + ], + "functionDocstring": [ + ], + "classDocstring": [ + (self.__checkClassDocstring, ("D104", "D205")), + (self.__checkEricNoBlankBeforeAndAfterClassOrFunction, + ("D242", "D243")), + ], + "methodDocstring": [ + ], + "defDocstring": [ + (self.__checkFunctionDocstring, ("D102", "D203")), + (self.__checkImperativeMood, ("D132",)), + (self.__checkNoSignature, ("D133",)), + (self.__checkEricReturn, ("D234", "D235")), + (self.__checkEricFunctionArguments, + ("D236", "D237", "D238", "D239")), + (self.__checkEricNoBlankBeforeAndAfterClassOrFunction, + ("D244", "D245")), + (self.__checkEricException, ("D250", "D251")), + ], + "docstring": [ + (self.__checkTripleDoubleQuotes, ("D111",)), + (self.__checkBackslashes, ("D112",)), + (self.__checkUnicode, ("D113",)), + (self.__checkIndent, ("D122",)), + (self.__checkEricEndsWithPeriod, ("D231",)), + (self.__checkEricBlankAfterSummary, ("D246",)), + (self.__checkEricNBlankAfterLastParagraph, ("D247",)), + (self.__checkEricQuotesOnSeparateLines, ("D222", "D223")) + ], + } + + self.__checkers = {} + for key, checkers in checkersWithCodes.items(): + for checker, codes in checkers: + if any(not (code and self.__ignoreCode(code)) + for code in codes): + if key not in self.__checkers: + self.__checkers[key] = [] + self.__checkers[key].append(checker) + + def __ignoreCode(self, code): + """ + Private method to check if the error code should be ignored. + + @param code message code to check for (string) + @return flag indicating to ignore the given code (boolean) + """ + return (code.startswith(self.__ignore) and + not code.startswith(self.__select)) + + def __error(self, lineNumber, offset, code, *args): + """ + Private method to record an issue. + + @param lineNumber line number of the issue (integer) + @param offset position within line of the issue (integer) + @param code message code (string) + @param args arguments for the message (list) + """ + if self.__ignoreCode(code): + return + + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + + # Don't care about expected codes + if code in self.__expected: + return + + if code and (self.counters[code] == 1 or self.__repeat): + if code in DocStyleChecker.Codes: + text = self.getMessage(code, *args) + else: + text = code + " " + QCoreApplication.translate( + "DocStyleChecker", "no message for this code defined") + # record the issue with one based line number + self.errors.append((self.__filename, lineNumber + 1, offset, text)) + + @classmethod + def getMessage(cls, code, *args): + """ + Class method to get a translated and formatted message for a + given code. + + @param code message code (string) + @param args arguments for a formatted message (list) + @return translated and formatted message (string) + """ + if code in DocStyleChecker.Messages: + return code + " " + QCoreApplication.translate( + "DocStyleChecker", DocStyleChecker.Messages[code]).format(*args) + else: + return code + " " + QCoreApplication.translate( + "DocStyleChecker", "no message for this code defined") + + def __resetReadline(self): + """ + Private method to reset the internal readline function. + """ + self.__lineNumber = 0 + + def __readline(self): + """ + Private method to get the next line from the source. + + @return next line of source (string) + """ + self.__lineNumber += 1 + if self.__lineNumber > len(self.__source): + return '' + return self.__source[self.__lineNumber - 1] + + def run(self): + """ + Public method to check the given source for violations of doc string + conventions according to PEP-257. + """ + if not self.__source or not self.__filename: + # don't do anything, if essential data is missing + return + + if not self.__checkers: + # don't do anything, if no codes were selected + return + + for keyword in self.__keywords: + if keyword in self.__checkers: + for check in self.__checkers[keyword]: + for context in self.__parseContexts(keyword): + docstring = self.__parseDocstring(context, keyword) + check(docstring, context) + + def __getSummaryLine(self, docstringContext): + """ + Private method to extract the summary line. + + @param docstringContext docstring context (DocStyleContext) + @return summary line (string) and the line it was found on (integer) + """ + lines = docstringContext.source() + + line = (lines[0] + .replace('r"""', "", 1) + .replace('u"""', "", 1) + .replace('"""', "") + .replace("r'''", "", 1) + .replace("u'''", "", 1) + .replace("'''", "") + .strip()) + + if len(lines) == 1 or len(line) > 0: + return line, 0 + return lines[1].strip().replace('"""', "").replace("'''", ""), 1 + + def __getSummaryLines(self, docstringContext): + """ + Private method to extract the summary lines. + + @param docstringContext docstring context (DocStyleContext) + @return summary lines (list of string) and the line it was found on + (integer) + """ + summaries = [] + lines = docstringContext.source() + + line0 = (lines[0] + .replace('r"""', "", 1) + .replace('u"""', "", 1) + .replace('"""', "") + .replace("r'''", "", 1) + .replace("u'''", "", 1) + .replace("'''", "") + .strip()) + if len(lines) > 1: + line1 = lines[1].strip().replace('"""', "").replace("'''", "") + else: + line1 = "" + if len(lines) > 2: + line2 = lines[2].strip().replace('"""', "").replace("'''", "") + else: + line2 = "" + if line0: + lineno = 0 + summaries.append(line0) + if not line0.endswith(".") and line1: + # two line summary + summaries.append(line1) + elif line1: + lineno = 1 + summaries.append(line1) + if not line1.endswith(".") and line2: + # two line summary + summaries.append(line2) + else: + lineno = 2 + summaries.append(line2) + return summaries, lineno + + if sys.version_info[0] < 3: + def __getArgNames(self, node): + """ + Private method to get the argument names of a function node. + + @param node AST node to extract arguments names from + @return tuple of two list of argument names, one for arguments + and one for keyword arguments (tuple of list of string) + """ + def unpackArgs(args): + """ + Local helper function to unpack function argument names. + + @param args list of AST node arguments + @return list of argument names (list of string) + """ + ret = [] + for arg in args: + if isinstance(arg, ast.Tuple): + ret.extend(unpackArgs(arg.elts)) + else: + ret.append(arg.id) + return ret + + arguments = unpackArgs(node.args.args) + if node.args.vararg is not None: + arguments.append(node.args.vararg) + kwarguments = [] + if node.args.kwarg is not None: + kwarguments.append(node.args.kwarg) + return arguments, kwarguments + else: + def __getArgNames(self, node): # __IGNORE_WARNING__ + """ + Private method to get the argument names of a function node. + + @param node AST node to extract arguments names from + @return tuple of two list of argument names, one for arguments + and one for keyword arguments (tuple of list of string) + """ + arguments = [] + arguments.extend([arg.arg for arg in node.args.args]) + if node.args.vararg is not None: + arguments.append(node.args.vararg) + + kwarguments = [] + kwarguments.extend([arg.arg for arg in node.args.kwonlyargs]) + if node.args.kwarg is not None: + kwarguments.append(node.args.kwarg) + return arguments, kwarguments + + ################################################################## + ## Parsing functionality below + ################################################################## + + def __parseModuleDocstring(self, source): + """ + Private method to extract a docstring given a module source. + + @param source source to parse (list of string) + @return context of extracted docstring (DocStyleContext) + """ + for kind, value, (line, char), _, _ in tokenize.generate_tokens( + StringIO("".join(source)).readline): + if kind in [tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL]: + continue + elif kind == tokenize.STRING: # first STRING should be docstring + return DocStyleContext(value, line - 1, "docstring") + else: + return None + + def __parseDocstring(self, context, what=''): + """ + Private method to extract a docstring given `def` or `class` source. + + @param context context data to get the docstring from (DocStyleContext) + @param what string denoting what is being parsed (string) + @return context of extracted docstring (DocStyleContext) + """ + moduleDocstring = self.__parseModuleDocstring(context.source()) + if what.startswith('module') or context.contextType() == "module": + return moduleDocstring + if moduleDocstring: + return moduleDocstring + + tokenGenerator = tokenize.generate_tokens( + StringIO(context.ssource()).readline) + try: + kind = None + while kind != tokenize.INDENT: + kind, _, _, _, _ = next(tokenGenerator) + kind, value, (line, char), _, _ = next(tokenGenerator) + if kind == tokenize.STRING: # STRING after INDENT is a docstring + return DocStyleContext( + value, context.start() + line - 1, "docstring") + except StopIteration: + pass + + return None + + def __parseTopLevel(self, keyword): + """ + Private method to extract top-level functions or classes. + + @param keyword keyword signaling what to extract (string) + @return extracted function or class contexts (list of DocStyleContext) + """ + self.__resetReadline() + tokenGenerator = tokenize.generate_tokens(self.__readline) + kind, value, char = None, None, None + contexts = [] + try: + while True: + start, end = None, None + while not (kind == tokenize.NAME and + value == keyword and + char == 0): + kind, value, (line, char), _, _ = next(tokenGenerator) + start = line - 1, char + while not (kind == tokenize.DEDENT and + value == '' and + char == 0): + kind, value, (line, char), _, _ = next(tokenGenerator) + end = line - 1, char + contexts.append(DocStyleContext( + self.__source[start[0]:end[0]], start[0], keyword)) + except StopIteration: + return contexts + + def __parseFunctions(self): + """ + Private method to extract top-level functions. + + @return extracted function contexts (list of DocStyleContext) + """ + if not self.__functionsCache: + self.__functionsCache = self.__parseTopLevel('def') + return self.__functionsCache + + def __parseClasses(self): + """ + Private method to extract top-level classes. + + @return extracted class contexts (list of DocStyleContext) + """ + if not self.__classesCache: + self.__classesCache = self.__parseTopLevel('class') + return self.__classesCache + + def __skipIndentedBlock(self, tokenGenerator): + """ + Private method to skip over an indented block of source code. + + @param tokenGenerator token generator + @return last token of the indented block + """ + kind, value, start, end, raw = next(tokenGenerator) + while kind != tokenize.INDENT: + kind, value, start, end, raw = next(tokenGenerator) + indent = 1 + for kind, value, start, end, raw in tokenGenerator: + if kind == tokenize.INDENT: + indent += 1 + elif kind == tokenize.DEDENT: + indent -= 1 + if indent == 0: + return kind, value, start, end, raw + + def __parseMethods(self): + """ + Private method to extract methods of all classes. + + @return extracted method contexts (list of DocStyleContext) + """ + if not self.__methodsCache: + contexts = [] + for classContext in self.__parseClasses(): + tokenGenerator = tokenize.generate_tokens( + StringIO(classContext.ssource()).readline) + kind, value, char = None, None, None + try: + while True: + start, end = None, None + while not (kind == tokenize.NAME and value == 'def'): + kind, value, (line, char), _, _ = \ + next(tokenGenerator) + start = line - 1, char + kind, value, (line, char), _, _ = \ + self.__skipIndentedBlock(tokenGenerator) + end = line - 1, char + startLine = classContext.start() + start[0] + endLine = classContext.start() + end[0] + contexts.append( + DocStyleContext(self.__source[startLine:endLine], + startLine, "def")) + except StopIteration: + pass + self.__methodsCache = contexts + + return self.__methodsCache + + def __parseContexts(self, kind): + """ + Private method to extract a context from the source. + + @param kind kind of context to extract (string) + @return requested contexts (list of DocStyleContext) + """ + if kind == 'moduleDocstring': + return [DocStyleContext(self.__source, 0, "module")] + if kind == 'functionDocstring': + return self.__parseFunctions() + if kind == 'classDocstring': + return self.__parseClasses() + if kind == 'methodDocstring': + return self.__parseMethods() + if kind == 'defDocstring': + return self.__parseFunctions() + self.__parseMethods() + if kind == 'docstring': + return ([DocStyleContext(self.__source, 0, "module")] + + self.__parseFunctions() + + self.__parseClasses() + + self.__parseMethods()) + return [] # fall back + + ################################################################## + ## Checking functionality below (PEP-257) + ################################################################## + + def __checkModulesDocstrings(self, docstringContext, context): + """ + Private method to check, if the module has a docstring. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + self.__error(context.start(), 0, "D101") + return + + docstring = docstringContext.ssource() + if (not docstring or not docstring.strip() or + not docstring.strip('\'"')): + self.__error(context.start(), 0, "D101") + + def __checkFunctionDocstring(self, docstringContext, context): + """ + Private method to check, that all public functions and methods + have a docstring. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + functionName = context.source()[0].lstrip().split()[1].split("(")[0] + if functionName.startswith('_') and not functionName.endswith('__'): + if self.__docType == "eric": + code = "D203" + else: + code = "D103" + else: + code = "D102" + + if docstringContext is None: + self.__error(context.start(), 0, code) + return + + docstring = docstringContext.ssource() + if (not docstring or not docstring.strip() or + not docstring.strip('\'"')): + self.__error(context.start(), 0, code) + + def __checkClassDocstring(self, docstringContext, context): + """ + Private method to check, that all public functions and methods + have a docstring. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + className = context.source()[0].lstrip().split()[1].split("(")[0] + if className.startswith('_'): + if self.__docType == "eric": + code = "D205" + else: + code = "D105" + else: + code = "D104" + + if docstringContext is None: + self.__error(context.start(), 0, code) + return + + docstring = docstringContext.ssource() + if (not docstring or not docstring.strip() or + not docstring.strip('\'"')): + self.__error(context.start(), 0, code) + + def __checkTripleDoubleQuotes(self, docstringContext, context): + """ + Private method to check, that all docstrings are surrounded + by triple double quotes. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstring = docstringContext.ssource().strip() + if not docstring.startswith(('"""', 'r"""', 'u"""')): + self.__error(docstringContext.start(), 0, "D111") + + def __checkBackslashes(self, docstringContext, context): + """ + Private method to check, that all docstrings containing + backslashes are surrounded by raw triple double quotes. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstring = docstringContext.ssource().strip() + if "\\" in docstring and not docstring.startswith('r"""'): + self.__error(docstringContext.start(), 0, "D112") + + def __checkUnicode(self, docstringContext, context): + """ + Private method to check, that all docstrings containing unicode + characters are surrounded by unicode triple double quotes. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstring = docstringContext.ssource().strip() + if not docstring.startswith('u"""') and \ + any(ord(char) > 127 for char in docstring): + self.__error(docstringContext.start(), 0, "D113") + + def __checkOneLiner(self, docstringContext, context): + """ + Private method to check, that one-liner docstrings fit on + one line with quotes. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + lines = docstringContext.source() + if len(lines) > 1: + nonEmptyLines = [l for l in lines if l.strip().strip('\'"')] + if len(nonEmptyLines) == 1: + modLen = len(context.indent() + '"""' + + nonEmptyLines[0].strip() + '"""') + if context.contextType() != "module": + modLen += 4 + if not nonEmptyLines[0].strip().endswith("."): + # account for a trailing dot + modLen += 1 + if modLen <= self.__maxLineLength: + self.__error(docstringContext.start(), 0, "D121") + + def __checkIndent(self, docstringContext, context): + """ + Private method to check, that docstrings are properly indented. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + lines = docstringContext.source() + if len(lines) == 1: + return + + nonEmptyLines = [l.rstrip() for l in lines[1:] if l.strip()] + if not nonEmptyLines: + return + + indent = min([len(l) - len(l.strip()) for l in nonEmptyLines]) + if context.contextType() == "module": + expectedIndent = 0 + else: + expectedIndent = len(context.indent()) + 4 + if indent != expectedIndent: + self.__error(docstringContext.start(), 0, "D122") + + def __checkEndsWithPeriod(self, docstringContext, context): + """ + Private method to check, that docstring summaries end with a period. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + summary, lineNumber = self.__getSummaryLine(docstringContext) + if not summary.endswith("."): + self.__error(docstringContext.start() + lineNumber, 0, "D131") + + def __checkImperativeMood(self, docstringContext, context): + """ + Private method to check, that docstring summaries are in + imperative mood. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + summary, lineNumber = self.__getSummaryLine(docstringContext) + firstWord = summary.strip().split()[0] + if firstWord.endswith("s") and not firstWord.endswith("ss"): + self.__error(docstringContext.start() + lineNumber, 0, "D132") + + def __checkNoSignature(self, docstringContext, context): + """ + Private method to check, that docstring summaries don't repeat + the function's signature. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + functionName = context.source()[0].lstrip().split()[1].split("(")[0] + summary, lineNumber = self.__getSummaryLine(docstringContext) + if functionName + "(" in summary.replace(" ", "") and \ + not functionName + "()" in summary.replace(" ", ""): + # report only, if it is not an abbreviated form (i.e. function() ) + self.__error(docstringContext.start() + lineNumber, 0, "D133") + + def __checkReturnType(self, docstringContext, context): + """ + Private method to check, that docstrings mention the return value type. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + if "return" not in docstringContext.ssource().lower(): + tokens = list( + tokenize.generate_tokens(StringIO(context.ssource()).readline)) + return_ = [tokens[i + 1][0] for i, token in enumerate(tokens) + if token[1] == "return"] + if (set(return_) - + set([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE]) != + set([])): + self.__error(docstringContext.end(), 0, "D134") + + def __checkNoBlankLineBefore(self, docstringContext, context): + """ + Private method to check, that function/method docstrings are not + preceded by a blank line. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + contextLines = context.source() + cti = 0 + while cti < len(contextLines) and \ + not contextLines[cti].strip().startswith( + ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''")): + cti += 1 + if cti == len(contextLines): + return + + if not contextLines[cti - 1].strip(): + self.__error(docstringContext.start(), 0, "D141") + + def __checkBlankBeforeAndAfterClass(self, docstringContext, context): + """ + Private method to check, that class docstrings have one + blank line around them. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + contextLines = context.source() + cti = 0 + while cti < len(contextLines) and \ + not contextLines[cti].strip().startswith( + ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''")): + cti += 1 + if cti == len(contextLines): + return + + start = cti + if contextLines[cti].strip() in ( + '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"): + # it is a multi line docstring + cti += 1 + + while cti < len(contextLines) and \ + not contextLines[cti].strip().endswith(('"""', "'''")): + cti += 1 + end = cti + if cti >= len(contextLines) - 1: + return + + if contextLines[start - 1].strip(): + self.__error(docstringContext.start(), 0, "D142") + if contextLines[end + 1].strip(): + self.__error(docstringContext.end(), 0, "D143") + + def __checkBlankAfterSummary(self, docstringContext, context): + """ + Private method to check, that docstring summaries are followed + by a blank line. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstrings = docstringContext.source() + if len(docstrings) <= 3: + # correct/invalid one-liner + return + + summary, lineNumber = self.__getSummaryLine(docstringContext) + if len(docstrings) > 2: + if docstrings[lineNumber + 1].strip(): + self.__error(docstringContext.start() + lineNumber, 0, "D144") + + def __checkBlankAfterLastParagraph(self, docstringContext, context): + """ + Private method to check, that the last paragraph of docstrings is + followed by a blank line. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstrings = docstringContext.source() + if len(docstrings) <= 3: + # correct/invalid one-liner + return + + if docstrings[-2].strip(): + self.__error(docstringContext.end(), 0, "D145") + + ################################################################## + ## Checking functionality below (eric specific ones) + ################################################################## + + def __checkEricQuotesOnSeparateLines(self, docstringContext, context): + """ + Private method to check, that leading and trailing quotes are on + a line by themselves. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + lines = docstringContext.source() + if lines[0].strip().strip('ru"\''): + self.__error(docstringContext.start(), 0, "D221") + if lines[-1].strip().strip('"\''): + self.__error(docstringContext.end(), 0, "D222") + + def __checkEricEndsWithPeriod(self, docstringContext, context): + """ + Private method to check, that docstring summaries end with a period. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + summaryLines, lineNumber = self.__getSummaryLines(docstringContext) + if summaryLines[-1].lstrip().startswith("@"): + summaryLines.pop(-1) + summary = " ".join([s.strip() for s in summaryLines if s]) + if not summary.endswith(".") and \ + not summary.split(None, 1)[0].lower() == "constructor": + self.__error( + docstringContext.start() + lineNumber + len(summaryLines) - 1, + 0, "D231") + + def __checkEricReturn(self, docstringContext, context): + """ + Private method to check, that docstrings contain an @return line + if they return anything and don't otherwise. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + tokens = list( + tokenize.generate_tokens(StringIO(context.ssource()).readline)) + return_ = [tokens[i + 1][0] for i, token in enumerate(tokens) + if token[1] in ("return", "yield")] + if "@return" not in docstringContext.ssource(): + if (set(return_) - + set([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE]) != + set([])): + self.__error(docstringContext.end(), 0, "D234") + else: + if (set(return_) - + set([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE]) == + set([])): + self.__error(docstringContext.end(), 0, "D235") + + def __checkEricFunctionArguments(self, docstringContext, context): + """ + Private method to check, that docstrings contain an @param and/or + @keyparam line for each argument. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + try: + tree = ast.parse(context.ssource()) + except (SyntaxError, TypeError): + return + if (isinstance(tree, ast.Module) and len(tree.body) == 1 and + isinstance(tree.body[0], ast.FunctionDef)): + functionDef = tree.body[0] + argNames, kwNames = self.__getArgNames(functionDef) + if "self" in argNames: + argNames.remove("self") + if "cls" in argNames: + argNames.remove("cls") + + docstring = docstringContext.ssource() + if (docstring.count("@param") + docstring.count("@keyparam") < + len(argNames + kwNames)): + self.__error(docstringContext.end(), 0, "D236") + elif (docstring.count("@param") + docstring.count("@keyparam") > + len(argNames + kwNames)): + self.__error(docstringContext.end(), 0, "D237") + else: + # extract @param and @keyparam from docstring + args = [] + kwargs = [] + for line in docstringContext.source(): + if line.strip().startswith(("@param", "@keyparam")): + at, name = line.strip().split(None, 2)[:2] + if at == "@keyparam": + kwargs.append(name.lstrip("*")) + args.append(name.lstrip("*")) + + # do the checks + for name in kwNames: + if name not in kwargs: + self.__error(docstringContext.end(), 0, "D238") + return + if argNames + kwNames != args: + self.__error(docstringContext.end(), 0, "D239") + + def __checkEricException(self, docstringContext, context): + """ + Private method to check, that docstrings contain an @exception line + if they raise an exception and don't otherwise. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + tokens = list( + tokenize.generate_tokens(StringIO(context.ssource()).readline)) + exception = [tokens[i + 1][0] for i, token in enumerate(tokens) + if token[1] == "raise"] + if "@exception" not in docstringContext.ssource() and \ + "@throws" not in docstringContext.ssource() and \ + "@raise" not in docstringContext.ssource(): + if (set(exception) - + set([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE]) != + set([])): + self.__error(docstringContext.end(), 0, "D250") + else: + if (set(exception) - + set([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE]) == + set([])): + self.__error(docstringContext.end(), 0, "D251") + + def __checkEricBlankAfterSummary(self, docstringContext, context): + """ + Private method to check, that docstring summaries are followed + by a blank line. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstrings = docstringContext.source() + if len(docstrings) <= 3: + # correct/invalid one-liner + return + + summaryLines, lineNumber = self.__getSummaryLines(docstringContext) + if len(docstrings) - 2 > lineNumber + len(summaryLines) - 1: + if docstrings[lineNumber + len(summaryLines)].strip(): + self.__error(docstringContext.start() + lineNumber, 0, "D246") + + def __checkEricNoBlankBeforeAndAfterClassOrFunction( + self, docstringContext, context): + """ + Private method to check, that class and function/method docstrings + have no blank line around them. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + contextLines = context.source() + isClassContext = contextLines[0].lstrip().startswith("class ") + cti = 0 + while cti < len(contextLines) and \ + not contextLines[cti].strip().startswith( + ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''")): + cti += 1 + if cti == len(contextLines): + return + + start = cti + if contextLines[cti].strip() in ( + '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"): + # it is a multi line docstring + cti += 1 + + while cti < len(contextLines) and \ + not contextLines[cti].strip().endswith(('"""', "'''")): + cti += 1 + end = cti + if cti >= len(contextLines) - 1: + return + + if isClassContext: + if not contextLines[start - 1].strip(): + self.__error(docstringContext.start(), 0, "D242") + if not contextLines[end + 1].strip(): + self.__error(docstringContext.end(), 0, "D243") + else: + if not contextLines[start - 1].strip(): + self.__error(docstringContext.start(), 0, "D244") + if not contextLines[end + 1].strip(): + self.__error(docstringContext.end(), 0, "D245") + + def __checkEricNBlankAfterLastParagraph(self, docstringContext, context): + """ + Private method to check, that the last paragraph of docstrings is + not followed by a blank line. + + @param docstringContext docstring context (DocStyleContext) + @param context context of the docstring (DocStyleContext) + """ + if docstringContext is None: + return + + docstrings = docstringContext.source() + if len(docstrings) <= 3: + # correct/invalid one-liner + return + + if not docstrings[-2].strip(): + self.__error(docstringContext.end(), 0, "D247")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/NamingStyleChecker.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a checker for PEP-8 naming conventions. +""" + +import collections +import ast +import re +import os + +from PyQt4.QtCore import QT_TRANSLATE_NOOP, QCoreApplication + + +class NamingStyleChecker(object): + """ + Class implementing a checker for PEP-8 naming conventions. + """ + LowercaseRegex = re.compile(r"[_a-z][_a-z0-9]*$") + UppercaseRegexp = re.compile(r"[_A-Z][_A-Z0-9]*$") + CamelcaseRegexp = re.compile(r"_?[A-Z][a-zA-Z0-9]*$") + MixedcaseRegexp = re.compile(r"_?[a-z][a-zA-Z0-9]*$") + + Codes = [ + "N801", "N802", "N803", "N804", "N805", "N806", "N807", "N808", + "N811", "N812", "N813", "N814", "N821", "N831" + ] + Messages = { + "N801": QT_TRANSLATE_NOOP("NamingStyleChecker", + "class names should use CapWords convention"), + "N802": QT_TRANSLATE_NOOP("NamingStyleChecker", + "function name should be lowercase"), + "N803": QT_TRANSLATE_NOOP("NamingStyleChecker", + "argument name should be lowercase"), + "N804": QT_TRANSLATE_NOOP("NamingStyleChecker", + "first argument of a class method should be named 'cls'"), + "N805": QT_TRANSLATE_NOOP("NamingStyleChecker", + "first argument of a method should be named 'self'"), + "N806": QT_TRANSLATE_NOOP("NamingStyleChecker", + "first argument of a static method should not be named" + " 'self' or 'cls"), + "N807": QT_TRANSLATE_NOOP("NamingStyleChecker", + "module names should be lowercase"), + "N808": QT_TRANSLATE_NOOP("NamingStyleChecker", + "package names should be lowercase"), + "N811": QT_TRANSLATE_NOOP("NamingStyleChecker", + "constant imported as non constant"), + "N812": QT_TRANSLATE_NOOP("NamingStyleChecker", + "lowercase imported as non lowercase"), + "N813": QT_TRANSLATE_NOOP("NamingStyleChecker", + "camelcase imported as lowercase"), + "N814": QT_TRANSLATE_NOOP("NamingStyleChecker", + "camelcase imported as constant"), + "N821": QT_TRANSLATE_NOOP("NamingStyleChecker", + "variable in function should be lowercase"), + "N831": QT_TRANSLATE_NOOP("NamingStyleChecker", + "names 'l', 'O' and 'I' should be avoided"), + } + + def __init__(self, tree, filename, options): + """ + Constructor (according to 'extended' pep8.py API) + + @param tree AST tree of the source file + @param filename name of the source file (string) + @param options options as parsed by pep8.StyleGuide + """ + self.__parents = collections.deque() + self.__tree = tree + self.__filename = filename + + self.__checkersWithCodes = { + "classdef": [ + (self.__checkClassName, ("N801",)), + (self.__checkNameToBeAvoided, ("N831",)), + ], + "functiondef": [ + (self.__checkFuntionName, ("N802",)), + (self.__checkFunctionArgumentNames, + ("N803", "N804", "N805", "N806")), + (self.__checkNameToBeAvoided, ("N831",)), + ], + "assign": [ + (self.__checkVariablesInFunction, ("N821",)), + (self.__checkNameToBeAvoided, ("N831",)), + ], + "importfrom": [ + (self.__checkImportAs, ("N811", "N812", "N813", "N814")), + ], + "module": [ + (self.__checkModule, ("N807", "N808")), + ], + } + + self.__checkers = {} + for key, checkers in self.__checkersWithCodes.items(): + for checker, codes in checkers: + if any(not (code and options.ignore_code(code)) + for code in codes): + if key not in self.__checkers: + self.__checkers[key] = [] + self.__checkers[key].append(checker) + + def run(self): + """ + Public method run by the pep8.py checker. + + @return tuple giving line number, offset within line, code and + checker function + """ + if self.__tree and self.__checkers: + return self.__visitTree(self.__tree) + else: + return () + + @classmethod + def getMessage(cls, code, *args): + """ + Class method to get a translated and formatted message for a + given code. + + @param code message code (string) + @param args arguments for a formatted message (list) + @return translated and formatted message (string) + """ + if code in cls.Messages: + return code + " " + QCoreApplication.translate("NamingStyleChecker", + cls.Messages[code]).format(*args) + else: + return code + " " + QCoreApplication.translate("NamingStyleChecker", + "no message for this code defined") + + def __visitTree(self, node): + """ + Private method to scan the given AST tree. + + @param node AST tree node to scan + @return tuple giving line number, offset within line, code and + checker function + """ + for error in self.__visitNode(node): + yield error + self.__parents.append(node) + for child in ast.iter_child_nodes(node): + for error in self.__visitTree(child): + yield error + self.__parents.pop() + + def __visitNode(self, node): + """ + Private method to inspect the given AST node. + + @param node AST tree node to inspect + @return tuple giving line number, offset within line, code and + checker function + """ + if isinstance(node, ast.ClassDef): + self.__tagClassFunctions(node) + elif isinstance(node, ast.FunctionDef): + self.__findGlobalDefs(node) + + checkerName = node.__class__.__name__.lower() + if checkerName in self.__checkers: + for checker in self.__checkers[checkerName]: + for error in checker(node, self.__parents): + yield error + (self.__checkers[checkerName],) + + def __tagClassFunctions(self, classNode): + """ + Private method to tag functions if they are methods, class methods or + static methods. + + @param classNode AST tree node to tag + """ + # try to find all 'old style decorators' like + # m = staticmethod(m) + lateDecoration = {} + for node in ast.iter_child_nodes(classNode): + if not (isinstance(node, ast.Assign) and + isinstance(node.value, ast.Call) and + isinstance(node.value.func, ast.Name)): + continue + funcName = node.value.func.id + if funcName in ("classmethod", "staticmethod"): + meth = (len(node.value.args) == 1 and node.value.args[0]) + if isinstance(meth, ast.Name): + lateDecoration[meth.id] = funcName + + # iterate over all functions and tag them + for node in ast.iter_child_nodes(classNode): + if not isinstance(node, ast.FunctionDef): + continue + + node.function_type = 'method' + if node.name == "__new__": + node.function_type = "classmethod" + + if node.name in lateDecoration: + node.function_type = lateDecoration[node.name] + elif node.decorator_list: + names = [d.id for d in node.decorator_list + if isinstance(d, ast.Name) and + d.id in ("classmethod", "staticmethod")] + if names: + node.function_type = names[0] + + def __findGlobalDefs(self, functionNode): + """ + Private method amend a node with global definitions information. + + @param functionNode AST tree node to amend + """ + globalNames = set() + nodesToCheck = collections.deque(ast.iter_child_nodes(functionNode)) + while nodesToCheck: + node = nodesToCheck.pop() + if isinstance(node, ast.Global): + globalNames.update(node.names) + + if not isinstance(node, (ast.FunctionDef, ast.ClassDef)): + nodesToCheck.extend(ast.iter_child_nodes(node)) + functionNode.global_names = globalNames + + def __getArgNames(self, node): + """ + Private method to get the argument names of a function node. + + @param node AST node to extract arguments names from + @return list of argument names (list of string) + """ + posArgs = [arg.arg for arg in node.args.args] + kwOnly = [arg.arg for arg in node.args.kwonlyargs] + return posArgs + kwOnly + + def __error(self, node, code): + """ + Private method to build the error information. + + @param node AST node to report an error for + @param code error code to report (string) + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + if isinstance(node, ast.Module): + lineno = 0 + offset = 0 + else: + lineno = node.lineno + offset = node.col_offset + if isinstance(node, ast.ClassDef): + lineno += len(node.decorator_list) + offset += 6 + elif isinstance(node, ast.FunctionDef): + lineno += len(node.decorator_list) + offset += 4 + return (lineno, offset, code) + + def __isNameToBeAvoided(self, name): + """ + Private method to check, if the given name should be avoided. + + @param name name to be checked (string) + @return flag indicating to avoid it (boolen) + """ + return name in ("l", "O", "I") + + def __checkNameToBeAvoided(self, node, parents): + """ + Private class to check the given node for a name to be avoided (N831). + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + if isinstance(node, (ast.ClassDef, ast.FunctionDef)): + name = node.name + if self.__isNameToBeAvoided(name): + yield self.__error(node, "N831") + return + + if isinstance(node, ast.FunctionDef): + argNames = self.__getArgNames(node) + for arg in argNames: + if self.__isNameToBeAvoided(arg): + yield self.__error(node, "N831") + return + + if isinstance(node, ast.Assign): + for target in node.targets: + name = isinstance(target, ast.Name) and target.id + if not name: + return + + if self.__isNameToBeAvoided(name): + yield self.__error(node, "N831") + return + + def __checkClassName(self, node, parents): + """ + Private class to check the given node for class name + conventions (N801). + + Almost without exception, class names use the CapWords convention. + Classes for internal use have a leading underscore in addition. + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + if not self.CamelcaseRegexp.match(node.name): + yield self.__error(node, "N801") + + def __checkFuntionName(self, node, parents): + """ + Private class to check the given node for function name + conventions (N802). + + Function names should be lowercase, with words separated by underscores + as necessary to improve readability. Functions <b>not</b> being + methods '__' in front and back are not allowed. Mixed case is allowed + only in contexts where that's already the prevailing style + (e.g. threading.py), to retain backwards compatibility. + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + functionType = getattr(node, "function_type", "function") + name = node.name + if (functionType == "function" and "__" in (name[:2], name[-2:])) or \ + not self.LowercaseRegex.match(name): + yield self.__error(node, "N802") + + def __checkFunctionArgumentNames(self, node, parents): + """ + Private class to check the argument names of functions + (N803, N804, N805, N806). + + The argument names of a function should be lowercase, with words + separated by underscores. A class method should have 'cls' as the + first argument. A method should have 'self' as the first argument. + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + if node.args.kwarg is not None: + if not self.LowercaseRegex.match(node.args.kwarg): + yield self.__error(node, "N803") + return + + if node.args.vararg is not None: + if not self.LowercaseRegex.match(node.args.vararg): + yield self.__error(node, "N803") + return + + argNames = self.__getArgNames(node) + functionType = getattr(node, "function_type", "function") + + if not argNames: + if functionType == "method": + yield self.__error(node, "N805") + elif functionType == "classmethod": + yield self.__error(node, "N804") + return + + if functionType == "method": + if argNames[0] != "self": + yield self.__error(node, "N805") + elif functionType == "classmethod": + if argNames[0] != "cls": + yield self.__error(node, "N804") + elif functionType == "staticmethod": + if argNames[0] in ("cls", "self"): + yield self.__error(node, "N806") + for arg in argNames: + if not self.LowercaseRegex.match(arg): + yield self.__error(node, "N803") + return + + def __checkVariablesInFunction(self, node, parents): + """ + Private method to check local variables in functions (N821). + + Local variables in functions should be lowercase. + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + for parentFunc in reversed(parents): + if isinstance(parentFunc, ast.ClassDef): + return + if isinstance(parentFunc, ast.FunctionDef): + break + else: + return + for target in node.targets: + name = isinstance(target, ast.Name) and target.id + if not name or name in parentFunc.global_names: + return + + if not self.LowercaseRegex.match(name) and name[:1] != '_': + yield self.__error(target, "N821") + + def __checkModule(self, node, parents): + """ + Private method to check module naming conventions (N807, N808). + + Module and package names should be lowercase. + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + if self.__filename: + moduleName = os.path.splitext(os.path.basename(self.__filename))[0] + if moduleName.lower() != moduleName: + yield self.__error(node, "N807") + + if moduleName == "__init__": + # we got a package + packageName = \ + os.path.split(os.path.dirname(self.__filename))[1] + if packageName.lower != packageName: + yield self.__error(node, "N808") + + def __checkImportAs(self, node, parents): + """ + Private method to check that imports don't change the + naming convention (N811, N812, N813, N814). + + @param node AST note to check + @param parents list of parent nodes + @return tuple giving line number, offset within line and error code + (integer, integer, string) + """ + for name in node.names: + if not name.asname: + continue + + if self.UppercaseRegexp.match(name.name): + if not self.UppercaseRegexp.match(name.asname): + yield self.__error(node, "N811") + elif self.LowercaseRegex.match(name.name): + if not self.LowercaseRegex.match(name.asname): + yield self.__error(node, "N812") + elif self.LowercaseRegex.match(name.asname): + yield self.__error(node, "N813") + elif self.UppercaseRegexp.match(name.asname): + yield self.__error(node, "N814")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/__init__.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing the code style checker and fixer plug-in. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/pep8.py Fri Oct 04 14:26:08 2013 +0200 @@ -0,0 +1,2075 @@ +# -*- coding: utf-8 -*- + +# +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006-2009 Johann C. Rocholl <johann@rocholl.net> +# Copyright (C) 2009-2013 Florent Xicluna <florent.xicluna@gmail.com> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements +900 syntax error +""" + +# +# This is a modified version to make the original pep8.py better suitable +# for being called from within the eric5 IDE. The modifications are as +# follows: +# +# - made messages translatable via Qt +# - added code for eric5 integration +# +# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> +# + +__version__ = '1.4.6' + +import os +import sys +import re +import time +import inspect +import keyword +import tokenize +from optparse import OptionParser +from fnmatch import fnmatch +try: + from configparser import RawConfigParser + from io import TextIOWrapper +except ImportError: + from ConfigParser import RawConfigParser # __IGNORE_WARNING__ + +from PyQt4.QtCore import QCoreApplication, QT_TRANSLATE_NOOP + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__' +DEFAULT_IGNORE = 'E123,E226,E24' +if sys.platform == 'win32': + DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') +else: + DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or + os.path.expanduser('~/.config'), 'pep8') +PROJECT_CONFIG = ('setup.cfg', 'tox.ini', '.pep8') +TESTSUITE_PATH = os.path.join(os.path.dirname(__file__), 'testsuite') +MAX_LINE_LENGTH = 79 +REPORT_FORMAT = { + 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', + 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', +} + +PyCF_ONLY_AST = 1024 +SINGLETONS = frozenset(['False', 'None', 'True']) +KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS +UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) +ARITHMETIC_OP = frozenset(['**', '*', '/', '//', '+', '-']) +WS_OPTIONAL_OPERATORS = ARITHMETIC_OP.union(['^', '&', '|', '<<', '>>', '%']) +WS_NEEDED_OPERATORS = frozenset([ + '**=', '*=', '/=', '//=', '+=', '-=', '!=', '<>', '<', '>', + '%=', '^=', '&=', '|=', '==', '<=', '>=', '<<=', '>>=', '=']) +WHITESPACE = frozenset(' \t') +SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, + tokenize.INDENT, tokenize.DEDENT]) +BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,') +RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,\s*\w+\s*,\s*\w+') +ERRORCODE_REGEX = re.compile(r'\b[A-Z]\d{3}\b') +DOCSTRING_REGEX = re.compile(r'u?r?["\']') +EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') +WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') +COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') +COMPARE_TYPE_REGEX = re.compile(r'(?:[=!]=|is(?:\s+not)?)\s*type(?:s.\w+Type' + r'|\s*\(\s*([^)]*[^ )])\s*\))') +KEYWORD_REGEX = re.compile(r'(\s*)\b(?:%s)\b(\s*)' % r'|'.join(KEYWORDS)) +OPERATOR_REGEX = re.compile(r'(?:[^,\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)') +LAMBDA_REGEX = re.compile(r'\blambda\b') +HUNK_REGEX = re.compile(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$') + +# Work around Python < 2.6 behaviour, which does not generate NL after +# a comment which is on a line by itself. +COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n' + + +############################################################################## +# Helper functions for translated and formatted messages +############################################################################## + + +pep8_messages = { + "E101": QT_TRANSLATE_NOOP("pep8", + "indentation contains mixed spaces and tabs"), + "E111": QT_TRANSLATE_NOOP("pep8", + "indentation is not a multiple of four"), + "E112": QT_TRANSLATE_NOOP("pep8", + "expected an indented block"), + "E113": QT_TRANSLATE_NOOP("pep8", + "unexpected indentation"), + "E121": QT_TRANSLATE_NOOP("pep8", + "continuation line indentation is not a multiple of four"), + "E122": QT_TRANSLATE_NOOP("pep8", + "continuation line missing indentation or outdented"), + "E123": QT_TRANSLATE_NOOP("pep8", + "closing bracket does not match indentation of opening bracket's line"), + "E124": QT_TRANSLATE_NOOP("pep8", + "closing bracket does not match visual indentation"), + "E125": QT_TRANSLATE_NOOP("pep8", + "continuation line does not distinguish itself from next logical line"), + "E126": QT_TRANSLATE_NOOP("pep8", + "continuation line over-indented for hanging indent"), + "E127": QT_TRANSLATE_NOOP("pep8", + "continuation line over-indented for visual indent"), + "E128": QT_TRANSLATE_NOOP("pep8", + "continuation line under-indented for visual indent"), + "E133": QT_TRANSLATE_NOOP("pep8", + "closing bracket is missing indentation"), + "W191": QT_TRANSLATE_NOOP("pep8", + "indentation contains tabs"), + "E201": QT_TRANSLATE_NOOP("pep8", + "whitespace after '{0}'"), + "E202": QT_TRANSLATE_NOOP("pep8", + "whitespace before '{0}'"), + "E203": QT_TRANSLATE_NOOP("pep8", + "whitespace before '{0}'"), + "E211": QT_TRANSLATE_NOOP("pep8", + "whitespace before '{0}'"), + "E221": QT_TRANSLATE_NOOP("pep8", + "multiple spaces before operator"), + "E222": QT_TRANSLATE_NOOP("pep8", + "multiple spaces after operator"), + "E223": QT_TRANSLATE_NOOP("pep8", + "tab before operator"), + "E224": QT_TRANSLATE_NOOP("pep8", + "tab after operator"), + "E225": QT_TRANSLATE_NOOP("pep8", + "missing whitespace around operator"), + "E226": QT_TRANSLATE_NOOP("pep8", + "missing whitespace around arithmetic operator"), + "E227": QT_TRANSLATE_NOOP("pep8", + "missing whitespace around bitwise or shift operator"), + "E228": QT_TRANSLATE_NOOP("pep8", + "missing whitespace around modulo operator"), + "E231": QT_TRANSLATE_NOOP("pep8", + "missing whitespace after '{0}'"), + "E241": QT_TRANSLATE_NOOP("pep8", + "multiple spaces after '{0}'"), + "E242": QT_TRANSLATE_NOOP("pep8", + "tab after '{0}'"), + "E251": QT_TRANSLATE_NOOP("pep8", + "unexpected spaces around keyword / parameter equals"), + "E261": QT_TRANSLATE_NOOP("pep8", + "at least two spaces before inline comment"), + "E262": QT_TRANSLATE_NOOP("pep8", + "inline comment should start with '# '"), + "E271": QT_TRANSLATE_NOOP("pep8", + "multiple spaces after keyword"), + "E272": QT_TRANSLATE_NOOP("pep8", + "multiple spaces before keyword"), + "E273": QT_TRANSLATE_NOOP("pep8", + "tab after keyword"), + "E274": QT_TRANSLATE_NOOP("pep8", + "tab before keyword"), + "W291": QT_TRANSLATE_NOOP("pep8", + "trailing whitespace"), + "W292": QT_TRANSLATE_NOOP("pep8", + "no newline at end of file"), + "W293": QT_TRANSLATE_NOOP("pep8", + "blank line contains whitespace"), + "E301": QT_TRANSLATE_NOOP("pep8", + "expected 1 blank line, found 0"), + "E302": QT_TRANSLATE_NOOP("pep8", + "expected 2 blank lines, found {0}"), + "E303": QT_TRANSLATE_NOOP("pep8", + "too many blank lines ({0})"), + "E304": QT_TRANSLATE_NOOP("pep8", + "blank lines found after function decorator"), + "W391": QT_TRANSLATE_NOOP("pep8", + "blank line at end of file"), + "E401": QT_TRANSLATE_NOOP("pep8", + "multiple imports on one line"), + "E501": QT_TRANSLATE_NOOP("pep8", + "line too long ({0} > {1} characters)"), + "E502": QT_TRANSLATE_NOOP("pep8", + "the backslash is redundant between brackets"), + "W601": QT_TRANSLATE_NOOP("pep8", + ".has_key() is deprecated, use 'in'"), + "W602": QT_TRANSLATE_NOOP("pep8", + "deprecated form of raising exception"), + "W603": QT_TRANSLATE_NOOP("pep8", + "'<>' is deprecated, use '!='"), + "W604": QT_TRANSLATE_NOOP("pep8", + "backticks are deprecated, use 'repr()'"), + "E701": QT_TRANSLATE_NOOP("pep8", + "multiple statements on one line (colon)"), + "E702": QT_TRANSLATE_NOOP("pep8", + "multiple statements on one line (semicolon)"), + "E703": QT_TRANSLATE_NOOP("pep8", + "statement ends with a semicolon"), + "E711": QT_TRANSLATE_NOOP("pep8", + "comparison to {0} should be {1}"), + "E712": QT_TRANSLATE_NOOP("pep8", + "comparison to {0} should be {1}"), + "E721": QT_TRANSLATE_NOOP("pep8", + "do not compare types, use 'isinstance()'"), + "E901": QT_TRANSLATE_NOOP("pep8", + "{0}: {1}"), +} + +pep8_messages_sample_args = { + "E201": ["([{"], + "E202": ["}])"], + "E203": [",;:"], + "E211": ["(["], + "E231": [",;:"], + "E241": [",;:"], + "E242": [",;:"], + "E302": [1], + "E303": [3], + "E501": [85, 79], + "E711": ["None", "'if cond is None:'"], + "E712": ["True", "'if cond is True:' or 'if cond:'"], + "E901": ["SyntaxError", "Invalid Syntax"], +} + + +def getMessage(code, *args): + """ + Function to get a translated and formatted message for a given code. + + @param code message code (string) + @param args arguments for a formatted message (list) + @return translated and formatted message (string) + """ + if code in pep8_messages: + return code + " " + QCoreApplication.translate("pep8", + pep8_messages[code]).format(*args) + else: + return code + " " + QCoreApplication.translate("pep8", + "no message for this code defined") + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if '\t' in indent: + return indent.index('\t'), "W191" + + +def trailing_whitespace(physical_line): + r""" + JCR: Trailing whitespace is superfluous. + FBM: Except when it occurs as part of a blank line (i.e. the line is + nothing but whitespace). According to Python docs[1] a line with only + whitespace is considered a blank line, and is to be ignored. However, + matching a blank line to its indentation level avoids mistakenly + terminating a multi-line statement (e.g. class declaration) when + pasting code into the standard Python interpreter. + + [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines + + The warning returned varies on whether the line itself is blank, for easier + filtering for those who want to indent their blank lines. + + Okay: spam(1)\n# + W291: spam(1) \n# + W293: class Foo(object):\n \n bang = 12 + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip(' \t\v') + if physical_line != stripped: + if stripped: + return len(stripped), "W291" + else: + return 0, "W293" + + +def trailing_blank_lines(physical_line, lines, line_number): + r""" + JCR: Trailing blank lines are superfluous. + + Okay: spam(1) + W391: spam(1)\n + """ + if not physical_line.rstrip() and line_number == len(lines): + return 0, "W391" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + + Reports warning W292. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292" + + +def maximum_line_length(physical_line, max_line_length): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + + Reports error E501. + """ + line = physical_line.rstrip() + length = len(line) + if length > max_line_length and not noqa(line): + if hasattr(line, 'decode'): # Python 2 + # The line could contain multi-byte characters + try: + length = len(line.decode('utf-8')) + except UnicodeError: + pass + if length > max_line_length: + return max_line_length, "E501", length, max_line_length + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, previous_indent_level): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number < 3 and not previous_logical: + return # Don't expect blank lines before the first line + if previous_logical.startswith('@'): + if blank_lines: + yield 0, "E304" + elif blank_lines > 2 or (indent_level and blank_lines == 2): + yield 0, "E303", blank_lines + elif logical_line.startswith(('def ', 'class ', '@')): + if indent_level: + if not (blank_lines or previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical)): + yield 0, "E301" + elif blank_lines != 2: + yield 0, "E302", blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): + text = match.group() + char = text.strip() + found = match.start() + if text == char + ' ': + # assert char in '([{' + yield found + 1, "E201", char + elif line[found - 1] != ',': + code = ('E202' if char in '}])' else 'E203') # if char in ',;:' + yield found, code, char + + +def whitespace_around_keywords(logical_line): + r""" + Avoid extraneous whitespace around keywords. + + Okay: True and False + E271: True and False + E272: True and False + E273: True and\tFalse + E274: True\tand False + """ + for match in KEYWORD_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E274" + elif len(before) > 1: + yield match.start(1), "E272" + + if '\t' in after: + yield match.start(2), "E273" + elif len(after) > 1: + yield match.start(2), "E271" + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + E231: [{'a':'b'}] + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']') and \ + before.rfind('{') < before.rfind('['): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + yield index, "E231", char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + yield 0, "E111" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + yield 0, "E112" + if indent_level > previous_indent_level and not indent_expect: + yield 0, "E113" + + +def continued_indentation(logical_line, tokens, indent_level, hang_closing, + noqa, verbose): + r""" + Continuation lines should align wrapped elements either vertically using + Python's implicit line joining inside parentheses, brackets and braces, or + using a hanging indent. + + When using a hanging indent the following considerations should be applied: + + - there should be no arguments on the first line, and + + - further indentation should be used to clearly distinguish itself as a + continuation line. + + Okay: a = (\n) + E123: a = (\n ) + + Okay: a = (\n 42) + E121: a = (\n 42) + E122: a = (\n42) + E123: a = (\n 42\n ) + E124: a = (24,\n 42\n) + E125: if (a or\n b):\n pass + E126: a = (\n 42) + E127: a = (24,\n 42) + E128: a = (24,\n 42) + """ + first_row = tokens[0][2][0] + nrows = 1 + tokens[-1][2][0] - first_row + if noqa or nrows == 1: + return + + # indent_next tells us whether the next block is indented; assuming + # that it is indented by 4 spaces, then we should not allow 4-space + # indents on the final continuation line; in turn, some other + # indents are allowed to have an extra 4 spaces. + indent_next = logical_line.endswith(':') + + row = depth = 0 + # remember how many brackets were opened on each line + parens = [0] * nrows + # relative indents of physical lines + rel_indent = [0] * nrows + # visual indents + indent_chances = {} + last_indent = tokens[0][2] + indent = [last_indent[1]] + if verbose >= 3: + print(">>> " + tokens[0][4].rstrip()) + + for token_type, text, start, end, line in tokens: + + last_token_multiline = (start[0] != end[0]) + newline = row < start[0] - first_row + if newline: + row = start[0] - first_row + newline = (not last_token_multiline and + token_type not in (tokenize.NL, tokenize.NEWLINE)) + + if newline: + # this is the beginning of a continuation line. + last_indent = start + if verbose >= 3: + print("... " + line.rstrip()) + + # record the initial indent. + rel_indent[row] = expand_indent(line) - indent_level + + if depth: + # a bracket expression in a continuation line. + # find the line that it was opened on + for open_row in range(row - 1, -1, -1): + if parens[open_row]: + break + else: + # an unbracketed continuation line (ie, backslash) + open_row = 0 + hang = rel_indent[row] - rel_indent[open_row] + close_bracket = (token_type == tokenize.OP and text in ']})') + visual_indent = (not close_bracket and hang > 0 and + indent_chances.get(start[1])) + + if close_bracket and indent[depth]: + # closing bracket for visual indent + if start[1] != indent[depth]: + yield start, "E124" + elif close_bracket and not hang: + # closing bracket matches indentation of opening bracket's line + if hang_closing: + yield start, "E133" + elif visual_indent is True: + # visual indent is verified + if not indent[depth]: + indent[depth] = start[1] + elif visual_indent in (text, str): + # ignore token lined up with matching one from a previous line + pass + elif indent[depth] and start[1] < indent[depth]: + # visual indent is broken + yield start, "E128" + elif hang == 4 or (indent_next and rel_indent[row] == 8): + # hanging indent is verified + if close_bracket and not hang_closing: + yield (start, "E123") + else: + # indent is broken + if hang <= 0: + error = "E122" + elif indent[depth]: + error = "E127" + elif hang % 4: + error = "E121" + else: + error = "E126" + yield start, error + + # look for visual indenting + if (parens[row] and token_type not in (tokenize.NL, tokenize.COMMENT) + and not indent[depth]): + indent[depth] = start[1] + indent_chances[start[1]] = True + if verbose >= 4: + print("bracket depth %s indent to %s" % (depth, start[1])) + # deal with implicit string concatenation + elif (token_type in (tokenize.STRING, tokenize.COMMENT) or + text in ('u', 'ur', 'b', 'br')): + indent_chances[start[1]] = str + # special case for the "if" statement because len("if (") == 4 + elif not indent_chances and not row and not depth and text == 'if': + indent_chances[end[1] + 1] = True + + # keep track of bracket depth + if token_type == tokenize.OP: + if text in '([{': + depth += 1 + indent.append(0) + parens[row] += 1 + if verbose >= 4: + print("bracket depth %s seen, col %s, visual min = %s" % + (depth, start[1], indent[depth])) + elif text in ')]}' and depth > 0: + # parent indents should not be more than this one + prev_indent = indent.pop() or last_indent[1] + for d in range(depth): + if indent[d] > prev_indent: + indent[d] = 0 + for ind in list(indent_chances): + if ind >= prev_indent: + del indent_chances[ind] + depth -= 1 + if depth: + indent_chances[indent[depth]] = True + for idx in range(row, -1, -1): + if parens[idx]: + parens[idx] -= 1 + rel_indent[row] = rel_indent[idx] + break + assert len(indent) == depth + 1 + if start[1] not in indent_chances: + # allow to line up tokens + indent_chances[start[1]] = text + + if indent_next and expand_indent(line) == indent_level + 4: + yield last_indent, "E125" + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type, prev_text, __, prev_end, __ = tokens[0] + for index in range(1, len(tokens)): + token_type, text, start, end, __ = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + (prev_type == tokenize.NAME or prev_text in '}])') and + # Syntax "class A (B):" is allowed, but avoid it + (index < 2 or tokens[index - 2][1] != 'class') and + # Allow "return (a.foo for a in range(5))" + not keyword.iskeyword(prev_text)): + yield prev_end, "E211", text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + for match in OPERATOR_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E223" + elif len(before) > 1: + yield match.start(1), "E221" + + if '\t' in after: + yield match.start(2), "E224" + elif len(after) > 1: + yield match.start(2), "E222" + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: alpha[:-i] + + E225: i=i+1 + E225: submitted +=1 + E225: x = x /2 - 1 + E225: z = x **y + E226: c = (a+b) * (a-b) + E226: hypot2 = x*x + y*y + E227: c = a|b + E228: msg = fmt%(errno, errmsg) + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start != prev_end: + # Found a (probably) needed space + if need_space is not True and not need_space[1]: + yield need_space[0], "E225" + need_space = False + elif text == '>' and prev_text in ('<', '-'): + # Tolerate the "<>" operator, even if running Python 3 + # Deal with Python 3's annotated return value "->" + pass + else: + if need_space is True or need_space[1]: + # A needed trailing space was not found + yield prev_end, "E225" + else: + code = 'E226' + if prev_text == '%': + code = 'E228' + elif prev_text not in ARITHMETIC_OP: + code = 'E227' + yield need_space[0], code + need_space = False + elif token_type == tokenize.OP and prev_end is not None: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in WS_NEEDED_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + # Check if the operator is being used as a binary operator + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + if prev_type == tokenize.OP: + binary_usage = (prev_text in '}])') + elif prev_type == tokenize.NAME: + binary_usage = (prev_text not in KEYWORDS) + else: + binary_usage = (prev_type not in SKIP_TOKENS) + + if binary_usage: + need_space = None + elif text in WS_OPTIONAL_OPERATORS: + need_space = None + + if need_space is None: + # Surrounding space is optional, but ensure that + # trailing space matches opening space + need_space = (prev_end, start != prev_end) + elif need_space and start == prev_end: + # A needed opening space was not found + yield prev_end, "E225" + need_space = False + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + JCR: This should also be applied around comma etc. + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line): + found = m.start() + 1 + if '\t' in m.group(): + yield found, "E242", m.group()[0] + else: + yield found, "E241", m.group()[0] + + +def whitespace_around_named_parameter_equals(logical_line, tokens): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + no_space = False + prev_end = None + message = "E251" + for token_type, text, start, end, line in tokens: + if no_space: + no_space = False + if start != prev_end: + yield prev_end, message + elif token_type == tokenize.OP: + if text == '(': + parens += 1 + elif text == ')': + parens -= 1 + elif parens and text == '=': + no_space = True + if start != prev_end: + yield prev_end, message + prev_end = end + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + yield prev_end, "E261" + symbol, sp, comment = text.partition(' ') + if symbol not in ('#', '#:') or comment[:1].isspace(): + yield start, "E262" + elif token_type != tokenize.NL: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if -1 < found and ';' not in line[:found]: + yield found, "E401" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + E703: do_four(); # useless semicolon + """ + line = logical_line + last_char = len(line) - 1 + found = line.find(':') + while -1 < found < last_char: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + before.count('(') <= before.count(')') and # (Python 3 annotation) + not LAMBDA_REGEX.search(before)): # lambda x: x + yield found, "E701" + found = line.find(':', found + 1) + found = line.find(';') + while -1 < found: + if found < last_char: + yield found, "E702" + else: + yield found, "E703" + found = line.find(';', found + 1) + + +def explicit_line_join(logical_line, tokens): + r""" + Avoid explicit line join between brackets. + + The preferred way of wrapping long lines is by using Python's implied line + continuation inside parentheses, brackets and braces. Long lines can be + broken over multiple lines by wrapping expressions in parentheses. These + should be used in preference to using a backslash for line continuation. + + E502: aaa = [123, \\n 123] + E502: aaa = ("bbb " \\n "ccc") + + Okay: aaa = [123,\n 123] + Okay: aaa = ("bbb "\n "ccc") + Okay: aaa = "bbb " \\n "ccc" + """ + prev_start = prev_end = parens = 0 + backslash = None + for token_type, text, start, end, line in tokens: + if start[0] != prev_start and parens and backslash: + yield backslash, "E502" + if end[0] != prev_end: + if line.rstrip('\r\n').endswith('\\'): + backslash = (end[0], len(line.splitlines()[-1]) - 1) + else: + backslash = None + prev_start = prev_end = end[0] + else: + prev_start = start[0] + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in ')]}': + parens -= 1 + + +def comparison_to_singleton(logical_line, noqa): + """ + Comparisons to singletons like None should always be done + with "is" or "is not", never the equality operators. + + Okay: if arg is not None: + E711: if arg != None: + E712: if arg == True: + + Also, beware of writing if x when you really mean if x is not None -- + e.g. when testing whether a variable or argument that defaults to None was + set to some other value. The other value might have a type (such as a + container) that could be false in a boolean context! + """ + match = not noqa and COMPARE_SINGLETON_REGEX.search(logical_line) + if match: + same = (match.group(1) == '==') + singleton = match.group(2) + msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) + if singleton in ('None',): + code = 'E711' + else: + code = 'E712' + nonzero = ((singleton == 'True' and same) or + (singleton == 'False' and not same)) + msg += " or 'if %scond:'" % ('' if nonzero else 'not ') + yield match.start(1), code, singleton, msg + + +def comparison_type(logical_line): + """ + Object type comparisons should always use isinstance() instead of + comparing types directly. + + Okay: if isinstance(obj, int): + E721: if type(obj) is type(1): + + When checking if an object is a string, keep in mind that it might be a + unicode string too! In Python 2.3, str and unicode have a common base + class, basestring, so you can do: + + Okay: if isinstance(obj, basestring): + Okay: if type(a1) is type(b1): + """ + match = COMPARE_TYPE_REGEX.search(logical_line) + if match: + inst = match.group(1) + if inst and isidentifier(inst) and inst not in SINGLETONS: + return # Allow comparison for types which are not obvious + yield match.start(), "E721" + + +def python_3000_has_key(logical_line): + r""" + The {}.has_key() method is removed in the Python 3. + Use the 'in' operation instead. + + Okay: if "alph" in d:\n print d["alph"] + W601: assert d.has_key('alph') + """ + pos = logical_line.find('.has_key(') + if pos > -1: + yield pos, "W601" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form is removed in Python 3. + + Okay: raise DummyError("Message") + W602: raise DummyError, "Message" + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match and not RERAISE_COMMA_REGEX.match(logical_line): + yield match.end() - 1, "W602" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3. + + Okay: if a != 'no': + W603: if a <> 'no': + """ + pos = logical_line.find('<>') + if pos > -1: + yield pos, "W603" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3. + Use repr() instead. + + Okay: val = repr(1 + 2) + W604: val = `1 + 2` + """ + pos = logical_line.find('`') + if pos > -1: + yield pos, "W604" + + +############################################################################## +# Helper functions +############################################################################## + + +if '' == ''.encode(): + # Python 2: implicit encoding. + def readlines(filename): + f = open(filename) + try: + return f.readlines() + finally: + f.close() + isidentifier = re.compile(r'[a-zA-Z_]\w*').match + stdin_get_value = sys.stdin.read +else: + # Python 3 + def readlines(filename): # __IGNORE_WARNING__ + f = open(filename, 'rb') + try: + coding, lines = tokenize.detect_encoding(f.readline) + f = TextIOWrapper(f, coding, line_buffering=True) + return [l.decode(coding) for l in lines] + f.readlines() + except (LookupError, SyntaxError, UnicodeError): + f.close() + # Fall back if files are improperly declared + f = open(filename, encoding='latin-1') + return f.readlines() + finally: + f.close() + isidentifier = str.isidentifier + + def stdin_get_value(): + return TextIOWrapper(sys.stdin.buffer, errors='ignore').read() +readlines.__doc__ = " Read the source code." +noqa = re.compile(r'# no(?:qa|pep8)\b', re.I).search + + +def expand_indent(line): + r""" + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 16 + """ + if '\t' not in line: + return len(line) - len(line.lstrip()) + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + # String modifiers (e.g. u or r) + start = text.index(text[-1]) + 1 + end = len(text) - 1 + # Triple quotes + if text[-3:] in ('"""', "'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def parse_udiff(diff, patterns=None, parent='.'): + """Return a dictionary of matching lines.""" + # For each file of the diff, the entry key is the filename, + # and the value is a set of row numbers to consider. + rv = {} + path = nrows = None + for line in diff.splitlines(): + if nrows: + if line[:1] != '-': + nrows -= 1 + continue + if line[:3] == '@@ ': + hunk_match = HUNK_REGEX.match(line) + row, nrows = [int(g or '1') for g in hunk_match.groups()] + rv[path].update(range(row, row + nrows)) + elif line[:3] == '+++': + path = line[4:].split('\t', 1)[0] + if path[:2] == 'b/': + path = path[2:] + rv[path] = set() + return dict([(os.path.join(parent, path), rows) + for (path, rows) in rv.items() + if rows and filename_match(path, patterns)]) + + +def filename_match(filename, patterns, default=True): + """ + Check if patterns contains a pattern that matches filename. + If patterns is unspecified, this always returns True. + """ + if not patterns: + return default + return any(fnmatch(filename, pattern) for pattern in patterns) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +_checks = {'physical_line': {}, 'logical_line': {}, 'tree': {}} + + +def register_check(check, codes=None): + """ + Register a new check object. + """ + def _add_check(check, kind, codes, args): + if check in _checks[kind]: + _checks[kind][check][0].extend(codes or []) + else: + _checks[kind][check] = (codes or [''], args) + if inspect.isfunction(check): + args = inspect.getargspec(check)[0] + if args and args[0] in ('physical_line', 'logical_line'): + if codes is None: + codes = ERRORCODE_REGEX.findall(check.__doc__ or '') + _add_check(check, args[0], codes, args) + elif inspect.isclass(check): + if inspect.getargspec(check.__init__)[0][:2] == ['self', 'tree']: + _add_check(check, 'tree', codes, None) + + +def init_checks_registry(): + """ + Register all globally visible functions where the first argument name + is 'physical_line' or 'logical_line'. + """ + mod = inspect.getmodule(register_check) + for (name, function) in inspect.getmembers(mod, inspect.isfunction): + register_check(function) +init_checks_registry() + + +class Checker(object): + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename=None, lines=None, + options=None, report=None, **kwargs): + if options is None: + options = StyleGuide(kwargs).options + else: + assert not kwargs + self._io_error = None + self._physical_checks = options.physical_checks + self._logical_checks = options.logical_checks + self._ast_checks = options.ast_checks + self.max_line_length = options.max_line_length + self.hang_closing = options.hang_closing + self.verbose = options.verbose + self.filename = filename + if filename is None: + self.filename = 'stdin' + self.lines = lines or [] + elif filename == '-': + self.filename = 'stdin' + self.lines = stdin_get_value().splitlines(True) + elif lines is None: + try: + self.lines = readlines(filename) + except IOError: + exc_type, exc = sys.exc_info()[:2] + self._io_error = '%s: %s' % (exc_type.__name__, exc) + self.lines = [] + else: + self.lines = lines + if self.lines: + ord0 = ord(self.lines[0][0]) + if ord0 in (0xef, 0xfeff): # Strip the UTF-8 BOM + if ord0 == 0xfeff: + self.lines[0] = self.lines[0][1:] + elif self.lines[0][:3] == '\xef\xbb\xbf': + self.lines[0] = self.lines[0][3:] + self.report = report or options.report + self.report_error = self.report.error + self.report_error_args = self.report.error_args + + # added for eric5 integration + self.options = options + + def report_invalid_syntax(self): + exc_type, exc = sys.exc_info()[:2] + if len(exc.args) > 1: + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + else: + offset = (1, 0) + self.report_error_args(offset[0], offset[1] or 0, + 'E901', self.report_invalid_syntax, + exc_type.__name__, exc.args[0]) + report_invalid_syntax.__doc__ = " Check if the syntax is valid." + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and line[:1] in WHITESPACE: + self.indent_char = line[0] + for name, check, argument_names in self._physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, code, *args = result + self.report_error_args(self.line_number, offset, code, check, + *args) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + comments = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type == tokenize.COMMENT: + comments.append(text) + continue + if token_type in SKIP_TOKENS: + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_row, end = previous[3] + start_row, start = token[2] + if end_row != start_row: # different row + prev_text = self.lines[end_row - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_row - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + self.noqa = comments and noqa(''.join(comments)) + # With Python 2, if the line ends with '\r\r\n' the assertion fails + # assert self.logical_line.strip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + self.build_tokens_line() + self.report.increment_logical_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if self.verbose >= 2: + print(self.logical_line[:80].rstrip()) + for name, check, argument_names in self._logical_checks: + if self.verbose >= 4: + print(' ' + name) + for result in self.run_check(check, argument_names): + offset, code, *args = result + if isinstance(offset, tuple): + orig_number, orig_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + orig_number = token[2][0] + orig_offset = (token[2][1] + offset - token_offset) + self.report_error_args(orig_number, orig_offset, code, check, + *args) + self.previous_logical = self.logical_line + + def check_ast(self): + try: + tree = compile(''.join(self.lines), '', 'exec', PyCF_ONLY_AST) + except (SyntaxError, TypeError): + return self.report_invalid_syntax() + for name, cls, _ in self._ast_checks: + # extended API for eric5 integration + checker = cls(tree, self.filename, self.options) + for lineno, offset, code, check, *args in checker.run(): + if not noqa(self.lines[lineno - 1]): + self.report_error_args(lineno, offset, code, check, *args) + + def generate_tokens(self): + if self._io_error: + self.report_error(1, 0, 'E902 %s' % self._io_error, readlines) + tokengen = tokenize.generate_tokens(self.readline_check_physical) + try: + for token in tokengen: + yield token + except (SyntaxError, tokenize.TokenError): + self.report_invalid_syntax() + + def check_all(self, expected=None, line_offset=0): + """ + Run all checks on the input file. + """ + self.report.init_file(self.filename, self.lines, expected, line_offset) + if self._ast_checks: + self.check_ast() + self.line_number = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + parens = 0 + for token in self.generate_tokens(): + self.tokens.append(token) + token_type, text = token[0:2] + if self.verbose >= 3: + if token[2][0] == token[3][0]: + pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) + else: + pos = 'l.%s' % token[3][0] + print('l.%s\t%s\t%s\t%r' % + (token[2][0], pos, tokenize.tok_name[token[0]], text)) + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in '}])': + parens -= 1 + elif not parens: + if token_type == tokenize.NEWLINE: + if self.blank_lines < blank_lines_before_comment: + self.blank_lines = blank_lines_before_comment + self.check_logical() + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + elif token_type == tokenize.NL: + if len(self.tokens) == 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + elif token_type == tokenize.COMMENT and len(self.tokens) == 1: + if blank_lines_before_comment < self.blank_lines: + blank_lines_before_comment = self.blank_lines + self.blank_lines = 0 + if COMMENT_WITH_NL: + # The comment also ends a physical line + self.tokens = [] + return self.report.get_file_results() + + +class BaseReport(object): + """Collect the results of the checks.""" + print_filename = False + + def __init__(self, options): + self._benchmark_keys = options.benchmark_keys + self._ignore_code = options.ignore_code + # Results + self.elapsed = 0 + self.total_errors = 0 + self.counters = dict.fromkeys(self._benchmark_keys, 0) + self.messages = {} + + def start(self): + """Start the timer.""" + self._start_time = time.time() + + def stop(self): + """Stop the timer.""" + self.elapsed = time.time() - self._start_time + + def init_file(self, filename, lines, expected, line_offset): + """Signal a new file.""" + self.filename = filename + self.lines = lines + self.expected = expected or () + self.line_offset = line_offset + self.file_errors = 0 + self.counters['files'] += 1 + self.counters['physical lines'] += len(lines) + + def increment_logical_line(self): + """Signal a new logical line.""" + self.counters['logical lines'] += 1 + + def error(self, line_number, offset, text, check): + """Report an error, according to options.""" + code = text[:4] + if self._ignore_code(code): + return + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + self.messages[code] = text[5:] + # Don't care about expected errors or warnings + if code in self.expected: + return + if self.print_filename and not self.file_errors: + print(self.filename) + self.file_errors += 1 + self.total_errors += 1 + return code + + def error_args(self, line_number, offset, code, check, *args): + """Report an error, according to options.""" + if self._ignore_code(code): + return + text = getMessage(code, *args) + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + self.messages[code] = text[5:] + # Don't care about expected errors or warnings + if code in self.expected: + return + if self.print_filename and not self.file_errors: + print(self.filename) + self.file_errors += 1 + self.total_errors += 1 + return code + + def get_file_results(self): + """Return the count of errors and warnings for this file.""" + return self.file_errors + + def get_count(self, prefix=''): + """Return the total count of errors and warnings.""" + return sum([self.counters[key] + for key in self.messages if key.startswith(prefix)]) + + def get_statistics(self, prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + return ['%-7s %s %s' % (self.counters[key], key, self.messages[key]) + for key in sorted(self.messages) if key.startswith(prefix)] + + def print_statistics(self, prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in self.get_statistics(prefix): + print(line) + + def print_benchmark(self): + """Print benchmark numbers.""" + print('%-7.2f %s' % (self.elapsed, 'seconds elapsed')) + if self.elapsed: + for key in self._benchmark_keys: + print('%-7d %s per second (%d total)' % + (self.counters[key] / self.elapsed, key, + self.counters[key])) + + +class FileReport(BaseReport): + """Collect the results of the checks and print only the filenames.""" + print_filename = True + + +class StandardReport(BaseReport): + """Collect and print the results of the checks.""" + + def __init__(self, options): + super(StandardReport, self).__init__(options) + self._fmt = REPORT_FORMAT.get(options.format.lower(), + options.format) + self._repeat = options.repeat + self._show_source = options.show_source + self._show_pep8 = options.show_pep8 + + def init_file(self, filename, lines, expected, line_offset): + """Signal a new file.""" + self._deferred_print = [] + return super(StandardReport, self).init_file( + filename, lines, expected, line_offset) + + def error(self, line_number, offset, text, check): + """Report an error, according to options.""" + code = super(StandardReport, self).error(line_number, offset, + text, check) + if code and (self.counters[code] == 1 or self._repeat): + self._deferred_print.append( + (line_number, offset, code, text[5:], check.__doc__)) + return code + + def error_args(self, line_number, offset, code, check, *args): + """Report an error, according to options.""" + code = super(StandardReport, self).error_args(line_number, offset, + code, check, *args) + if code and (self.counters[code] == 1 or self._repeat): + text = getMessage(code, *args) + self._deferred_print.append( + (line_number, offset, code, text[5:], check.__doc__)) + return code + + def get_file_results(self): + """Print the result and return the overall count for this file.""" + self._deferred_print.sort() + for line_number, offset, code, text, doc in self._deferred_print: + print(self._fmt % { + 'path': self.filename, + 'row': self.line_offset + line_number, 'col': offset + 1, + 'code': code, 'text': text, + }) + if self._show_source: + if line_number > len(self.lines): + line = '' + else: + line = self.lines[line_number - 1] + print(line.rstrip()) + print(' ' * offset + '^') + if self._show_pep8 and doc: + print(doc.lstrip('\n').rstrip()) + return self.file_errors + + +class DiffReport(StandardReport): + """Collect and print the results for the changed lines only.""" + + def __init__(self, options): + super(DiffReport, self).__init__(options) + self._selected = options.selected_lines + + def error(self, line_number, offset, text, check): + if line_number not in self._selected[self.filename]: + return + return super(DiffReport, self).error(line_number, offset, text, check) + + +class StyleGuide(object): + """Initialize a PEP-8 instance with few options.""" + + def __init__(self, *args, **kwargs): + # build options from the command line + self.checker_class = kwargs.pop('checker_class', Checker) + parse_argv = kwargs.pop('parse_argv', False) + config_file = kwargs.pop('config_file', None) + parser = kwargs.pop('parser', None) + options, self.paths = process_options( + parse_argv=parse_argv, config_file=config_file, parser=parser) + if args or kwargs: + # build options from dict + options_dict = dict(*args, **kwargs) + options.__dict__.update(options_dict) + if 'paths' in options_dict: + self.paths = options_dict['paths'] + + self.runner = self.input_file + self.options = options + + if not options.reporter: + options.reporter = BaseReport if options.quiet else StandardReport + + for index, value in enumerate(options.exclude): + options.exclude[index] = value.rstrip('/') + options.select = tuple(options.select or ()) + if not (options.select or options.ignore or + options.testsuite or options.doctest) and DEFAULT_IGNORE: + # The default choice: ignore controversial checks + options.ignore = tuple(DEFAULT_IGNORE.split(',')) + else: + # Ignore all checks which are not explicitly selected + options.ignore = ('',) if options.select else tuple(options.ignore) + options.benchmark_keys = BENCHMARK_KEYS[:] + options.ignore_code = self.ignore_code + options.physical_checks = self.get_checks('physical_line') + options.logical_checks = self.get_checks('logical_line') + options.ast_checks = self.get_checks('tree') + self.init_report() + + def init_report(self, reporter=None): + """Initialize the report instance.""" + self.options.report = (reporter or self.options.reporter)(self.options) + return self.options.report + + def check_files(self, paths=None): + """Run all checks on the paths.""" + if paths is None: + paths = self.paths + report = self.options.report + runner = self.runner + report.start() + try: + for path in paths: + if os.path.isdir(path): + self.input_dir(path) + elif not self.excluded(path): + runner(path) + except KeyboardInterrupt: + print('... stopped') + report.stop() + return report + + def input_file(self, filename, lines=None, expected=None, line_offset=0): + """Run all checks on a Python source file.""" + if self.options.verbose: + print('checking %s' % filename) + fchecker = self.checker_class( + filename, lines=lines, options=self.options) + return fchecker.check_all(expected=expected, line_offset=line_offset) + + def input_dir(self, dirname): + """Check all files in this directory and all subdirectories.""" + dirname = dirname.rstrip('/') + if self.excluded(dirname): + return 0 + counters = self.options.report.counters + verbose = self.options.verbose + filepatterns = self.options.filename + runner = self.runner + for root, dirs, files in os.walk(dirname): + if verbose: + print('directory ' + root) + counters['directories'] += 1 + for subdir in sorted(dirs): + if self.excluded(subdir, root): + dirs.remove(subdir) + for filename in sorted(files): + # contain a pattern that matches? + if ((filename_match(filename, filepatterns) and + not self.excluded(filename, root))): + runner(os.path.join(root, filename)) + + def excluded(self, filename, parent=None): + """ + Check if options.exclude contains a pattern that matches filename. + """ + if not self.options.exclude: + return False + basename = os.path.basename(filename) + if filename_match(basename, self.options.exclude): + return True + if parent: + filename = os.path.join(parent, filename) + return filename_match(filename, self.options.exclude) + + def ignore_code(self, code): + """ + Check if the error code should be ignored. + + If 'options.select' contains a prefix of the error code, + return False. Else, if 'options.ignore' contains a prefix of + the error code, return True. + """ + return (code.startswith(self.options.ignore) and + not code.startswith(self.options.select)) + + def get_checks(self, argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name and which contain selected tests. + """ + checks = [] + for check, attrs in _checks[argument_name].items(): + (codes, args) = attrs + if any(not (code and self.ignore_code(code)) for code in codes): + checks.append((check.__name__, check, args)) + return sorted(checks) + + +def get_parser(prog='pep8', version=__version__): + parser = OptionParser(prog=prog, version=version, + usage="%prog [options] input ...") + parser.config_options = [ + 'exclude', 'filename', 'select', 'ignore', 'max-line-length', + 'hang-closing', 'count', 'format', 'quiet', 'show-pep8', + 'show-source', 'statistics', 'verbose'] + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', default=True, action='store_true', + help="(obsolete) show all occurrences of the same error") + parser.add_option('--first', action='store_false', dest='repeat', + help="show first occurrence of each error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %default)") + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns " + "(default: %default)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error " + "(implies --first)") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--max-line-length', type='int', metavar='n', + default=MAX_LINE_LENGTH, + help="set maximum allowed line length " + "(default: %default)") + parser.add_option('--hang-closing', action='store_true', + help="hang closing bracket instead of matching " + "indentation of opening bracket's line") + parser.add_option('--format', metavar='format', default='default', + help="set the error format [default|pylint|<custom>]") + parser.add_option('--diff', action='store_true', + help="report only lines changed according to the " + "unified diff received on STDIN") + group = parser.add_option_group("Testing Options") + if os.path.exists(TESTSUITE_PATH): + group.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + group.add_option('--doctest', action='store_true', + help="run doctest on myself") + group.add_option('--benchmark', action='store_true', + help="measure processing speed") + return parser + + +def read_config(options, args, arglist, parser): + """Read both user configuration and local configuration.""" + config = RawConfigParser() + + user_conf = options.config + if user_conf and os.path.isfile(user_conf): + if options.verbose: + print('user configuration: %s' % user_conf) + config.read(user_conf) + + parent = tail = args and os.path.abspath(os.path.commonprefix(args)) + while tail: + if config.read([os.path.join(parent, fn) for fn in PROJECT_CONFIG]): + if options.verbose: + print('local configuration: in %s' % parent) + break + parent, tail = os.path.split(parent) + + pep8_section = parser.prog + if config.has_section(pep8_section): + option_list = dict([(o.dest, o.type or o.action) + for o in parser.option_list]) + + # First, read the default values + new_options, _ = parser.parse_args([]) + + # Second, parse the configuration + for opt in config.options(pep8_section): + if options.verbose > 1: + print(" %s = %s" % (opt, config.get(pep8_section, opt))) + if opt.replace('_', '-') not in parser.config_options: + print("Unknown option: '%s'\n not in [%s]" % + (opt, ' '.join(parser.config_options))) + sys.exit(1) + normalized_opt = opt.replace('-', '_') + opt_type = option_list[normalized_opt] + if opt_type in ('int', 'count'): + value = config.getint(pep8_section, opt) + elif opt_type == 'string': + value = config.get(pep8_section, opt) + else: + assert opt_type in ('store_true', 'store_false') + value = config.getboolean(pep8_section, opt) + setattr(new_options, normalized_opt, value) + + # Third, overwrite with the command-line options + options, _ = parser.parse_args(arglist, values=new_options) + options.doctest = options.testsuite = False + return options + + +def process_options(arglist=None, parse_argv=False, config_file=None, + parser=None): + """Process options passed either via arglist or via command line args.""" + if not arglist and not parse_argv: + # Don't read the command line if the module is used as a library. + arglist = [] + if not parser: + parser = get_parser() + if not parser.has_option('--config'): + if config_file is True: + config_file = DEFAULT_CONFIG + group = parser.add_option_group("Configuration", description=( + "The project options are read from the [%s] section of the " + "tox.ini file or the setup.cfg file located in any parent folder " + "of the path(s) being processed. Allowed options are: %s." % + (parser.prog, ', '.join(parser.config_options)))) + group.add_option('--config', metavar='path', default=config_file, + help="user config file location (default: %default)") + options, args = parser.parse_args(arglist) + options.reporter = None + + if options.ensure_value('testsuite', False): + args.append(options.testsuite) + elif not options.ensure_value('doctest', False): + if parse_argv and not args: + if options.diff or any(os.path.exists(name) + for name in PROJECT_CONFIG): + args = ['.'] + else: + parser.error('input not specified') + options = read_config(options, args, arglist, parser) + options.reporter = parse_argv and options.quiet == 1 and FileReport + + options.filename = options.filename and options.filename.split(',') + options.exclude = options.exclude.split(',') + options.select = options.select and options.select.split(',') + options.ignore = options.ignore and options.ignore.split(',') + + if options.diff: + options.reporter = DiffReport + stdin = stdin_get_value() + options.selected_lines = parse_udiff(stdin, options.filename, args[0]) + args = sorted(options.selected_lines) + + return options, args + + +def _main(): + """Parse options and run checks on Python source.""" + pep8style = StyleGuide(parse_argv=True, config_file=True) + options = pep8style.options + if options.doctest or options.testsuite: + from testsuite.support import run_tests + report = run_tests(pep8style) + else: + report = pep8style.check_files() + if options.statistics: + report.print_statistics() + if options.benchmark: + report.print_benchmark() + if options.testsuite and not options.quiet: + report.print_results() + if report.total_errors: + if options.count: + sys.stderr.write(str(report.total_errors) + '\n') + sys.exit(1) + +if __name__ == '__main__': + _main()
--- a/Plugins/CheckerPlugins/Pep8/CodeStyleChecker.py Wed Oct 02 18:58:13 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing the code style checker. -""" - -import os - -from PyQt4.QtCore import QProcess, QCoreApplication - -from . import pep8 -from .NamingStyleChecker import NamingStyleChecker -from .DocStyleChecker import DocStyleChecker - -import Preferences -import Utilities - -from eric5config import getConfig - - -class CodeStyleCheckerPy2(object): - """ - Class implementing the code style checker interface for Python 2. - """ - def __init__(self, filename, lines, repeat=False, - select="", ignore="", max_line_length=79, - hang_closing=False, docType="pep257"): - """ - Constructor - - @param filename name of the file to check (string) - @param lines source of the file (list of strings) (ignored) - @keyparam repeat flag indicating to repeat message categories (boolean) - @keyparam select list of message IDs to check for - (comma separated string) - @keyparam ignore list of message IDs to ignore - (comma separated string) - @keyparam max_line_length maximum allowed line length (integer) - @keyparam hang_closing flag indicating to allow hanging closing - brackets (boolean) - @keyparam docType type of the documentation strings - (string, one of 'eric' or 'pep257') - """ - assert docType in ("eric", "pep257") - - self.errors = [] - self.counters = {} - - interpreter = Preferences.getDebugger("PythonInterpreter") - if interpreter == "" or not Utilities.isExecutable(interpreter): - self.errors.append((filename, 1, 1, - QCoreApplication.translate("CodeStyleCheckerPy2", - "Python2 interpreter not configured."))) - return - - checker = os.path.join(getConfig('ericDir'), - "UtilitiesPython2", "Pep8Checker.py") - - args = [checker] - if repeat: - args.append("-r") - if select: - args.append("-s") - args.append(select) - if ignore: - args.append("-i") - args.append(ignore) - args.append("-m") - args.append(str(max_line_length)) - if hang_closing: - args.append("-h") - args.append("-d") - args.append(docType) - args.append("-f") - args.append(filename) - - proc = QProcess() - proc.setProcessChannelMode(QProcess.MergedChannels) - proc.start(interpreter, args) - finished = proc.waitForFinished(15000) - if finished: - output = \ - str(proc.readAllStandardOutput(), - Preferences.getSystem("IOEncoding"), - 'replace').splitlines() - if output[0] == "ERROR": - self.errors.append((filename, 1, 1, output[2])) - return - - if output[0] == "NO_PEP8": - return - - index = 0 - while index < len(output): - if output[index] == "PEP8_STATISTICS": - index += 1 - break - - fname = output[index + 1] - lineno = int(output[index + 2]) - position = int(output[index + 3]) - code = output[index + 4] - arglen = int(output[index + 5]) - args = [] - argindex = 0 - while argindex < arglen: - args.append(output[index + 6 + argindex]) - argindex += 1 - index += 6 + arglen - - if code in NamingStyleChecker.Codes: - text = NamingStyleChecker.getMessage(code, *args) - elif code in DocStyleChecker.Codes: - text = DocStyleChecker.getMessage(code, *args) - else: - text = pep8.getMessage(code, *args) - self.errors.append((fname, lineno, position, text)) - while index < len(output): - code, countStr = output[index].split(None, 1) - self.counters[code] = int(countStr) - index += 1 - else: - self.errors.append((filename, 1, 1, - QCoreApplication.translate("CodeStyleCheckerPy2", - "Python2 interpreter did not finish within 15s.")))
--- a/Plugins/CheckerPlugins/Pep8/CodeStyleCheckerDialog.py Wed Oct 02 18:58:13 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,906 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2011 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a dialog to show the results of the code style check. -""" - -import os -import fnmatch - -from PyQt4.QtCore import pyqtSlot, Qt -from PyQt4.QtGui import QDialog, QTreeWidgetItem, QAbstractButton, \ - QDialogButtonBox, QApplication, QHeaderView, QIcon - -from E5Gui.E5Application import e5App - -from .Ui_CodeStyleCheckerDialog import Ui_CodeStyleCheckerDialog - -import UI.PixmapCache -import Preferences -import Utilities - -from . import pep8 -from .NamingStyleChecker import NamingStyleChecker - -# register the name checker -pep8.register_check(NamingStyleChecker, NamingStyleChecker.Codes) - -from .DocStyleChecker import DocStyleChecker - - -class CodeStyleCheckerReport(pep8.BaseReport): - """ - Class implementing a special report to be used with our dialog. - """ - def __init__(self, options): - """ - Constructor - - @param options options for the report (optparse.Values) - """ - super().__init__(options) - - self.__repeat = options.repeat - self.errors = [] - - def error_args(self, line_number, offset, code, check, *args): - """ - Public method to collect the error messages. - - @param line_number line number of the issue (integer) - @param offset position within line of the issue (integer) - @param code message code (string) - @param check reference to the checker function (function) - @param args arguments for the message (list) - @return error code (string) - """ - code = super().error_args(line_number, offset, code, check, *args) - if code and (self.counters[code] == 1 or self.__repeat): - if code in NamingStyleChecker.Codes: - text = NamingStyleChecker.getMessage(code, *args) - else: - text = pep8.getMessage(code, *args) - self.errors.append( - (self.filename, line_number, offset, text) - ) - return code - - -class CodeStyleCheckerDialog(QDialog, Ui_CodeStyleCheckerDialog): - """ - Class implementing a dialog to show the results of the code style check. - """ - filenameRole = Qt.UserRole + 1 - lineRole = Qt.UserRole + 2 - positionRole = Qt.UserRole + 3 - messageRole = Qt.UserRole + 4 - fixableRole = Qt.UserRole + 5 - codeRole = Qt.UserRole + 6 - - def __init__(self, parent=None): - """ - Constructor - - @param parent reference to the parent widget (QWidget) - """ - super().__init__(parent) - self.setupUi(self) - - self.docTypeComboBox.addItem(self.trUtf8("PEP-257"), "pep257") - self.docTypeComboBox.addItem(self.trUtf8("Eric"), "eric") - - self.statisticsButton = self.buttonBox.addButton( - self.trUtf8("Statistics..."), QDialogButtonBox.ActionRole) - self.statisticsButton.setToolTip( - self.trUtf8("Press to show some statistics for the last run")) - self.statisticsButton.setEnabled(False) - self.showButton = self.buttonBox.addButton( - self.trUtf8("Show"), QDialogButtonBox.ActionRole) - self.showButton.setToolTip( - self.trUtf8("Press to show all files containing an issue")) - self.showButton.setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) - - self.resultList.headerItem().setText(self.resultList.columnCount(), "") - self.resultList.header().setSortIndicator(0, Qt.AscendingOrder) - - self.checkProgress.setVisible(False) - self.checkProgressLabel.setVisible(False) - self.checkProgressLabel.setMaximumWidth(600) - - self.noResults = True - self.cancelled = False - self.__lastFileItem = None - - self.__fileOrFileList = "" - self.__project = None - self.__forProject = False - self.__data = {} - self.__statistics = {} - - self.on_loadDefaultButton_clicked() - - def __resort(self): - """ - Private method to resort the tree. - """ - self.resultList.sortItems(self.resultList.sortColumn(), - self.resultList.header().sortIndicatorOrder() - ) - - def __createResultItem(self, file, line, pos, message, fixed, autofixing): - """ - Private method to create an entry in the result list. - - @param file file name of the file (string) - @param line line number of issue (integer or string) - @param pos character position of issue (integer or string) - @param message message text (string) - @param fixed flag indicating a fixed issue (boolean) - @param autofixing flag indicating, that we are fixing issues - automatically (boolean) - @return reference to the created item (QTreeWidgetItem) - """ - from .CodeStyleFixer import FixableCodeStyleIssues - - if self.__lastFileItem is None: - # It's a new file - self.__lastFileItem = QTreeWidgetItem(self.resultList, [file]) - self.__lastFileItem.setFirstColumnSpanned(True) - self.__lastFileItem.setExpanded(True) - self.__lastFileItem.setData(0, self.filenameRole, file) - - fixable = False - code, message = message.split(None, 1) - itm = QTreeWidgetItem(self.__lastFileItem, - ["{0:6}".format(line), code, message]) - if code.startswith("W"): - itm.setIcon(1, UI.PixmapCache.getIcon("warning.png")) - elif code.startswith("N"): - itm.setIcon(1, UI.PixmapCache.getIcon("namingError.png")) - elif code.startswith("D"): - itm.setIcon(1, UI.PixmapCache.getIcon("docstringError.png")) - else: - itm.setIcon(1, UI.PixmapCache.getIcon("syntaxError.png")) - if fixed: - itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed.png")) - elif code in FixableCodeStyleIssues and not autofixing: - itm.setIcon(0, UI.PixmapCache.getIcon("issueFixable.png")) - fixable = True - - itm.setTextAlignment(0, Qt.AlignRight) - itm.setTextAlignment(1, Qt.AlignHCenter) - - itm.setTextAlignment(0, Qt.AlignVCenter) - itm.setTextAlignment(1, Qt.AlignVCenter) - itm.setTextAlignment(2, Qt.AlignVCenter) - - itm.setData(0, self.filenameRole, file) - itm.setData(0, self.lineRole, int(line)) - itm.setData(0, self.positionRole, int(pos)) - itm.setData(0, self.messageRole, message) - itm.setData(0, self.fixableRole, fixable) - itm.setData(0, self.codeRole, code) - - return itm - - def __modifyFixedResultItem(self, itm, text, fixed): - """ - Private method to modify a result list entry to show its - positive fixed state. - - @param itm reference to the item to modify (QTreeWidgetItem) - @param text text to be appended (string) - @param fixed flag indicating a fixed issue (boolean) - """ - if fixed: - message = itm.data(0, self.messageRole) + text - itm.setText(2, message) - itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed.png")) - - itm.setData(0, self.messageRole, message)