--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Wed Jul 13 14:55:47 2022 +0200 @@ -16,22 +16,28 @@ """ Class implementing a checker for import statements. """ + Codes = [ ## Local imports - "I101", "I102", "I103", - + "I101", + "I102", + "I103", ## Imports order - "I201", "I202", "I203", "I204", - + "I201", + "I202", + "I203", + "I204", ## Various other import related - "I901", "I902", "I903", "I904", + "I901", + "I902", + "I903", + "I904", ] - def __init__(self, source, filename, tree, select, ignore, expected, - repeat, args): + def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): """ Constructor - + @param source source code to be checked @type list of str @param filename name of the source file @@ -57,25 +63,24 @@ self.__source = source[:] self.__tree = copy.deepcopy(tree) self.__args = args - + # statistics counters self.counters = {} - + # collection of detected errors self.errors = [] - + checkersWithCodes = [ (self.__checkLocalImports, ("I101", "I102", "I103")), (self.__checkImportOrder, ("I201", "I202", "I203", "I204")), (self.__tidyImports, ("I901", "I902", "I903", "I904")), ] - + self.__checkers = [] for checker, codes in checkersWithCodes: - if any(not (code and self.__ignoreCode(code)) - for code in codes): + if any(not (code and self.__ignoreCode(code)) for code in codes): self.__checkers.append(checker) - + def __ignoreCode(self, code): """ Private method to check if the message code should be ignored. @@ -85,13 +90,12 @@ @return flag indicating to ignore the given code @rtype bool """ - return (code.startswith(self.__ignore) and - not code.startswith(self.__select)) - + return code.startswith(self.__ignore) and not code.startswith(self.__select) + def __error(self, lineNumber, offset, code, *args): """ Private method to record an issue. - + @param lineNumber line number of the issue @type int @param offset position within line of the issue @@ -103,16 +107,16 @@ """ if self.__ignoreCode(code): return - + if code in self.counters: self.counters[code] += 1 else: self.counters[code] = 1 - + # Don't care about expected codes if code in self.__expected: return - + if code and (self.counters[code] == 1 or self.__repeat): # record the issue with one based line number self.errors.append( @@ -124,7 +128,7 @@ "args": args, } ) - + def run(self): """ Public method to check the given source against miscellaneous @@ -133,18 +137,18 @@ if not self.__filename: # don't do anything, if essential data is missing return - + if not self.__checkers: # don't do anything, if no codes were selected return - + for check in self.__checkers: check() - + def getStandardModules(self): """ Public method to get a list of modules of the standard library. - + @return set of builtin modules @rtype set of str """ @@ -152,58 +156,231 @@ return sys.stdlib_module_names except AttributeError: return { - "__future__", "__main__", "_dummy_thread", "_thread", "abc", - "aifc", "argparse", "array", "ast", "asynchat", "asyncio", - "asyncore", "atexit", "audioop", "base64", "bdb", "binascii", - "binhex", "bisect", "builtins", "bz2", "calendar", "cgi", - "cgitb", "chunk", "cmath", "cmd", "code", "codecs", "codeop", - "collections", "colorsys", "compileall", "concurrent", - "configparser", "contextlib", "contextvars", "copy", "copyreg", - "cProfile", "crypt", "csv", "ctypes", "curses", "dataclasses", - "datetime", "dbm", "decimal", "difflib", "dis", "distutils", - "doctest", "dummy_threading", "email", "encodings", - "ensurepip", "enum", "errno", "faulthandler", "fcntl", - "filecmp", "fileinput", "fnmatch", "formatter", "fractions", - "ftplib", "functools", "gc", "getopt", "getpass", "gettext", - "glob", "grp", "gzip", "hashlib", "heapq", "hmac", "html", - "http", "imaplib", "imghdr", "imp", "importlib", "inspect", - "io", "ipaddress", "itertools", "json", "keyword", "lib2to3", - "linecache", "locale", "logging", "lzma", "mailbox", "mailcap", - "marshal", "math", "mimetypes", "mmap", "modulefinder", - "msilib", "msvcrt", "multiprocessing", "netrc", "nis", - "nntplib", "numbers", "operator", "optparse", "os", - "ossaudiodev", "parser", "pathlib", "pdb", "pickle", - "pickletools", "pipes", "pkgutil", "platform", "plistlib", - "poplib", "posix", "pprint", "profile", "pstats", "pty", "pwd", - "py_compile", "pyclbr", "pydoc", "queue", "quopri", "random", - "re", "readline", "reprlib", "resource", "rlcompleter", - "runpy", "sched", "secrets", "select", "selectors", "shelve", - "shlex", "shutil", "signal", "site", "smtpd", "smtplib", - "sndhdr", "socket", "socketserver", "spwd", "sqlite3", "ssl", - "stat", "statistics", "string", "stringprep", "struct", - "subprocess", "sunau", "symbol", "symtable", "sys", - "sysconfig", "syslog", "tabnanny", "tarfile", "telnetlib", - "tempfile", "termios", "test", "textwrap", "threading", "time", - "timeit", "tkinter", "token", "tokenize", "trace", "traceback", - "tracemalloc", "tty", "turtle", "turtledemo", "types", - "typing", "unicodedata", "unittest", "urllib", "uu", "uuid", - "venv", "warnings", "wave", "weakref", "webbrowser", "winreg", - "winsound", "wsgiref", "xdrlib", "xml", "xmlrpc", "zipapp", - "zipfile", "zipimport", "zlib", "zoneinfo", + "__future__", + "__main__", + "_dummy_thread", + "_thread", + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "cProfile", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fractions", + "ftplib", + "functools", + "gc", + "getopt", + "getpass", + "gettext", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", + "zoneinfo", } - + ####################################################################### ## Local imports ## ## adapted from: flake8-local-import v1.0.6 ####################################################################### - + def __checkLocalImports(self): """ Private method to check local imports. """ from .LocalImportVisitor import LocalImportVisitor - + visitor = LocalImportVisitor(self.__args, self) visitor.visit(copy.deepcopy(self.__tree)) for violation in visitor.violations: @@ -211,70 +388,69 @@ 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 - ): + 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)) - + + 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 @@ -282,14 +458,14 @@ """ 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__": @@ -297,13 +473,13 @@ 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 @@ -323,46 +499,40 @@ expectedList = sorted(actualList) if expectedList != actualList: return (node, "I204", ", ".join(expectedList)) - + return None - + ####################################################################### ## Tidy imports ## ## adapted from: flake8-tidy-imports v4.5.0 ####################################################################### - + def __tidyImports(self): """ Private method to check various other import related topics. """ self.__bannedModules = self.__args.get("BannedModules", []) self.__banRelativeImports = self.__args.get("BanRelativeImports", "") - + ruleMethods = [] if not self.__ignoreCode("I901"): ruleMethods.append(self.__checkUnnecessaryAlias) - if ( - not self.__ignoreCode("I902") and - bool(self.__bannedModules) - ): + if not self.__ignoreCode("I902") and bool(self.__bannedModules): ruleMethods.append(self.__checkBannedImport) if ( - (not self.__ignoreCode("I903") and - self.__banRelativeImports == "parents") or - (not self.__ignoreCode("I904") and - self.__banRelativeImports == "true") - ): + not self.__ignoreCode("I903") and self.__banRelativeImports == "parents" + ) or (not self.__ignoreCode("I904") and self.__banRelativeImports == "true"): ruleMethods.append(self.__checkBannedRelativeImports) - + for node in ast.walk(self.__tree): for method in ruleMethods: method(node) - + def __checkUnnecessaryAlias(self, node): """ Private method to check unnecessary import aliases. - + @param node reference to the node to be checked @type ast.AST """ @@ -373,36 +543,32 @@ importedName = alias.name else: fromName, importedName = alias.name.rsplit(".", 1) - + if importedName == alias.asname: if fromName: - rewritten = "from {0} import {1}".format( - fromName, importedName) + rewritten = "from {0} import {1}".format(fromName, importedName) else: rewritten = "import {0}".format(importedName) - - self.__error(node.lineno - 1, node.col_offset, "I901", - rewritten) - + + self.__error(node.lineno - 1, node.col_offset, "I901", rewritten) + elif isinstance(node, ast.ImportFrom): for alias in node.names: if alias.name == alias.asname: - rewritten = "from {0} import {1}".format( - node.module, alias.name) - - self.__error(node.lineno - 1, node.col_offset, "I901", - rewritten) - + rewritten = "from {0} import {1}".format(node.module, alias.name) + + self.__error(node.lineno - 1, node.col_offset, "I901", rewritten) + def __checkBannedImport(self, node): """ Private method to check import of banned modules. - + @param node reference to the node to be checked @type ast.AST """ if not bool(self.__bannedModules): return - + if isinstance(node, ast.Import): moduleNames = [alias.name for alias in node.names] elif isinstance(node, ast.ImportFrom): @@ -412,12 +578,12 @@ moduleNames.append("{0}.{1}".format(nodeModule, alias.name)) else: return - + # Sort from most to least specific paths. moduleNames.sort(key=len, reverse=True) - + warned = set() - + for moduleName in moduleNames: if moduleName in self.__bannedModules: if any(mod.startswith(moduleName) for mod in warned): @@ -426,29 +592,28 @@ continue else: warned.add(moduleName) - self.__error(node.lineno - 1, node.col_offset, "I902", - moduleName) - + self.__error(node.lineno - 1, node.col_offset, "I902", moduleName) + def __checkBannedRelativeImports(self, node): """ Private method to check if relative imports are banned. - + @param node reference to the node to be checked @type ast.AST """ if not self.__banRelativeImports: return - + elif self.__banRelativeImports == "parents": minNodeLevel = 1 msgCode = "I903" else: minNodeLevel = 0 msgCode = "I904" - + if ( - self.__banRelativeImports and - isinstance(node, ast.ImportFrom) and - node.level > minNodeLevel + self.__banRelativeImports + and isinstance(node, ast.ImportFrom) + and node.level > minNodeLevel ): self.__error(node.lineno - 1, node.col_offset, msgCode)