Sun, 16 Jan 2011 16:09:21 +0100
Started to implement a fixer for PEP 8 issues.
--- a/Plugins/CheckerPlugins/Pep8/Pep8CodeSelectionDialog.py Sat Jan 15 19:31:56 2011 +0100 +++ b/Plugins/CheckerPlugins/Pep8/Pep8CodeSelectionDialog.py Sun Jan 16 16:09:21 2011 +0100 @@ -11,6 +11,7 @@ from PyQt4.QtGui import QDialog, QTreeWidgetItem from . import pep8 +from .Pep8Fixer import Pep8FixableIssues from .Ui_Pep8CodeSelectionDialog import Ui_Pep8CodeSelectionDialog @@ -18,11 +19,13 @@ """ Class implementing a dialog to select PEP 8 message codes. """ - def __init__(self, codes, parent = None): + 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) """ QDialog.__init__(self, parent) @@ -30,7 +33,11 @@ codeList = [code.strip() for code in codes.split(",") if code.strip()] - for code in sorted(pep8.pep8_messages.keys(), key=lambda a: a[1:]): + if showFixCodes: + selectableCodes = Pep8FixableIssues + else: + selectableCodes = pep8.pep8_messages.keys() + for code in sorted(selectableCodes, key=lambda a: a[1:]): if code in pep8.pep8_messages_sample_args: message = QCoreApplication.translate("pep8", pep8.pep8_messages[code]).format(
--- a/Plugins/CheckerPlugins/Pep8/Pep8Dialog.py Sat Jan 15 19:31:56 2011 +0100 +++ b/Plugins/CheckerPlugins/Pep8/Pep8Dialog.py Sun Jan 16 16:09:21 2011 +0100 @@ -21,6 +21,7 @@ from .Pep8Checker import Pep8Checker, Pep8Py2Checker from .Pep8CodeSelectionDialog import Pep8CodeSelectionDialog from .Pep8StatisticsDialog import Pep8StatisticsDialog +from .Pep8Fixer import Pep8Fixer from .Ui_Pep8Dialog import Ui_Pep8Dialog @@ -80,6 +81,8 @@ UI.PixmapCache.getIcon("clearLeft.png")) self.clearButtonIncludeMessages.setIcon( UI.PixmapCache.getIcon("clearLeft.png")) + self.clearButtonFixIssues.setIcon( + UI.PixmapCache.getIcon("clearLeft.png")) self.on_loadDefaultButton_clicked() def __resort(self): @@ -162,27 +165,31 @@ self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.__data = self.__project.getData("CHECKERSPARMS", "Pep8Checker") - if self.__data is None or "ExcludeFiles" not in self.__data: + if self.__data is None or \ + "ExcludeFiles" not in self.__data or \ + len(self.__data) != 6: # initialize the data structure self.__data = { "ExcludeFiles" : "", "ExcludeMessages" : pep8.DEFAULT_IGNORE, "IncludeMessages" : "", "RepeatMessages" : False, + "FixCodes" : "", + "FixIssues" : False, } 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.fixIssuesCheckBox.setChecked(self.__data["FixIssues"]) - def start(self, fn, codestring = "", save = False, repeat = None): + def start(self, fn, save = False, repeat = None): """ Public slot to start the PEP 8 check. @param fn file or list of files or directory to be checked (string or list of strings) - @keyparam codestring string containing the code to be checked (string). - If this is given, file must be a single file name. @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 @@ -234,9 +241,7 @@ if f.endswith( tuple(Preferences.getPython("PythonExtensions")))] - if (codestring and len(py3files) == 1) or \ - (codestring and len(py2files) == 1) or \ - (not codestring and len(py3files) + len(py2files) > 0): + if len(py3files) + len(py2files) > 0: self.checkProgress.setMaximum(len(py3files) + len(py2files)) QApplication.processEvents() @@ -244,6 +249,8 @@ excludeMessages = self.excludeMessagesEdit.text() includeMessages = self.includeMessagesEdit.text() repeatMessages = self.repeatCheckBox.isChecked() + fixCodes = self.fixIssuesEdit.text() + fixIssues = self.fixIssuesCheckBox.isChecked() and repeatMessages # now go through all the files progress = 0 @@ -257,24 +264,24 @@ self.__lastFileItem = None - if codestring: - source = codestring.splitlines(True) - else: - try: - source = Utilities.readEncodedFile(file)[0] - # convert eols - source = Utilities.convertLineEnds(source, "\n") - 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]) - progress += 1 - continue + 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]) + progress += 1 + continue flags = Utilities.extractFlags(source) ext = os.path.splitext(file)[1] + if fixIssues: + fixer = Pep8Fixer(self.__project, file, source, + fixCodes, True) # always fix in place + else: + fixer = None if ("FileType" in flags and flags["FileType"] in ["Python", "Python2"]) or \ file in py2files or \ @@ -300,8 +307,14 @@ if not source[lineno - 1].strip()\ .endswith("__IGNORE_WARNING__"): self.noResults = 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) + fixer and fixer.saveFile(encoding) self.__updateStatistics(checker.statistics) progress += 1 self.checkProgress.setValue(progress) @@ -345,6 +358,8 @@ "ExcludeMessages" : self.excludeMessagesEdit.text(), "IncludeMessages" : self.includeMessagesEdit.text(), "RepeatMessages" : self.repeatCheckBox.isChecked(), + "FixCodes" : self.fixIssuesEdit.text(), + "FixIssues" : self.fixIssuesCheckBox.isChecked(), } if data != self.__data: self.__data = data @@ -362,7 +377,8 @@ Private slot to select the message codes to be excluded via a selection dialog. """ - dlg = Pep8CodeSelectionDialog(self.excludeMessagesEdit.text(), self) + dlg = Pep8CodeSelectionDialog( + self.excludeMessagesEdit.text(), False, self) if dlg.exec_() == QDialog.Accepted: self.excludeMessagesEdit.setText(dlg.getSelectedCodes()) @@ -372,10 +388,22 @@ Private slot to select the message codes to be included via a selection dialog. """ - dlg = Pep8CodeSelectionDialog(self.includeMessagesEdit.text(), self) + dlg = Pep8CodeSelectionDialog( + self.includeMessagesEdit.text(), False, self) if dlg.exec_() == QDialog.Accepted: self.includeMessagesEdit.setText(dlg.getSelectedCodes()) + @pyqtSlot() + def on_fixIssuesSelectButton_clicked(self): + """ + Private slot to select the issue codes to be fixed via a + selection dialog. + """ + dlg = Pep8CodeSelectionDialog( + self.fixIssuesEdit.text(), True, self) + if dlg.exec_() == QDialog.Accepted: + self.fixIssuesEdit.setText(dlg.getSelectedCodes()) + @pyqtSlot(QTreeWidgetItem, int) def on_resultList_itemActivated(self, item, column): """ @@ -458,6 +486,10 @@ "PEP8/IncludeMessages")) ## self.repeatCheckBox.setChecked(Preferences.toBool( ## Preferences.Prefs.settings.value("PEP8/RepeatMessages"))) + self.fixIssuesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/FixCodes")) + self.fixIssuesCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/FixIssues"))) @pyqtSlot() def on_storeDefaultButton_clicked(self): @@ -473,6 +505,10 @@ 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/FixIssues", + self.fixIssuesCheckBox.isChecked()) @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button):
--- a/Plugins/CheckerPlugins/Pep8/Pep8Dialog.ui Sat Jan 15 19:31:56 2011 +0100 +++ b/Plugins/CheckerPlugins/Pep8/Pep8Dialog.ui Sun Jan 16 16:09:21 2011 +0100 @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>650</width> - <height>600</height> + <height>700</height> </rect> </property> <property name="windowTitle"> @@ -52,7 +52,7 @@ </property> </widget> </item> - <item row="0" column="4" rowspan="4"> + <item row="0" column="4" rowspan="5"> <widget class="Line" name="line"> <property name="lineWidth"> <number>2</number> @@ -62,7 +62,7 @@ </property> </widget> </item> - <item row="0" column="5" rowspan="4"> + <item row="0" column="5" rowspan="5"> <layout class="QVBoxLayout" name="verticalLayout_2"> <item> <widget class="QPushButton" name="startButton"> @@ -136,7 +136,7 @@ <item row="1" column="3"> <widget class="QToolButton" name="excludeMessagesSelectButton"> <property name="toolTip"> - <string>Press to select the messages from a list</string> + <string>Press to select the message codes from a list</string> </property> <property name="text"> <string>...</string> @@ -170,23 +170,87 @@ <item row="2" column="3"> <widget class="QToolButton" name="includeMessagesSelectButton"> <property name="toolTip"> - <string>Press to select the messages from a list</string> + <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" colspan="4"> - <widget class="QCheckBox" name="repeatCheckBox"> + <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="QLineEdit" name="fixIssuesEdit"> <property name="toolTip"> - <string>Select to repeat each message type</string> + <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="clearButtonFixIssues"> + <property name="toolTip"> + <string>Press to clear the fix issues edit</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QToolButton" name="fixIssuesSelectButton"> + <property name="toolTip"> + <string>Press to select the message codes from a list</string> </property> <property name="text"> - <string>Repeat messages</string> + <string>...</string> </property> </widget> </item> + <item row="4" column="0" colspan="4"> + <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> </widget> </item> @@ -262,7 +326,11 @@ <tabstop>includeMessagesEdit</tabstop> <tabstop>clearButtonIncludeMessages</tabstop> <tabstop>includeMessagesSelectButton</tabstop> + <tabstop>fixIssuesEdit</tabstop> + <tabstop>clearButtonFixIssues</tabstop> + <tabstop>fixIssuesSelectButton</tabstop> <tabstop>repeatCheckBox</tabstop> + <tabstop>fixIssuesCheckBox</tabstop> <tabstop>storeDefaultButton</tabstop> <tabstop>resultList</tabstop> <tabstop>buttonBox</tabstop> @@ -276,11 +344,11 @@ <slot>clear()</slot> <hints> <hint type="sourcelabel"> - <x>505</x> - <y>40</y> + <x>493</x> + <y>42</y> </hint> <hint type="destinationlabel"> - <x>472</x> + <x>460</x> <y>39</y> </hint> </hints> @@ -308,8 +376,8 @@ <slot>clear()</slot> <hints> <hint type="sourcelabel"> - <x>494</x> - <y>100</y> + <x>493</x> + <y>106</y> </hint> <hint type="destinationlabel"> <x>458</x> @@ -317,5 +385,37 @@ </hint> </hints> </connection> + <connection> + <sender>clearButtonFixIssues</sender> + <signal>clicked()</signal> + <receiver>fixIssuesEdit</receiver> + <slot>clear()</slot> + <hints> + <hint type="sourcelabel"> + <x>478</x> + <y>122</y> + </hint> + <hint type="destinationlabel"> + <x>445</x> + <y>122</y> + </hint> + </hints> + </connection> + <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/Pep8/Pep8Fixer.py Sun Jan 16 16:09:21 2011 +0100 @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to fix certain PEP 8 issues. +""" + +import os +import re + +from PyQt4.QtCore import QObject + +from E5Gui import E5MessageBox + +import Utilities + +Pep8FixableIssues = ["W191", "W291", "W292", "W293", "E301", "E303", "E304", "W391", "W603"] + +class Pep8Fixer(QObject): + """ + Class implementing a fixer for certain PEP 8 issues. + """ + def __init__(self, project, filename, sourceLines, fixCodes, 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 inPlace flag indicating to modify the file in place (boolean) + """ + QObject.__init__(self) + + 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()] + + 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 = { + "W191" : self.__fixTabs, + "W291" : self.__fixWhitespace, + "W292" : self.__fixNewline, + "W293" : self.__fixWhitespace, + "E301" : self.__fixOneBlankLine, + "E303" : self.__fixTooManyBlankLines, + "E304" : self.__fixBlankLinesAfterDecorator, + "W391" : self.__fixTrailingBlankLines, + "W603" : self.__fixNotEqual, + } + self.__modified = False + self.__stack = [] # these need to be fixed before the file is saved + # but after all inline fixes + + 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 + + # apply deferred fixes + self.__finalize() + + txt = "".join(self.__source) + try: + Utilities.writeEncodedFile(self.__filename, txt, encoding) + except (IOError, Utilities.CodingError, UnicodeError) as err: + E5MessageBox.critical(self, + self.trUtf8("Fix PEP 8 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 fixIssue(self, line, pos, message): + """ + Public method to fix the fixable issues. + + @param line line number of issue (integer or string) + @param pos character position of issue (integer or string) + @param message message text (string) + @return flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + code = message.split(None, 1)[0].strip() + + if (code in self.__fixCodes or len(self.__fixCodes) == 0) and \ + code in self.__fixes: + res = self.__fixes[code](code, line, pos) + if res[0]: + self.__modified = True + else: + res = (False, "") + + return res + + def __finalize(self): + """ + Private method to apply all deferred fixes. + """ + for code, line, pos in reversed(self.__stack): + self.__fixes[code](code, line, pos, apply=True) + + def __getEol(self): + """ + Private method to get the applicable eol string. + + @return eol string (string) + """ + if self.__origName: + fn = self.__origName + else: + fn = self.__filename + + if self.__project.isOpen() and self.__project.isProjectFile(fn): + eol = self.__project.getLineSeparator() + else: + eol = Utilities.linesep() + return eol + + def __fixTabs(self, code, line, pos): + """ + Private method to fix obsolete tab usage. + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + self.__source[line - 1] = self.__source[line - 1].replace("\t", " ") + return (True, self.trUtf8("Tab converted to 4 spaces.")) + + def __fixWhitespace(self, code, line, pos): + """ + Private method to fix trailing whitespace. + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + self.__source[line - 1] = re.sub(r'[\t ]*$', "", + self.__source[line - 1]) + return (True, self.trUtf8("Whitespace stripped from end of line.")) + + def __fixNewline(self, code, line, pos): + """ + Private method to fix a missing newline at the end of file. + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + self.__source[line - 1] += self.__getEol() + return (True, self.trUtf8("newline added to end of file.")) + + def __fixTrailingBlankLines(self, code, line, pos): + """ + Private method to fix trailing blank lines. + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + index = line - 1 + while index: + if self.__source[index].strip() == "": + del self.__source[index] + index -= 1 + else: + break + return (True, self.trUtf8( + "Superfluous trailing blank lines removed from end of file.")) + + def __fixNotEqual(self, code, line, pos): + """ + Private method to fix the not equal notation. + + @param code code of the issue (string) + @param line line number of the issue (integer) + @param pos position inside line (integer) + @return flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + self.__source[line - 1] = self.__source[line - 1].replace("<>", "!=") + return (True, self.trUtf8("'<>' replaced by '!='.")) + + def __fixBlankLinesAfterDecorator(self, code, line, pos, apply=False): + """ + Private method to fix superfluous blank lines after a function + decorator. + + @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 flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + if apply: + index = line - 2 + while index: + if self.__source[index].strip() == "": + del self.__source[index] + index -= 1 + else: + break + else: + self.__stack.append((code, line, pos)) + return (True, self.trUtf8( + "Superfluous blank lines after function decorator removed.")) + + def __fixTooManyBlankLines(self, code, line, pos, apply=False): + """ + Private method to fix superfluous blank lines. + + @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 flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + if apply: + index = line - 3 + while index: + if self.__source[index].strip() == "": + del self.__source[index] + index -= 1 + else: + break + else: + self.__stack.append((code, line, pos)) + return (True, self.trUtf8("Superfluous blank lines removed.")) + + def __fixOneBlankLine(self, code, line, pos, apply=False): + """ + Private method to fix the need for one blank line. + + @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 flag indicating an applied fix (boolean) and a message for + the fix (string) + """ + if apply: + self.__source.insert(line - 1, self.__getEol()) + else: + self.__stack.append((code, line, pos)) + return (True, self.trUtf8("One blank line inserted."))
--- a/Plugins/PluginPep8Checker.py Sat Jan 15 19:31:56 2011 +0100 +++ b/Plugins/PluginPep8Checker.py Sun Jan 16 16:09:21 2011 +0100 @@ -265,10 +265,10 @@ """ editor = e5App().getObject("ViewManager").activeWindow() if editor is not None: - self.__editorPep8CheckerDialog = Pep8Dialog() - self.__editorPep8CheckerDialog.show() - self.__editorPep8CheckerDialog.start( - editor.getFileName(), - codestring = editor.text(), - save = True, - repeat = True) + if editor.checkDirty(): + self.__editorPep8CheckerDialog = Pep8Dialog() + self.__editorPep8CheckerDialog.show() + self.__editorPep8CheckerDialog.start( + editor.getFileName(), + save = True, + repeat = True)
--- a/eric5.e4p Sat Jan 15 19:31:56 2011 +0100 +++ b/eric5.e4p Sun Jan 16 16:09:21 2011 +0100 @@ -852,6 +852,7 @@ <Source>UtilitiesPython2/pep8.py</Source> <Source>UtilitiesPython2/Pep8Checker.py</Source> <Source>Plugins/CheckerPlugins/Pep8/Pep8StatisticsDialog.py</Source> + <Source>Plugins/CheckerPlugins/Pep8/Pep8Fixer.py</Source> </Sources> <Forms> <Form>PyUnit/UnittestDialog.ui</Form>