Plugins/CheckerPlugins/Pep8/Pep8Dialog.py

branch
Py2 comp.
changeset 2911
ce77f0b1ee67
parent 2847
1843ef6e2656
parent 2881
e942480a6130
child 3056
9986ec0e559a
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)

eric ide

mercurial