--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,1298 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show the results of the code style check. +""" + +from __future__ import unicode_literals + +import os +import fnmatch + +from PyQt5.QtCore import pyqtSlot, Qt, QTimer +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QAbstractButton, \ + QDialogButtonBox, QApplication, QHeaderView, QListWidgetItem + +from E5Gui.E5Application import e5App + +from .Ui_CodeStyleCheckerDialog import Ui_CodeStyleCheckerDialog + +import UI.PixmapCache +import Preferences +import Utilities + +from . import pycodestyle + +try: + basestring # __IGNORE_WARNING__ +except Exception: + basestring = str # define for Python3 + + +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 + ignoredRole = Qt.UserRole + 7 + + availableFutures = [ + 'division', 'absolute_import', 'with_statement', + 'print_function', 'unicode_literals', 'generator_stop'] + + noResults = 0 + noFiles = 1 + hasResults = 2 + + def __init__(self, styleCheckService, parent=None): + """ + Constructor + + @param styleCheckService reference to the service + (CodeStyleCheckService) + @param parent reference to the parent widget (QWidget) + """ + super(CodeStyleCheckerDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.optionsTabWidget.setCurrentIndex(0) + + self.excludeMessagesSelectButton.setIcon( + UI.PixmapCache.getIcon("select.png")) + self.includeMessagesSelectButton.setIcon( + UI.PixmapCache.getIcon("select.png")) + self.fixIssuesSelectButton.setIcon( + UI.PixmapCache.getIcon("select.png")) + self.noFixIssuesSelectButton.setIcon( + UI.PixmapCache.getIcon("select.png")) + + self.docTypeComboBox.addItem(self.tr("PEP-257"), "pep257") + self.docTypeComboBox.addItem(self.tr("Eric"), "eric") + + for future in CodeStyleCheckerDialog.availableFutures: + itm = QListWidgetItem(future, self.futuresList) + itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable) + itm.setCheckState(Qt.Unchecked) + + self.statisticsButton = self.buttonBox.addButton( + self.tr("Statistics..."), QDialogButtonBox.ActionRole) + self.statisticsButton.setToolTip( + self.tr("Press to show some statistics for the last run")) + self.statisticsButton.setEnabled(False) + self.showButton = self.buttonBox.addButton( + self.tr("Show"), QDialogButtonBox.ActionRole) + self.showButton.setToolTip( + self.tr("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.addBuiltinButton.setIcon(UI.PixmapCache.getIcon("plus.png")) + self.deleteBuiltinButton.setIcon(UI.PixmapCache.getIcon("minus.png")) + + self.checkProgress.setVisible(False) + self.checkProgressLabel.setVisible(False) + self.checkProgressLabel.setMaximumWidth(600) + + self.styleCheckService = styleCheckService + self.styleCheckService.styleChecked.connect(self.__processResult) + self.styleCheckService.batchFinished.connect(self.__batchFinished) + self.styleCheckService.error.connect(self.__processError) + self.filename = None + + self.results = CodeStyleCheckerDialog.noResults + self.cancelled = False + self.__lastFileItem = None + self.__batch = False + self.__finished = True + self.__errorItem = None + + self.__fileOrFileList = "" + self.__project = None + self.__forProject = False + self.__data = {} + self.__statistics = {} + self.__onlyFixes = {} + self.__noFixCodesList = [] + + self.on_loadDefaultButton_clicked() + + def __resort(self): + """ + Private method to resort the tree. + """ + self.resultList.sortItems(self.resultList.sortColumn(), + self.resultList.header().sortIndicatorOrder() + ) + + def __createErrorItem(self, filename, message): + """ + Private slot to create a new error item in the result list. + + @param filename name of the file + @type str + @param message error message + @type str + """ + if self.__errorItem is None: + self.__errorItem = QTreeWidgetItem(self.resultList, [ + self.tr("Errors")]) + self.__errorItem.setExpanded(True) + self.__errorItem.setForeground(0, Qt.red) + + msg = "{0} ({1})".format(self.__project.getRelativePath(filename), + message) + if not self.resultList.findItems(msg, Qt.MatchExactly): + itm = QTreeWidgetItem(self.__errorItem, [msg]) + itm.setForeground(0, Qt.red) + itm.setFirstColumnSpanned(True) + + def __createResultItem(self, filename, line, pos, message, fixed, + autofixing, ignored): + """ + Private method to create an entry in the result list. + + @param filename 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) + @param ignored flag indicating an ignored issue (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, [ + self.__project.getRelativePath(filename)]) + self.__lastFileItem.setFirstColumnSpanned(True) + self.__lastFileItem.setExpanded(True) + self.__lastFileItem.setData(0, self.filenameRole, filename) + + fixable = False + code, message = message.split(None, 1) + itm = QTreeWidgetItem( + self.__lastFileItem, + ["{0:6}".format(line), code, message]) + if code.startswith(("W", "-", "C", "M")): + 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 and \ + code not in self.__noFixCodesList: + 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, filename) + 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) + itm.setData(0, self.ignoredRole, ignored) + + if ignored: + font = itm.font(0) + font.setItalic(True) + for col in range(itm.columnCount()): + itm.setFont(col, font) + + 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: + code, message = text.split(None, 1) + 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, ignoredErrors): + """ + 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) + @param ignoredErrors number of ignored errors (integer) + """ + self.__statistics["_FilesCount"] += 1 + stats = [k for k in statistics.keys() if k[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] + self.__statistics["_IssuesFixed"] += fixer + self.__statistics["_IgnoredErrors"] += ignoredErrors + + 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 + + def __resetStatistics(self): + """ + Private slot to reset the statistics data. + """ + self.__statistics = {} + self.__statistics["_FilesCount"] = 0 + self.__statistics["_FilesIssues"] = 0 + self.__statistics["_IssuesFixed"] = 0 + self.__statistics["_IgnoredErrors"] = 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": pycodestyle.DEFAULT_IGNORE, + "IncludeMessages": "", + "RepeatMessages": False, + "FixCodes": "", + "FixIssues": False, + } + if "MaxLineLength" not in self.__data: + self.__data["MaxLineLength"] = pycodestyle.MAX_LINE_LENGTH + if "MaxDocLineLength" not in self.__data: + # Use MAX_LINE_LENGTH to avoid messages on existing code + self.__data["MaxDocLineLength"] = pycodestyle.MAX_LINE_LENGTH + if "BlankLines" not in self.__data: + self.__data["BlankLines"] = (2, 1) + # top level, method + 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" + if "ShowIgnored" not in self.__data: + self.__data["ShowIgnored"] = False + if "MaxCodeComplexity" not in self.__data: + self.__data["MaxCodeComplexity"] = 10 + if "LineComplexity" not in self.__data: + self.__data["LineComplexity"] = 15 + if "LineComplexityScore" not in self.__data: + self.__data["LineComplexityScore"] = 10 + if "ValidEncodings" not in self.__data: + self.__data["ValidEncodings"] = "latin-1, utf-8" + if "CopyrightMinFileSize" not in self.__data or \ + "CopyrightAuthor" not in self.__data: + self.__data["CopyrightMinFileSize"] = 0 + self.__data["CopyrightAuthor"] = "" + if "FutureChecker" not in self.__data: + self.__data["FutureChecker"] = "" + if "BuiltinsChecker" not in self.__data: + self.__data["BuiltinsChecker"] = { + "str": ["unicode", ], + "chr": ["unichr", ], + } + + 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.ignoredCheckBox.setChecked(self.__data["ShowIgnored"]) + self.lineLengthSpinBox.setValue(self.__data["MaxLineLength"]) + self.docLineLengthSpinBox.setValue(self.__data["MaxDocLineLength"]) + self.blankBeforeTopLevelSpinBox.setValue(self.__data["BlankLines"][0]) + self.blankBeforeMethodSpinBox.setValue(self.__data["BlankLines"][1]) + self.hangClosingCheckBox.setChecked(self.__data["HangClosing"]) + self.docTypeComboBox.setCurrentIndex( + self.docTypeComboBox.findData(self.__data["DocstringType"])) + self.complexitySpinBox.setValue(self.__data["MaxCodeComplexity"]) + self.lineComplexitySpinBox.setValue(self.__data["LineComplexity"]) + self.lineComplexityScoreSpinBox.setValue( + self.__data["LineComplexityScore"]) + self.encodingsEdit.setText(self.__data["ValidEncodings"]) + self.copyrightFileSizeSpinBox.setValue( + self.__data["CopyrightMinFileSize"]) + self.copyrightAuthorEdit.setText(self.__data["CopyrightAuthor"]) + self.__initFuturesList(self.__data["FutureChecker"]) + self.__initBuiltinsIgnoreList(self.__data["BuiltinsChecker"]) + + 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() + + if save: + self.__fileOrFileList = fn + + if isinstance(fn, list): + self.files = fn[:] + elif os.path.isdir(fn): + self.files = [] + extensions = set(Preferences.getPython("PythonExtensions") + + Preferences.getPython("Python3Extensions")) + for ext in extensions: + self.files.extend(Utilities.direntries( + fn, True, '*{0}'.format(ext), 0)) + else: + self.files = [fn] + + # filter the list depending on the filter string + if self.files: + filterString = self.excludeFilesEdit.text() + filterList = [f.strip() for f in filterString.split(",") + if f.strip()] + for fileFilter in filterList: + self.files = \ + [f for f in self.files + if not fnmatch.fnmatch(f, fileFilter.strip())] + + self.__errorItem = None + self.__resetStatistics() + self.__clearErrors(self.files) + + if len(self.files) > 0: + self.checkProgress.setMaximum(len(self.files)) + self.checkProgressLabel.setVisible(len(self.files) > 1) + self.checkProgress.setVisible(len(self.files) > 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() + self.__noFixCodesList = \ + [c.strip() for c in noFixCodes.split(",") if c.strip()] + fixIssues = self.fixIssuesCheckBox.isChecked() and repeatMessages + self.showIgnored = self.ignoredCheckBox.isChecked() and \ + repeatMessages + maxLineLength = self.lineLengthSpinBox.value() + maxDocLineLength = self.docLineLengthSpinBox.value() + blankLines = ( + self.blankBeforeTopLevelSpinBox.value(), + self.blankBeforeMethodSpinBox.value() + ) + hangClosing = self.hangClosingCheckBox.isChecked() + docType = self.docTypeComboBox.itemData( + self.docTypeComboBox.currentIndex()) + codeComplexityArgs = { + "McCabeComplexity": self.complexitySpinBox.value(), + "LineComplexity": self.lineComplexitySpinBox.value(), + "LineComplexityScore": self.lineComplexityScoreSpinBox.value(), + } + miscellaneousArgs = { + "CodingChecker": self.encodingsEdit.text(), + "CopyrightChecker": { + "MinFilesize": self.copyrightFileSizeSpinBox.value(), + "Author": self.copyrightAuthorEdit.text(), + }, + "FutureChecker": self.__getSelectedFutureImports(), + "BuiltinsChecker": self.__getBuiltinsIgnoreList(), + } + + self.__options = [excludeMessages, includeMessages, repeatMessages, + fixCodes, noFixCodes, fixIssues, maxLineLength, + maxDocLineLength, blankLines, hangClosing, + docType, codeComplexityArgs, miscellaneousArgs] + + # now go through all the files + self.progress = 0 + self.files.sort() + if len(self.files) == 1: + self.__batch = False + self.check() + else: + self.__batch = True + self.checkBatch() + else: + self.results = CodeStyleCheckerDialog.noFiles + self.__finished = False + self.__finish() + + def __modifyOptions(self, source): + """ + Private method to modify the options based on eflag: entries. + + This method looks for comment lines like '# eflag: noqa = M601' + at the end of the source in order to extend the list of excluded + messages for one file only. + + @param source source text (list of str or str) + @return list of checker options + """ + options = self.__options[:] + flags = Utilities.extractFlags(source) + if "noqa" in flags and isinstance(flags["noqa"], basestring): + excludeMessages = \ + options[0].strip().rstrip(",") + if excludeMessages: + excludeMessages += "," + excludeMessages += flags["noqa"] + options[0] = excludeMessages + return options + + def check(self, codestring=''): + """ + Public method to start a style check for one file. + + The results are reported to the __processResult slot. + + @keyparam codestring optional sourcestring (str) + """ + if not self.files: + self.checkProgressLabel.setPath("") + self.checkProgress.setMaximum(1) + self.checkProgress.setValue(1) + self.__finish() + return + + self.filename = self.files.pop(0) + self.checkProgress.setValue(self.progress) + self.checkProgressLabel.setPath(self.filename) + QApplication.processEvents() + + if self.cancelled: + self.__resort() + return + + self.__lastFileItem = None + + if codestring: + source = codestring.splitlines(True) + encoding = Utilities.get_coding(source) + else: + try: + source, encoding = Utilities.readEncodedFile( + self.filename) + source = source.splitlines(True) + except (UnicodeError, IOError) as msg: + self.results = CodeStyleCheckerDialog.hasResults + self.__createResultItem( + self.filename, 1, 1, + self.tr("Error: {0}").format(str(msg)) + .rstrip(), False, False, False) + self.progress += 1 + # Continue with next file + self.check() + return + if encoding.endswith( + ('-selected', '-default', '-guessed', '-ignore')): + encoding = encoding.rsplit('-', 1)[0] + + options = self.__modifyOptions(source) + + errors = [] + self.__itms = [] + for error, itm in self.__onlyFixes.pop(self.filename, []): + errors.append(error) + self.__itms.append(itm) + + eol = self.__getEol(self.filename) + args = options + [ + errors, eol, encoding, Preferences.getEditor("CreateBackupFile") + ] + self.__finished = False + self.styleCheckService.styleCheck( + None, self.filename, source, args) + + def checkBatch(self): + """ + Public method to start a style check batch job. + + The results are reported to the __processResult slot. + """ + self.__lastFileItem = None + + self.checkProgressLabel.setPath(self.tr("Preparing files...")) + progress = 0 + + argumentsList = [] + for filename in self.files: + progress += 1 + self.checkProgress.setValue(progress) + QApplication.processEvents() + + try: + source, encoding = Utilities.readEncodedFile( + filename) + source = source.splitlines(True) + except (UnicodeError, IOError) as msg: + self.results = CodeStyleCheckerDialog.hasResults + self.__createResultItem( + filename, 1, 1, + self.tr("Error: {0}").format(str(msg)) + .rstrip(), False, False, False) + continue + + if encoding.endswith( + ('-selected', '-default', '-guessed', '-ignore')): + encoding = encoding.rsplit('-', 1)[0] + + options = self.__modifyOptions(source) + + errors = [] + self.__itms = [] + for error, itm in self.__onlyFixes.pop(filename, []): + errors.append(error) + self.__itms.append(itm) + + eol = self.__getEol(filename) + args = options + [ + errors, eol, encoding, + Preferences.getEditor("CreateBackupFile") + ] + argumentsList.append((filename, source, args)) + + # reset the progress bar to the checked files + self.checkProgress.setValue(self.progress) + self.checkProgressLabel.setPath(self.tr("Transferring data...")) + QApplication.processEvents() + + self.__finished = False + self.styleCheckService.styleBatchCheck(argumentsList) + + def __batchFinished(self): + """ + Private slot handling the completion of a batch job. + """ + self.checkProgressLabel.setPath("") + self.checkProgress.setMaximum(1) + self.checkProgress.setValue(1) + self.__finish() + + def __processError(self, fn, msg): + """ + Private slot to process an error indication from the service. + + @param fn filename of the file + @type str + @param msg error message + @type str + """ + self.__createErrorItem(fn, msg) + + if not self.__batch: + self.check() + + def __processResult(self, fn, codeStyleCheckerStats, fixes, results): + """ + Private slot called after perfoming a style check on one file. + + @param fn filename of the just checked file (str) + @param codeStyleCheckerStats stats of style and name check (dict) + @param fixes number of applied fixes (int) + @param results tuple for each found violation of style (tuple of + lineno (int), position (int), text (str), ignored (bool), + fixed (bool), autofixing (bool)) + """ + if self.__finished: + return + + # Check if it's the requested file, otherwise ignore signal if not + # in batch mode + if not self.__batch and fn != self.filename: + return + + # disable updates of the list for speed + self.resultList.setUpdatesEnabled(False) + self.resultList.setSortingEnabled(False) + + fixed = None + ignoredErrors = 0 + if self.__itms: + for itm, (_lineno, _position, text, _ignored, fixed, + _autofixing) in zip(self.__itms, results): + self.__modifyFixedResultItem(itm, text, fixed) + self.__updateFixerStatistics(fixes) + else: + self.__lastFileItem = None + + for lineno, position, text, ignored, fixed, autofixing in results: + if ignored: + ignoredErrors += 1 + if self.showIgnored: + text = self.tr("{0} (ignored)").format(text) + else: + continue + self.results = CodeStyleCheckerDialog.hasResults + self.__createResultItem( + fn, lineno, position, text, fixed, autofixing, ignored) + + self.__updateStatistics( + codeStyleCheckerStats, fixes, ignoredErrors) + + if fixed: + vm = e5App().getObject("ViewManager") + editor = vm.getOpenEditor(fn) + if editor: + editor.refresh() + + self.progress += 1 + + self.__resort() + # reenable updates of the list + self.resultList.setSortingEnabled(True) + self.resultList.setUpdatesEnabled(True) + + self.checkProgress.setValue(self.progress) + self.checkProgressLabel.setPath(fn) + QApplication.processEvents() + + if not self.__batch: + self.check() + + def __finish(self): + """ + Private slot called when the code style check finished or the user + pressed the cancel button. + """ + if not self.__finished: + self.__finished = True + + self.cancelled = True + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.statisticsButton.setEnabled(True) + self.showButton.setEnabled(True) + self.startButton.setEnabled(True) + + if self.results != CodeStyleCheckerDialog.hasResults: + if self.results == CodeStyleCheckerDialog.noResults: + QTreeWidgetItem( + self.resultList, [self.tr('No issues found.')]) + else: + QTreeWidgetItem( + self.resultList, + [self.tr('No files found (check your ignore list).')]) + QApplication.processEvents() + self.showButton.setEnabled(False) + else: + self.showButton.setEnabled(True) + self.resultList.header().resizeSections( + QHeaderView.ResizeToContents) + self.resultList.header().setStretchLastSection(True) + + self.checkProgress.setVisible(False) + self.checkProgressLabel.setVisible(False) + + def __getEol(self, fn): + """ + Private method to get the applicable eol string. + + @param fn filename where to determine the line ending (str) + @return eol string (string) + """ + if self.__project.isOpen() and self.__project.isProjectFile(fn): + eol = self.__project.getEolString() + else: + eol = Utilities.linesep() + return eol + + @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(), + "ShowIgnored": self.ignoredCheckBox.isChecked(), + "MaxLineLength": self.lineLengthSpinBox.value(), + "MaxDocLineLength": self.docLineLengthSpinBox.value(), + "BlankLines": ( + self.blankBeforeTopLevelSpinBox.value(), + self.blankBeforeMethodSpinBox.value() + ), + "HangClosing": self.hangClosingCheckBox.isChecked(), + "DocstringType": self.docTypeComboBox.itemData( + self.docTypeComboBox.currentIndex()), + "MaxCodeComplexity": self.complexitySpinBox.value(), + "LineComplexity": self.lineComplexitySpinBox.value(), + "LineComplexityScore": self.lineComplexityScoreSpinBox.value(), + "ValidEncodings": self.encodingsEdit.text(), + "CopyrightMinFileSize": self.copyrightFileSizeSpinBox.value(), + "CopyrightAuthor": self.copyrightAuthorEdit.text(), + "FutureChecker": self.__getSelectedFutureImports(), + "BuiltinsChecker": self.__getBuiltinsIgnoreList(), + } + if data != self.__data: + self.__data = data + self.__project.setData("CHECKERSPARMS", "Pep8Checker", + self.__data) + + self.resultList.clear() + self.results = CodeStyleCheckerDialog.noResults + 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.results != CodeStyleCheckerDialog.hasResults: + 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 in ["E901", "E902"]: + editor.toggleSyntaxError(lineno, 0, True, message, True) + else: + editor.toggleWarning( + lineno, 0, True, message, warningType=editor.WarningStyle) + + editor.updateVerticalScrollBar() + + @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.clearStyleWarnings() + for cindex in range(itm.childCount()): + citm = itm.child(cindex) + lineno = citm.data(0, self.lineRole) + message = citm.data(0, self.messageRole) + editor.toggleWarning( + lineno, 0, True, message, warningType=editor.WarningStyle) + + # 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 file not in errorFiles: + editor = vm.getOpenEditor(file) + editor.clearStyleWarnings() + + editor = vm.activeWindow() + editor.updateVerticalScrollBar() + + @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", pycodestyle.DEFAULT_IGNORE)) + self.includeMessagesEdit.setText(Preferences.Prefs.settings.value( + "PEP8/IncludeMessages", "")) + self.repeatCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/RepeatMessages", False))) + 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", False))) + self.ignoredCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/ShowIgnored", False))) + self.lineLengthSpinBox.setValue(int(Preferences.Prefs.settings.value( + "PEP8/MaxLineLength", pycodestyle.MAX_LINE_LENGTH))) + # Use MAX_LINE_LENGTH to avoid messages on existing code + self.docLineLengthSpinBox.setValue(int( + Preferences.Prefs.settings.value( + "PEP8/MaxDocLineLength", pycodestyle.MAX_LINE_LENGTH))) + self.blankBeforeTopLevelSpinBox.setValue( + int(Preferences.Prefs.settings.value( + "PEP8/BlankLinesBeforeTopLevel", 2))) + self.blankBeforeMethodSpinBox.setValue( + int(Preferences.Prefs.settings.value( + "PEP8/BlankLinesBeforeMethod", 1))) + self.hangClosingCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value("PEP8/HangClosing", False))) + self.docTypeComboBox.setCurrentIndex(self.docTypeComboBox.findData( + Preferences.Prefs.settings.value("PEP8/DocstringType", "pep257"))) + self.complexitySpinBox.setValue(int(Preferences.Prefs.settings.value( + "PEP8/MaxCodeComplexity", 10))) + self.lineComplexitySpinBox.setValue( + int(Preferences.Prefs.settings.value( + "PEP8/LineComplexity", 15))) + self.lineComplexityScoreSpinBox.setValue( + int(Preferences.Prefs.settings.value( + "PEP8/LineComplexityScore", 10))) + self.encodingsEdit.setText(Preferences.Prefs.settings.value( + "PEP8/ValidEncodings", "latin-1, utf-8")) + self.copyrightFileSizeSpinBox.setValue(int( + Preferences.Prefs.settings.value("PEP8/CopyrightMinFileSize", 0))) + self.copyrightAuthorEdit.setText( + Preferences.Prefs.settings.value("PEP8/CopyrightAuthor", "")) + self.__initFuturesList( + Preferences.Prefs.settings.value("PEP8/FutureChecker", "")) + self.__initBuiltinsIgnoreList(Preferences.toDict( + Preferences.Prefs.settings.value("PEP8/BuiltinsChecker", { + "str": ["unicode", ], + "chr": ["unichr", ], + }))) + + @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/ShowIgnored", self.ignoredCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/MaxLineLength", self.lineLengthSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/MaxDocLineLength", self.docLineLengthSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/BlankLinesBeforeTopLevel", + self.blankBeforeTopLevelSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/BlankLinesBeforeMethod", + self.blankBeforeMethodSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/HangClosing", self.hangClosingCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/DocstringType", self.docTypeComboBox.itemData( + self.docTypeComboBox.currentIndex())) + Preferences.Prefs.settings.setValue( + "PEP8/MaxCodeComplexity", self.complexitySpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/LineComplexity", self.lineComplexitySpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/LineComplexityScore", + self.lineComplexityScoreSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/ValidEncodings", self.encodingsEdit.text()) + Preferences.Prefs.settings.setValue( + "PEP8/CopyrightMinFileSize", self.copyrightFileSizeSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/CopyrightAuthor", self.copyrightAuthorEdit.text()) + Preferences.Prefs.settings.setValue( + "PEP8/FutureChecker", self.__getSelectedFutureImports()) + Preferences.Prefs.settings.setValue( + "PEP8/BuiltinsChecker", self.__getBuiltinsIgnoreList()) + + @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", pycodestyle.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/ShowIgnored", False) + Preferences.Prefs.settings.setValue( + "PEP8/MaxLineLength", pycodestyle.MAX_LINE_LENGTH) + # Hard reset to pycodestyle preferences + Preferences.Prefs.settings.setValue( + "PEP8/MaxDocLineLength", pycodestyle.MAX_DOC_LENGTH) + Preferences.Prefs.settings.setValue( + "PEP8/BlankLinesBeforeTopLevel", 2) + Preferences.Prefs.settings.setValue( + "PEP8/BlankLinesBeforeMethod", 1) + Preferences.Prefs.settings.setValue("PEP8/HangClosing", False) + Preferences.Prefs.settings.setValue("PEP8/DocstringType", "pep257") + Preferences.Prefs.settings.setValue("PEP8/MaxCodeComplexity", 10) + Preferences.Prefs.settings.setValue("PEP8/LineComplexity", 15) + Preferences.Prefs.settings.setValue("PEP8/LineComplexityScore", 10) + Preferences.Prefs.settings.setValue( + "PEP8/ValidEncodings", "latin-1, utf-8") + Preferences.Prefs.settings.setValue("PEP8/CopyrightMinFileSize", 0) + Preferences.Prefs.settings.setValue("PEP8/CopyrightAuthor", "") + Preferences.Prefs.settings.setValue("PEP8/FutureChecker", "") + Preferences.Prefs.settings.setValue("PEP8/BuiltinsChecker", { + "str": ["unicode", ], + "chr": ["unichr", ], + }) + # Update UI with default values + self.on_loadDefaultButton_clicked() + + @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): + if self.__batch: + self.styleCheckService.cancelStyleBatchCheck() + QTimer.singleShot(1000, self.__finish) + else: + self.__finish() + elif button == self.showButton: + self.on_showButton_clicked() + elif button == self.statisticsButton: + self.on_statisticsButton_clicked() + + def __clearErrors(self, files): + """ + Private method to clear all warning markers of open editors to be + checked. + + @param files list of files to be checked (list of string) + """ + vm = e5App().getObject("ViewManager") + openFiles = vm.getOpenFilenames() + for file in [f for f in openFiles if f in files]: + editor = vm.getOpenEditor(file) + editor.clearStyleWarnings() + + @pyqtSlot() + def on_fixButton_clicked(self): + """ + Private slot to fix selected issues. + + Build a dictionary of issues to fix. Update the initialized __options. + Then call check with the dict as keyparam to fix selected issues. + """ + fixableItems = self.__getSelectedFixableItems() + # dictionary of lists of tuples containing the issue and the item + fixesDict = {} + for itm in fixableItems: + filename = itm.data(0, self.filenameRole) + if filename not in fixesDict: + fixesDict[filename] = [] + fixesDict[filename].append(( + (filename, itm.data(0, self.lineRole), + itm.data(0, self.positionRole), + "{0} {1}".format(itm.data(0, self.codeRole), + itm.data(0, self.messageRole))), + itm + )) + + # update the configuration values (3: fixCodes, 4: noFixCodes, + # 5: fixIssues, 6: maxLineLength) + self.__options[3] = self.fixIssuesEdit.text() + self.__options[4] = self.noFixIssuesEdit.text() + self.__options[5] = True + self.__options[6] = self.lineLengthSpinBox.value() + + self.files = list(fixesDict.keys()) + # now go through all the files + self.progress = 0 + self.files.sort() + self.cancelled = False + self.__onlyFixes = fixesDict + self.check() + + 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 citm not in fixableItems: + fixableItems.append(citm) + elif self.__itemFixable(itm) and itm not 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) and + not itm.data(0, self.ignoredRole)) + + def __initFuturesList(self, selectedFutures): + """ + Private method to set the selected status of the future imports. + + @param selectedFutures comma separated list of expected future imports + @type str + """ + if selectedFutures: + expectedImports = [ + i.strip() for i in selectedFutures.split(",") + if bool(i.strip())] + else: + expectedImports = [] + for row in range(self.futuresList.count()): + itm = self.futuresList.item(row) + if itm.text() in expectedImports: + itm.setCheckState(Qt.Checked) + else: + itm.setCheckState(Qt.Unchecked) + + def __getSelectedFutureImports(self): + """ + Private method to get the expected future imports. + + @return expected future imports as a comma separated string + @rtype str + """ + selectedFutures = [] + for row in range(self.futuresList.count()): + itm = self.futuresList.item(row) + if itm.checkState() == Qt.Checked: + selectedFutures.append(itm.text()) + return ", ".join(selectedFutures) + + def __initBuiltinsIgnoreList(self, builtinsIgnoreDict): + """ + Private method to populate the list of shadowed builtins to be ignored. + + @param builtinsIgnoreDict dictionary containing the builtins + assignments to be ignored + @type dict of list of str + """ + self.builtinsAssignmentList.clear() + for left, rightList in builtinsIgnoreDict.items(): + for right in rightList: + QTreeWidgetItem(self.builtinsAssignmentList, [left, right]) + + self.on_builtinsAssignmentList_itemSelectionChanged() + + def __getBuiltinsIgnoreList(self): + """ + Private method to get a dictionary containing the builtins assignments + to be ignored. + + @return dictionary containing the builtins assignments to be ignored + @rtype dict of list of str + """ + builtinsIgnoreDict = {} + for row in range(self.builtinsAssignmentList.topLevelItemCount()): + itm = self.builtinsAssignmentList.topLevelItem(row) + left, right = itm.text(0), itm.text(1) + if left not in builtinsIgnoreDict: + builtinsIgnoreDict[left] = [] + builtinsIgnoreDict[left].append(right) + + return builtinsIgnoreDict + + @pyqtSlot() + def on_builtinsAssignmentList_itemSelectionChanged(self): + """ + Private slot to react upon changes of the selected builtin assignments. + """ + self.deleteBuiltinButton.setEnabled( + len(self.builtinsAssignmentList.selectedItems()) > 0) + + @pyqtSlot() + def on_addBuiltinButton_clicked(self): + """ + Private slot to add a built-in assignment to be ignored. + """ + from .CodeStyleAddBuiltinIgnoreDialog import \ + CodeStyleAddBuiltinIgnoreDialog + dlg = CodeStyleAddBuiltinIgnoreDialog(self) + if dlg.exec_() == QDialog.Accepted: + left, right = dlg.getData() + QTreeWidgetItem(self.builtinsAssignmentList, [left, right]) + + @pyqtSlot() + def on_deleteBuiltinButton_clicked(self): + """ + Private slot to delete the selected items from the list. + """ + for itm in self.builtinsAssignmentList.selectedItems(): + index = self.builtinsAssignmentList.indexOfTopLevelItem(itm) + self.builtinsAssignmentList.takeTopLevelItem(index) + del itm