diff -r 1843ef6e2656 -r ce77f0b1ee67 Plugins/CheckerPlugins/Pep8/Pep8Dialog.py --- a/Plugins/CheckerPlugins/Pep8/Pep8Dialog.py Mon Aug 12 22:21:53 2013 +0200 +++ b/Plugins/CheckerPlugins/Pep8/Pep8Dialog.py Sun Sep 08 19:04:07 2013 +0200 @@ -11,7 +11,6 @@ import os import fnmatch -import tokenize from PyQt4.QtCore import pyqtSlot, Qt from PyQt4.QtGui import QDialog, QTreeWidgetItem, QAbstractButton, \ @@ -28,6 +27,40 @@ import Utilities +class Pep8Report(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(Pep8Report, self).__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) + """ + code = super(Pep8Report, self).error_args(line_number, offset, code, check, *args) + if code and (self.counters[code] == 1 or self.__repeat): + text = pep8.getMessage(code, *args) + self.errors.append( + (self.filename, line_number, offset, text) + ) + return code + + class Pep8Dialog(QDialog, Ui_Pep8Dialog): """ Class implementing a dialog to show the results of the PEP 8 check. @@ -36,8 +69,8 @@ lineRole = Qt.UserRole + 2 positionRole = Qt.UserRole + 3 messageRole = Qt.UserRole + 4 - - settingsKey = "PEP8/" + fixableRole = Qt.UserRole + 5 + codeRole = Qt.UserRole + 6 def __init__(self, parent=None): """ @@ -103,6 +136,7 @@ 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]) @@ -114,6 +148,7 @@ itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed.png")) elif code in Pep8FixableIssues: itm.setIcon(0, UI.PixmapCache.getIcon("issueFixable.png")) + fixable = True itm.setTextAlignment(0, Qt.AlignRight) itm.setTextAlignment(1, Qt.AlignHCenter) @@ -126,40 +161,24 @@ itm.setData(0, self.lineRole, int(line)) itm.setData(0, self.positionRole, int(pos)) itm.setData(0, self.messageRole, message) - - def __createErrorItem(self, file, line, pos, message): - """ - 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) + itm.setData(0, self.fixableRole, fixable) + itm.setData(0, self.codeRole, code) + + def __modifyFixedResultItem(self, itm, text): """ - 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) - - itm = QTreeWidgetItem(self.__lastFileItem, - ["{0:6}".format(line), '', message]) - itm.setIcon(0, UI.PixmapCache.getIcon("syntaxError.png")) + Private method to modify a result list entry to show its + positive fixed state. - itm.setTextAlignment(0, Qt.AlignRight) - itm.setTextAlignment(1, Qt.AlignHCenter) + @param itm reference to the item to modify (QTreeWidgetItem) + @param text text to be appended (string) + """ + message = itm.data(0, self.messageRole) + text + itm.setText(2, message) + itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed.png")) - 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, False) + def __updateStatistics(self, statistics, fixer): """ Private method to update the collected statistics. @@ -179,6 +198,14 @@ 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 PEP 8 fixer (Pep8Fixer) + """ + self.__statistics["_IssuesFixed"] += fixer.fixed + def __resetStatistics(self): """ Private slot to reset the statistics data. @@ -205,8 +232,7 @@ self.__data = self.__project.getData("CHECKERSPARMS", "Pep8Checker") if self.__data is None or \ - "ExcludeFiles" not in self.__data or \ - len(self.__data) != 6: + len(self.__data) < 6: # initialize the data structure self.__data = { "ExcludeFiles": "", @@ -216,12 +242,22 @@ "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" + 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"]) def start(self, fn, save=False, repeat=None): """ @@ -243,6 +279,7 @@ self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.statisticsButton.setEnabled(False) self.showButton.setEnabled(False) + self.fixButton.setEnabled(False) if repeat is not None: self.repeatCheckBox.setChecked(repeat) QApplication.processEvents() @@ -282,7 +319,10 @@ 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() try: # disable updates of the list for speed @@ -315,50 +355,49 @@ if fixIssues: from .Pep8Fixer import Pep8Fixer fixer = Pep8Fixer(self.__project, file, source, - fixCodes, True) # always fix in place + fixCodes, noFixCodes, maxLineLength, + True) # always fix in place else: fixer = None - from .Pep8Checker import Pep8Checker - checker = Pep8Checker(file, source, - repeat=repeatMessages, - select=includeMessages, - ignore=excludeMessages) - try: - checker.check_all() - except tokenize.TokenError as msg: - self.noResults = False - self.__createErrorItem(file, 1, -1, - self.trUtf8("Token Error: {0}".format(str(msg)))) - except IndentationError as err: - self.noResults = False - self.__createErrorItem(file, err.lineno, -1, - self.trUtf8("Indentation Error: {0}".format(str(err.msg)))) - except Exception as err: - self.noResults = False - self.__createErrorItem(file, 1, -1, - self.trUtf8("Unspecific Error: {0}".format(str(err)))) + 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: - checker.messages.sort(key=lambda a: a[1]) - for message in checker.messages: - fname, lineno, position, text = message - if lineno > len(source): - lineno = len(source) - if "__IGNORE_WARNING__" not in Utilities.extractLineFlags( - source[lineno - 1].strip()): - self.noResults = False - fixed = False - if fixer: - fixed, msg = fixer.fixIssue(lineno, position, text) - if fixed: - text += "\n" + \ - self.trUtf8("Fix: {0}").format(msg) - self.__createResultItem( - fname, lineno, position, text, fixed) - if fixer: - fixer.saveFile(encoding) - self.__updateStatistics(checker.statistics, fixer) - finally: - progress += 1 + ignore = [] + styleGuide = pep8.StyleGuide( + reporter=Pep8Report, + repeat=repeatMessages, + select=select, + ignore=ignore, + max_line_length=maxLineLength, + hang_closing=hangClosing, + ) + report = styleGuide.check_files([file]) + report.errors.sort(key=lambda a: a[1]) + + for fname, lineno, position, text in report.errors: + if lineno > len(source): + lineno = len(source) + if "__IGNORE_WARNING__" not in Utilities.extractLineFlags( + source[lineno - 1].strip()): + self.noResults = False + fixed = False + if fixer: + fixed, msg = fixer.fixIssue(lineno, position, text) + if fixed: + text += "\n" + \ + self.trUtf8("Fix: {0}").format(msg) + self.__createResultItem( + fname, lineno, position, text, fixed) + if fixer: + fixer.saveFile(encoding) + self.__updateStatistics(report.counters, fixer) + progress += 1 finally: # reenable updates of the list self.resultList.setSortingEnabled(True) @@ -407,7 +446,10 @@ "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(), } if data != self.__data: self.__data = data @@ -419,17 +461,26 @@ 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 .Pep8CodeSelectionDialog import Pep8CodeSelectionDialog + dlg = Pep8CodeSelectionDialog(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. """ - from .Pep8CodeSelectionDialog import Pep8CodeSelectionDialog - dlg = Pep8CodeSelectionDialog( - self.excludeMessagesEdit.text(), False, self) - if dlg.exec_() == QDialog.Accepted: - self.excludeMessagesEdit.setText(dlg.getSelectedCodes()) + self.__selectCodes(self.excludeMessagesEdit, False) @pyqtSlot() def on_includeMessagesSelectButton_clicked(self): @@ -437,11 +488,7 @@ Private slot to select the message codes to be included via a selection dialog. """ - from .Pep8CodeSelectionDialog import Pep8CodeSelectionDialog - dlg = Pep8CodeSelectionDialog( - self.includeMessagesEdit.text(), False, self) - if dlg.exec_() == QDialog.Accepted: - self.includeMessagesEdit.setText(dlg.getSelectedCodes()) + self.__selectCodes(self.includeMessagesEdit, False) @pyqtSlot() def on_fixIssuesSelectButton_clicked(self): @@ -449,11 +496,15 @@ Private slot to select the issue codes to be fixed via a selection dialog. """ - from .Pep8CodeSelectionDialog import Pep8CodeSelectionDialog - dlg = Pep8CodeSelectionDialog( - self.fixIssuesEdit.text(), True, self) - if dlg.exec_() == QDialog.Accepted: - self.fixIssuesEdit.setText(dlg.getSelectedCodes()) + 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): @@ -473,14 +524,17 @@ message = item.data(0, self.messageRole) vm = e5App().getObject("ViewManager") - vm.openSourceFile(fn, lineno=lineno, pos=position) + vm.openSourceFile(fn, lineno=lineno, pos=position + 1) editor = vm.getOpenEditor(fn) - if position > 0: - editor.toggleFlakesWarning(lineno, True, message) - else: - error = message.split(':', 1)[-1] - editor.toggleSyntaxError(lineno, 1, True, error.strip(), show=True) + editor.toggleFlakesWarning(lineno, True, message) + + @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): @@ -540,10 +594,18 @@ "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"))) @pyqtSlot() def on_storeDefaultButton_clicked(self): @@ -557,10 +619,36 @@ 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()) + + @pyqtSlot() + def on_resetDefaultButton_clicked(self): + """ + Slot documentation goes here. + """ + raise NotImplementedError + 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) @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): @@ -587,3 +675,96 @@ for file in openFiles: editor = vm.getOpenEditor(file) editor.clearFlakesWarnings() + + @pyqtSlot() + def on_fixButton_clicked(self): + """ + Private slot to fix selected issues. + """ + # TODO: test this + from .Pep8Fixer import Pep8Fixer + + # 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 + + fixer = Pep8Fixer(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 = fixer.fixIssue(lineno, position, text) + if fixed: + text = "\n" + self.trUtf8("Fix: {0}").format(msg) + self.__modifyFixedResultItem(itm, text) + 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)