Thu, 02 Dec 2021 18:53:26 +0100
Continued implementing a checker for import statements (import order).
--- a/eric7.epj Wed Dec 01 20:09:57 2021 +0100 +++ b/eric7.epj Thu Dec 02 18:53:26 2021 +0100 @@ -2301,7 +2301,9 @@ "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py", "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py", "eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerUtilities.py", - "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py" + "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py", + "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsEnums.py", + "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportNode.py" ], "SPELLEXCLUDES": "Dictionaries/excludes.dic", "SPELLLANGUAGE": "en_US",
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Wed Dec 01 20:09:57 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Thu Dec 02 18:53:26 2021 +0100 @@ -548,7 +548,7 @@ if "ImportsChecker" not in self.__data: self.__data["ImportsChecker"] = { - "ApplicationModuleNames": [], + "ApplicationPackageNames": [], } self.__initCategoriesList(self.__data["EnabledCheckerCategories"]) @@ -813,8 +813,8 @@ } importsArgs = { - "ApplicationModuleNames": - sorted(self.appModulesEdit.toPlainText().split()), + "ApplicationPackageNames": + sorted(self.appPackagesEdit.toPlainText().split()), } self.__options = [excludeMessages, includeMessages, repeatMessages, @@ -1250,8 +1250,8 @@ self.typedExceptionsCheckBox.isChecked(), }, "ImportsChecker": { - "ApplicationModuleNames": - sorted(self.appModulesEdit.toPlainText().split()) + "ApplicationPackageNames": + sorted(self.appPackagesEdit.toPlainText().split()) }, } if ( @@ -1596,9 +1596,9 @@ SecurityDefaults["check_typed_exception"]))), # Imports Checker - self.appModulesEdit.setPlainText(" ".join( + self.appPackagesEdit.setPlainText(" ".join( sorted(Preferences.toList(Preferences.getSettings().value( - "PEP8/ApplicationModuleNames", []))))) + "PEP8/ApplicationPackageNames", []))))) self.__cleanupData() @@ -1738,8 +1738,8 @@ # Imports Checker Preferences.getSettings().setValue( - "PEP8/ApplicationModuleNames", - sorted(self.appModulesEdit.toPlainText().split())) + "PEP8/ApplicationPackageNames", + sorted(self.appPackagesEdit.toPlainText().split())) @pyqtSlot() def on_resetDefaultButton_clicked(self): @@ -1867,7 +1867,7 @@ # Imports Checker Preferences.getSettings().setValue( - "PEP8/ApplicationModuleNames", []) + "PEP8/ApplicationPackageNames", []) # Update UI with default values self.on_loadDefaultButton_clicked()
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Wed Dec 01 20:09:57 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Thu Dec 02 18:53:26 2021 +0100 @@ -267,8 +267,8 @@ <rect> <x>0</x> <y>0</y> - <width>611</width> - <height>1417</height> + <width>365</width> + <height>1152</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_4"> @@ -1261,13 +1261,13 @@ <item> <widget class="QGroupBox" name="groupBox_15"> <property name="title"> - <string>Application Modules</string> + <string>Application Packages</string> </property> <layout class="QVBoxLayout" name="verticalLayout_14"> <item> <widget class="QLabel" name="label_35"> <property name="text"> - <string>Enter top level application module names separated by a space character:</string> + <string>Enter top level application package names separated by a space character:</string> </property> <property name="wordWrap"> <bool>true</bool> @@ -1275,7 +1275,7 @@ </widget> </item> <item> - <widget class="QPlainTextEdit" name="appModulesEdit"/> + <widget class="QPlainTextEdit" name="appPackagesEdit"/> </item> </layout> </widget> @@ -1707,7 +1707,7 @@ <tabstop>ecHighRiskCombo</tabstop> <tabstop>ecMediumRiskCombo</tabstop> <tabstop>typedExceptionsCheckBox</tabstop> - <tabstop>appModulesEdit</tabstop> + <tabstop>appPackagesEdit</tabstop> <tabstop>startButton</tabstop> <tabstop>loadDefaultButton</tabstop> <tabstop>storeDefaultButton</tabstop>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportNode.py Thu Dec 02 18:53:26 2021 +0100 @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class representing an import or import from node. +""" + +# +# adapted from flake8-alphabetize v0.0.17 +# + +import ast +from functools import total_ordering + +from .ImportsEnums import GroupEnum, NodeTypeEnum + + +class ImportNodeException(Exception): + """ + Class representing an exception for an invalid import node. + """ + pass + + +@total_ordering +class ImportNode: + """ + Class representing an import or import from node. + """ + def __init__(self, appNames, astNode, checker): + """ + Constructor + + @param appNames list of application package names + @type list of str + @param astNode reference to the ast node + @type ast.AST + @param checker reference to the checker object + @type ImportsChecker + @exception ImportNodeException raised to indicate an invalid node was + given to this class + """ + if ( + self.nodeType not in (NodeTypeEnum.IMPORT, + NodeTypeEnum.IMPORT_FROM) + ): + raise ImportNodeException( + "Node type {0} not recognized".format(type(astNode)) + ) + + self.node = astNode + self.error = None + level = None + group = None + + if isinstance(astNode, ast.Import): + self.nodeType = NodeTypeEnum.IMPORT + names = astNode.names + + self.moduleName = names[0].name + level = 0 + + elif isinstance(astNode, ast.ImportFrom): + module = astNode.module + self.moduleName = "" if module is None else module + self.nodeType = NodeTypeEnum.IMPORT_FROM + + names = [n.name for n in astNode.names] + expectedNames = sorted(names) + if names != expectedNames: + self.error = (self.node, "I202", ", ".join(expectedNames)) + level = astNode.level + + if self.moduleName == "__future__": + group = GroupEnum.FUTURE + elif self.moduleName in checker.getStandardModules(): + group = GroupEnum.STDLIB + elif level > 0: + group = GroupEnum.APPLICATION + else: + group = GroupEnum.THIRD_PARTY + for name in appNames: + if ( + name == self.moduleName or + self.moduleName.startswith("{0}.".format(name)) + ): + group = GroupEnum.APPLICATION + break + + if group == GroupEnum.STDLIB: + self.sorter = group, self.nodeType, self.moduleName + else: + m = self.moduleName + dotIndex = m.find(".") + topName = m if dotIndex == -1 else m[:dotIndex] + self.sorter = group, level, topName, self.nodeType, m + + def __eq__(self, other): + """ + Special method implementing the equality operator. + + @param other reference to the object to compare + @type ImportNode + @return flag indicating equality + @rtype bool + """ + return self.sorter == other.sorter + + def __lt__(self, other): + """ + Special method implementing the less than operator. + + @param other reference to the object to compare + @type ImportNode + @return flag indicating a less than situation + @rtype bool + """ + return self.sorter < other.sorter + + def __str__(self): + """ + Special method to create a string representation of the instance. + + @return string representation of the instance + @rtype str + @exception ImportNodeException raised to indicate an invalid node was + given to this class + """ + if ( + self.nodeType not in (NodeTypeEnum.IMPORT, + NodeTypeEnum.IMPORT_FROM) + ): + raise ImportNodeException( + "The node type {0} is not recognized.".format(self.nodeType) + ) + + if self.nodeType == NodeTypeEnum.IMPORT: + return "import {0}".format(self.moduleName) + elif self.nodeType == NodeTypeEnum.IMPORT_FROM: + level = self.node.level + levelStr = "" if level == 0 else "." * level + names = [ + n.name + ("" if n.asname is None else + " as {0}".format(n.asname)) + for n in self.node.names + ] + return "from {0}{1} import {2}".format( + levelStr, self.moduleName, ", ".join(names)) + + return None
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Wed Dec 01 20:09:57 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Thu Dec 02 18:53:26 2021 +0100 @@ -7,6 +7,7 @@ Module implementing a checker for import statements. """ +import ast import copy import sys @@ -18,6 +19,9 @@ Codes = [ ## Local imports "I101", "I102", "I103", + + ## Imports order + "I201", "I202", "I203", "I204", ] def __init__(self, source, filename, tree, select, ignore, expected, @@ -59,6 +63,7 @@ checkersWithCodes = [ (self.__checkLocalImports, ("I101", "I102", "I103")), + (self.__checkImportOrder, ("I201", "I202", "I203", "I204")) ] self.__checkers = [] @@ -198,6 +203,121 @@ visitor = LocalImportVisitor(self.__args, self) visitor.visit(copy.deepcopy(self.__tree)) for violation in visitor.violations: - node = violation[0] - reason = violation[1] - self.__error(node.lineno - 1, node.col_offset, reason) + if not self.__ignoreCode(violation[1]): + node = violation[0] + reason = violation[1] + self.__error(node.lineno - 1, node.col_offset, reason) + + ####################################################################### + ## Import order + ## + ## adapted from: flake8-alphabetize v0.0.17 + ####################################################################### + + def __checkImportOrder(self): + """ + Private method to check the order of import statements. + """ + from .ImportNode import ImportNode + + errors = [] + imports = [] + importNodes, listNode = self.__findNodes(self.__tree) + + # check for an error in '__all__' + allError = self.__findErrorInAll(listNode) + if allError is not None: + errors.append(allError) + + for importNode in importNodes: + if ( + isinstance(importNode, ast.Import) and + len(importNode.names) > 1 + ): + # skip suck imports because its already handled by pycodestyle + continue + + imports.append(ImportNode( + self.__args.get("ApplicationPackageNames", []), + importNode, self)) + + lenImports = len(imports) + if lenImports > 0: + p = imports[0] + if p.error is not None: + errors.append(p.error) + + if lenImports > 1: + for n in imports[1:]: + if n.error is not None: + errors.append(n.error) + + if n == p: + errors.append((n.node, "I203", str(p), str(n))) + elif n < p: + errors.append((n.node, "I201", str(n), str(p))) + + p = n + + for error in errors: + if not self.__ignoreCode(error[1]): + node = error[0] + reason = error[1] + args = error[2:] + self.__error(node.lineno - 1, node.col_offset, reason, *args) + + def __findNodes(self, tree): + """ + Private method to find all import and import from nodes of the given + tree. + + @param tree reference to the ast node tree to be parsed + @type ast.AST + @return tuple containing a list of import nodes and the '__all__' node + @rtype tuple of (ast.Import | ast.ImportFrom, ast.List | ast.Tuple) + """ + importNodes = [] + listNode = None + + if isinstance(tree, ast.Module): + body = tree.body + + for n in body: + if isinstance(n, (ast.Import, ast.ImportFrom)): + importNodes.append(n) + + elif isinstance(n, ast.Assign): + for t in n.targets: + if isinstance(t, ast.Name) and t.id == "__all__": + value = n.value + + if isinstance(value, (ast.List, ast.Tuple)): + listNode = value + + return importNodes, listNode + + def __findErrorInAll(self, node): + """ + Private method to check the '__all__' node for errors. + + @param node reference to the '__all__' node + @type ast.List or ast.Tuple + @return tuple containing a reference to the node and an error code + @rtype rtype tuple of (ast.List | ast.Tuple, str) + """ + if node is not None: + actualList = [] + for el in node.elts: + if isinstance(el, ast.Constant): + actualList.append(el.value) + elif isinstance(el, ast.Str): + actualList.append(el.s) + else: + # Can't handle anything that isn't a string literal + return None + + expectedList = sorted(actualList) + if expectedList != actualList: + return (node, "I204", ", ".join(expectedList)) + + return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsEnums.py Thu Dec 02 18:53:26 2021 +0100 @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some enums for the import order checker. +""" + +# +# adapted from flake8-alphabetize v0.0.17 +# + +import enum + + +class GroupEnum(enum.IntEnum): + """ + Class representing the various import groups. + """ + FUTURE = 1 + STDLIB = 2 + THIRD_PARTY = 3 + APPLICATION = 4 + + +class NodeTypeEnum(enum.IntEnum): + """ + Class representing the import node types. + """ + IMPORT = 1 + IMPORT_FROM = 2
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py Wed Dec 01 20:09:57 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py Thu Dec 02 18:53:26 2021 +0100 @@ -27,7 +27,7 @@ @param checker reference to the checker @type ImportsChecker """ - self.__appImportNames = args.get("ApplicationModuleNames", []) + self.__appImportNames = args.get("ApplicationPackageNames", []) self.__checker = checker self.violations = []
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py Wed Dec 01 20:09:57 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py Thu Dec 02 18:53:26 2021 +0100 @@ -20,8 +20,28 @@ "I103": QCoreApplication.translate( "ImportsChecker", "packages from standard modules should not be imported locally"), + + "I201": QCoreApplication.translate( + "ImportsChecker", + "Import statements are in the wrong order. " + "'{0}' should be before '{1}'"), + "I202": QCoreApplication.translate( + "ImportsChecker", + "Imported names are in the wrong order. " + "Should be {0}"), + "I203": QCoreApplication.translate( + "ImportsChecker", + "Import statements should be combined. " + "'{0}' should be combined with '{1}'"), + "I204": QCoreApplication.translate( + "ImportsChecker", + "The names in __all__ are in the wrong order. " + "The order should be {0}"), } _importsMessagesSampleArgs = { - + "I201": ["import os", "import sys"], + "I202": ["copy, os, sys"], + "I203": ["from foo import bar", "from foo import baz"], + "I204": ["bar, baz, foo"], }