--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Fri Apr 02 11:59:41 2021 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Sat May 01 14:27:20 2021 +0200 @@ -10,6 +10,8 @@ import os import fnmatch import copy +import collections +import json from PyQt5.QtCore import pyqtSlot, Qt, QTimer, QCoreApplication from PyQt5.QtGui import QIcon @@ -31,11 +33,9 @@ from .Miscellaneous.MiscellaneousDefaults import ( MiscellaneousCheckerDefaultArgs ) - -try: - basestring # __IGNORE_WARNING__ -except Exception: - basestring = str # define for Python3 +from .Annotations.AnnotationsCheckerDefaults import ( + AnnotationsCheckerDefaultArgs +) class CodeStyleCheckerDialog(QDialog, Ui_CodeStyleCheckerDialog): @@ -91,6 +91,9 @@ "W": QCoreApplication.translate( "CheckerCategories", "Warnings"), + "Y": QCoreApplication.translate( + "CheckerCategories", + "Simplify Code"), } noResults = 0 @@ -109,7 +112,7 @@ @param parent reference to the parent widget @type QWidget """ - super(CodeStyleCheckerDialog, self).__init__(parent) + super().__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.WindowType.Window) @@ -192,15 +195,28 @@ self.__project = None self.__forProject = False self.__data = {} - self.__statistics = {} + self.__statistics = collections.defaultdict(self.__defaultStatistics) self.__onlyFixes = {} self.__noFixCodesList = [] + self.__detectedCodes = [] self.on_loadDefaultButton_clicked() self.mainWidget.setCurrentWidget(self.configureTab) self.optionsTabWidget.setCurrentWidget(self.globalOptionsTab) + def __defaultStatistics(self): + """ + Private method to return the default statistics entry. + + @return dictionary with default statistics entry + @rtype dict + """ + return { + "total": 0, + "ignored": 0, + } + def __resort(self): """ Private method to resort the tree. @@ -275,6 +291,7 @@ self.__lastFileItem.setData(0, self.filenameRole, filename) msgCode = result["code"].split(".", 1)[0] + self.__detectedCodes.append(msgCode) fixable = False itm = QTreeWidgetItem( @@ -292,6 +309,8 @@ itm.setIcon(1, UI.PixmapCache.getIcon("docstringError")) elif msgCode.startswith("P"): itm.setIcon(1, UI.PixmapCache.getIcon("dirClosed")) + elif msgCode.startswith("Y"): + itm.setIcon(1, UI.PixmapCache.getIcon("filePython")) elif msgCode.startswith("S"): if "severity" in result: if result["severity"] == "H": @@ -374,16 +393,14 @@ @type int """ self.__statistics["_FilesCount"] += 1 - stats = [k for k in statistics.keys() if k[0].isupper()] + stats = [k for k in statistics 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] + for key in stats: + self.__statistics[key]["total"] += statistics[key] + for key in ignoredErrors: + self.__statistics[key]["ignored"] += ignoredErrors[key] self.__statistics["_IssuesFixed"] += fixer - self.__statistics["_IgnoredErrors"] += ignoredErrors self.__statistics["_SecurityOK"] += securityOk def __updateFixerStatistics(self, fixer): @@ -399,11 +416,10 @@ """ Private slot to reset the statistics data. """ - self.__statistics = {} + self.__statistics.clear() self.__statistics["_FilesCount"] = 0 self.__statistics["_FilesIssues"] = 0 self.__statistics["_IssuesFixed"] = 0 - self.__statistics["_IgnoredErrors"] = 0 self.__statistics["_SecurityOK"] = 0 def prepare(self, fileList, project): @@ -497,10 +513,30 @@ ) if "AnnotationsChecker" not in self.__data: - self.__data["AnnotationsChecker"] = { - "MinimumCoverage": 75, - "MaximumComplexity": 3, - } + self.__data["AnnotationsChecker"] = copy.deepcopy( + AnnotationsCheckerDefaultArgs) + else: + # We are upgrading from an older data structure + if "MaximumLength" not in self.__data["AnnotationsChecker"]: + # MaximumLength is the sentinel for the first extension + self.__data["AnnotationsChecker"].update({ + "MaximumLength": + AnnotationsCheckerDefaultArgs["MaximumLength"], + "SuppressNoneReturning": + AnnotationsCheckerDefaultArgs["SuppressNoneReturning"], + "SuppressDummyArgs": + AnnotationsCheckerDefaultArgs["SuppressDummyArgs"], + "AllowUntypedDefs": + AnnotationsCheckerDefaultArgs["AllowUntypedDefs"], + "AllowUntypedNested": + AnnotationsCheckerDefaultArgs["AllowUntypedNested"], + "MypyInitReturn": + AnnotationsCheckerDefaultArgs["MypyInitReturn"], + "DispatchDecorators": + AnnotationsCheckerDefaultArgs["DispatchDecorators"], + "OverloadDecorators": + AnnotationsCheckerDefaultArgs["OverloadDecorators"], + }) if "SecurityChecker" not in self.__data: from .Security.SecurityDefaults import SecurityDefaults @@ -557,10 +593,30 @@ self.__data["CommentedCodeChecker"]["Aggressive"]) self.__initCommentedCodeCheckerWhiteList( self.__data["CommentedCodeChecker"]["WhiteList"]) + + # type annotations self.minAnnotationsCoverageSpinBox.setValue( self.__data["AnnotationsChecker"]["MinimumCoverage"]) self.maxAnnotationsComplexitySpinBox.setValue( self.__data["AnnotationsChecker"]["MaximumComplexity"]) + self.maxAnnotationsLengthSpinBox.setValue( + self.__data["AnnotationsChecker"]["MaximumLength"]) + self.suppressNoneReturningCheckBox.setChecked( + self.__data["AnnotationsChecker"]["SuppressNoneReturning"]) + self.suppressDummyArgsCheckBox.setChecked( + self.__data["AnnotationsChecker"]["SuppressDummyArgs"]) + self.allowUntypedDefsCheckBox.setChecked( + self.__data["AnnotationsChecker"]["AllowUntypedDefs"]) + self.allowUntypedNestedCheckBox.setChecked( + self.__data["AnnotationsChecker"]["AllowUntypedNested"]) + self.mypyInitReturnCheckBox.setChecked( + self.__data["AnnotationsChecker"]["MypyInitReturn"]) + self.dispatchDecoratorEdit.setText( + ", ".join( + self.__data["AnnotationsChecker"]["DispatchDecorators"])) + self.overloadDecoratorEdit.setText( + ", ".join( + self.__data["AnnotationsChecker"]["OverloadDecorators"])) # security self.tmpDirectoriesEdit.setPlainText("\n".join( @@ -712,11 +768,30 @@ "WhiteList": self.__getCommentedCodeCheckerWhiteList(), } } + annotationArgs = { "MinimumCoverage": self.minAnnotationsCoverageSpinBox.value(), "MaximumComplexity": self.maxAnnotationsComplexitySpinBox.value(), + "MaximumLength": + self.maxAnnotationsLengthSpinBox.value(), + "SuppressNoneReturning": + self.suppressNoneReturningCheckBox.isChecked(), + "SuppressDummyArgs": + self.suppressDummyArgsCheckBox.isChecked(), + "AllowUntypedDefs": + self.allowUntypedDefsCheckBox.isChecked(), + "AllowUntypedNested": + self.allowUntypedNestedCheckBox.isChecked(), + "MypyInitReturn": + self.mypyInitReturnCheckBox.isChecked(), + "DispatchDecorators": + [d.strip() + for d in self.dispatchDecoratorEdit.text().split(",")], + "OverloadDecorators": + [d.strip() + for d in self.overloadDecoratorEdit.text().split(",")], } securityArgs = { @@ -785,7 +860,7 @@ """ options = self.__options[:] flags = Utilities.extractFlags(source) - if "noqa" in flags and isinstance(flags["noqa"], basestring): + if "noqa" in flags and isinstance(flags["noqa"], str): excludeMessages = options[0].strip().rstrip(",") if excludeMessages: excludeMessages += "," @@ -864,11 +939,9 @@ self.__lastFileItem = None self.checkProgressLabel.setPath(self.tr("Preparing files...")) - progress = 0 argumentsList = [] - for filename in self.files: - progress += 1 + for progress, filename in enumerate(self.files, start=1): self.checkProgress.setValue(progress) QApplication.processEvents() @@ -957,7 +1030,7 @@ self.resultList.setSortingEnabled(False) fixed = None - ignoredErrors = 0 + ignoredErrors = collections.defaultdict(int) securityOk = 0 if self.__itms: for itm, result in zip(self.__itms, results): @@ -968,7 +1041,7 @@ for result in results: if result["ignored"]: - ignoredErrors += 1 + ignoredErrors[result["code"]] += 1 if self.showIgnored: result["display"] = self.tr( "{0} (ignored)" @@ -978,11 +1051,12 @@ elif result["securityOk"]: securityOk += 1 - continue + if result["code"].startswith("S"): + continue self.results = CodeStyleCheckerDialog.hasResults self.__createResultItem(fn, result) - + self.__updateStatistics( codeStyleCheckerStats, fixes, ignoredErrors, securityOk) @@ -1060,6 +1134,12 @@ QHeaderView.ResizeMode.ResizeToContents) self.resultList.header().setStretchLastSection(True) + if self.__detectedCodes: + self.filterComboBox.addItem("") + self.filterComboBox.addItems(sorted(set(self.__detectedCodes))) + self.filterComboBox.setEnabled(True) + self.filterButton.setEnabled(True) + self.checkProgress.setVisible(False) self.checkProgressLabel.setVisible(False) @@ -1074,10 +1154,11 @@ @return eol string @rtype str """ - if self.__project.isOpen() and self.__project.isProjectFile(fn): - eol = self.__project.getEolString() - else: - eol = Utilities.linesep() + eol = ( + self.__project.getEolString() + if self.__project.isOpen() and self.__project.isProjectFile(fn) + else Utilities.linesep() + ) return eol @pyqtSlot() @@ -1124,6 +1205,26 @@ self.minAnnotationsCoverageSpinBox.value(), "MaximumComplexity": self.maxAnnotationsComplexitySpinBox.value(), + "MaximumLength": + self.maxAnnotationsLengthSpinBox.value(), + "SuppressNoneReturning": + self.suppressNoneReturningCheckBox.isChecked(), + "SuppressDummyArgs": + self.suppressDummyArgsCheckBox.isChecked(), + "AllowUntypedDefs": + self.allowUntypedDefsCheckBox.isChecked(), + "AllowUntypedNested": + self.allowUntypedNestedCheckBox.isChecked(), + "MypyInitReturn": + self.mypyInitReturnCheckBox.isChecked(), + "DispatchDecorators": + [d.strip() + for d in self.dispatchDecoratorEdit.text().split(",") + ], + "OverloadDecorators": + [d.strip() + for d in self.overloadDecoratorEdit.text().split(",") + ], }, "SecurityChecker": { "HardcodedTmpDirectories": [ @@ -1156,7 +1257,10 @@ self.typedExceptionsCheckBox.isChecked(), }, } - if data != self.__data: + if ( + json.dumps(data, sort_keys=True) != + json.dumps(self.__data, sort_keys=True) + ): self.__data = data self.__project.setData("CHECKERSPARMS", "Pep8Checker", self.__data) @@ -1164,6 +1268,10 @@ self.resultList.clear() self.results = CodeStyleCheckerDialog.noResults self.cancelled = False + self.__detectedCodes.clear() + self.filterComboBox.clear() + self.filterComboBox.setEnabled(False) + self.filterButton.setEnabled(False) self.start(self.__fileOrFileList) @@ -1404,12 +1512,48 @@ "CommentedCodeChecker"]["WhiteList"] ) )) + + # type annotations self.minAnnotationsCoverageSpinBox.setValue(int( Preferences.Prefs.settings.value( - "PEP8/MinimumAnnotationsCoverage", 75))) + "PEP8/MinimumAnnotationsCoverage", + AnnotationsCheckerDefaultArgs["MinimumCoverage"]))) self.maxAnnotationsComplexitySpinBox.setValue(int( Preferences.Prefs.settings.value( - "PEP8/MaximumAnnotationComplexity", 3))) + "PEP8/MaximumAnnotationComplexity", + AnnotationsCheckerDefaultArgs["MaximumComplexity"]))) + self.maxAnnotationsLengthSpinBox.setValue(int( + Preferences.Prefs.settings.value( + "PEP8/MaximumAnnotationLength", + AnnotationsCheckerDefaultArgs["MaximumLength"]))) + self.suppressNoneReturningCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value( + "PEP8/SuppressNoneReturning", + AnnotationsCheckerDefaultArgs["SuppressNoneReturning"]))) + self.suppressDummyArgsCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value( + "PEP8/SuppressDummyArgs", + AnnotationsCheckerDefaultArgs["SuppressDummyArgs"]))) + self.allowUntypedDefsCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value( + "PEP8/AllowUntypedDefs", + AnnotationsCheckerDefaultArgs["AllowUntypedDefs"]))) + self.allowUntypedNestedCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value( + "PEP8/AllowUntypedNested", + AnnotationsCheckerDefaultArgs["AllowUntypedNested"]))) + self.mypyInitReturnCheckBox.setChecked(Preferences.toBool( + Preferences.Prefs.settings.value( + "PEP8/MypyInitReturn", + AnnotationsCheckerDefaultArgs["MypyInitReturn"]))) + self.dispatchDecoratorEdit.setText(", ".join(Preferences.toList( + Preferences.Prefs.settings.value( + "PEP8/DispatchDecorators", + AnnotationsCheckerDefaultArgs["DispatchDecorators"])))) + self.overloadDecoratorEdit.setText(", ".join(Preferences.toList( + Preferences.Prefs.settings.value( + "PEP8/OverloadDecorators", + AnnotationsCheckerDefaultArgs["OverloadDecorators"])))) # security from .Security.SecurityDefaults import SecurityDefaults @@ -1517,12 +1661,40 @@ Preferences.Prefs.settings.setValue( "PEP8/CommentedCodeWhitelist", self.__getCommentedCodeCheckerWhiteList()) + + # type annotations Preferences.Prefs.settings.setValue( "PEP8/MinimumAnnotationsCoverage", self.minAnnotationsCoverageSpinBox.value()) Preferences.Prefs.settings.setValue( "PEP8/MaximumAnnotationComplexity", self.maxAnnotationsComplexitySpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/MaximumAnnotationLength", + self.maxAnnotationsLengthSpinBox.value()) + Preferences.Prefs.settings.setValue( + "PEP8/SuppressNoneReturning", + self.suppressNoneReturningCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/SuppressDummyArgs", + self.suppressDummyArgsCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/AllowUntypedDefs", + self.allowUntypedDefsCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/AllowUntypedNested", + self.allowUntypedNestedCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/MypyInitReturn", + self.mypyInitReturnCheckBox.isChecked()) + Preferences.Prefs.settings.setValue( + "PEP8/DispatchDecorators", + [d.strip() + for d in self.dispatchDecoratorEdit.text().split(",")]) + Preferences.Prefs.settings.setValue( + "PEP8/OverloadDecorators", + [d.strip() + for d in self.overloadDecoratorEdit.text().split(",")]) # security Preferences.Prefs.settings.setValue( @@ -1620,10 +1792,38 @@ MiscellaneousCheckerDefaultArgs[ "CommentedCodeChecker"]["WhiteList"] ) + + # type annotations Preferences.Prefs.settings.setValue( - "PEP8/MinimumAnnotationsCoverage", 75) + "PEP8/MinimumAnnotationsCoverage", + AnnotationsCheckerDefaultArgs["MinimumCoverage"]) + Preferences.Prefs.settings.setValue( + "PEP8/MaximumAnnotationComplexity", + AnnotationsCheckerDefaultArgs["MaximumComplexity"]) + Preferences.Prefs.settings.setValue( + "PEP8/MaximumAnnotationLength", + AnnotationsCheckerDefaultArgs["MaximumLength"]) + Preferences.Prefs.settings.setValue( + "PEP8/SuppressNoneReturning", + AnnotationsCheckerDefaultArgs["SuppressNoneReturning"]) + Preferences.Prefs.settings.setValue( + "PEP8/SuppressDummyArgs", + AnnotationsCheckerDefaultArgs["SuppressDummyArgs"]) Preferences.Prefs.settings.setValue( - "PEP8/MaximumAnnotationComplexity", 3) + "PEP8/AllowUntypedDefs", + AnnotationsCheckerDefaultArgs["AllowUntypedDefs"]) + Preferences.Prefs.settings.setValue( + "PEP8/AllowUntypedNested", + AnnotationsCheckerDefaultArgs["AllowUntypedNested"]) + Preferences.Prefs.settings.setValue( + "PEP8/MypyInitReturn", + AnnotationsCheckerDefaultArgs["MypyInitReturn"]) + Preferences.Prefs.settings.setValue( + "PEP8/DispatchDecorators", + AnnotationsCheckerDefaultArgs["DispatchDecorators"]) + Preferences.Prefs.settings.setValue( + "PEP8/OverloadDecorators", + AnnotationsCheckerDefaultArgs["OverloadDecorators"]) # security from .Security.SecurityDefaults import SecurityDefaults @@ -1779,12 +1979,11 @@ @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 = [] + expectedImports = ( + [i.strip() for i in selectedFutures.split(",") if bool(i.strip())] + if selectedFutures else + [] + ) for row in range(self.futuresList.count()): itm = self.futuresList.item(row) if itm.text() in expectedImports: @@ -1878,13 +2077,12 @@ categories @type str """ - if enabledCategories: - enabledCategoriesList = [ - c.strip() for c in enabledCategories.split(",") - if bool(c.strip())] - else: - enabledCategoriesList = list( - CodeStyleCheckerDialog.checkCategories.keys()) + enabledCategoriesList = ( + [c.strip() for c in enabledCategories.split(",") + if bool(c.strip())] + if enabledCategories else + list(CodeStyleCheckerDialog.checkCategories.keys()) + ) for row in range(self.categoriesList.count()): itm = self.categoriesList.item(row) if itm.data(Qt.ItemDataRole.UserRole) in enabledCategoriesList: @@ -2023,3 +2221,27 @@ row = self.whitelistWidget.row(itm) self.whitelistWidget.takeItem(row) del itm + + @pyqtSlot() + def on_filterButton_clicked(self): + """ + Private slot to filter the list of messages based on selected message + code. + """ + selectedMessageCode = self.filterComboBox.currentText() + + for topRow in range(self.resultList.topLevelItemCount()): + topItem = self.resultList.topLevelItem(topRow) + topItem.setExpanded(True) + visibleChildren = topItem.childCount() + for childIndex in range(topItem.childCount()): + childItem = topItem.child(childIndex) + hideChild = ( + childItem.data(0, self.codeRole) != selectedMessageCode + if selectedMessageCode else + False + ) + childItem.setHidden(hideChild) + if hideChild: + visibleChildren -= 1 + topItem.setHidden(visibleChildren == 0)