Wed, 01 Dec 2021 20:09:57 +0100
Continued implementing a checker for import statements.
--- a/eric7.epj Wed Dec 01 18:03:11 2021 +0100 +++ b/eric7.epj Wed Dec 01 20:09:57 2021 +0100 @@ -2300,7 +2300,8 @@ "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/__init__.py", "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py", "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py", - "eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerUtilities.py" + "eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerUtilities.py", + "eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py" ], "SPELLEXCLUDES": "Dictionaries/excludes.dic", "SPELLLANGUAGE": "en_US",
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Wed Dec 01 18:03:11 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Wed Dec 01 20:09:57 2021 +0100 @@ -177,10 +177,11 @@ @param args arguments used by the codeStyleCheck function (list of excludeMessages, includeMessages, repeatMessages, fixCodes, noFixCodes, fixIssues, maxLineLength, maxDocLineLength, blankLines, - hangClosing, docType, codeComplexityArgs, miscellaneousArgs, errors, - eol, encoding, backup) + hangClosing, docType, codeComplexityArgs, miscellaneousArgs, + annotationArgs, securityArgs, importsArgs, errors, eol, encoding, + backup) @type list of (str, str, bool, str, str, bool, int, list of (int, int), - bool, str, dict, dict, list of str, str, str, bool) + bool, str, dict, dict, dict, dict, list of str, str, str, bool) @return tuple of statistics (dict) and list of results (tuple for each found violation of style (lineno, position, text, ignored, fixed, autofixing, fixedMsg)) @@ -338,9 +339,10 @@ excludeMessages, includeMessages, repeatMessages, fixCodes, noFixCodes, fixIssues, maxLineLength, maxDocLineLength, blankLines, hangClosing, docType, codeComplexityArgs, miscellaneousArgs, - annotationArgs, securityArgs, errors, eol, encoding, backup) + annotationArgs, securityArgs, importsArgs, errors, eol, encoding, + backup) @type list of (str, str, bool, str, str, bool, int, list of (int, int), - bool, str, dict, dict, dict, list of str, str, str, bool) + bool, str, dict, dict, dict, dict, list of str, str, str, bool) @return tuple of statistics data and list of result dictionaries with keys: <ul> @@ -360,7 +362,7 @@ (excludeMessages, includeMessages, repeatMessages, fixCodes, noFixCodes, fixIssues, maxLineLength, maxDocLineLength, blankLines, hangClosing, docType, codeComplexityArgs, miscellaneousArgs, annotationArgs, - securityArgs, errors, eol, encoding, backup) = args + securityArgs, importsArgs, errors, eol, encoding, backup) = args stats = {} @@ -468,8 +470,8 @@ # check import statements importsChecker = ImportsChecker( - source, filename, tree, select, ignore, [], repeatMessages, {}) - # TODO: add arguments + source, filename, tree, select, ignore, [], repeatMessages, + importsArgs) importsChecker.run() stats.update(importsChecker.counters) errors += importsChecker.errors
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Wed Dec 01 18:03:11 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Wed Dec 01 20:09:57 2021 +0100 @@ -546,6 +546,11 @@ SecurityDefaults["check_typed_exception"], } + if "ImportsChecker" not in self.__data: + self.__data["ImportsChecker"] = { + "ApplicationModuleNames": [], + } + self.__initCategoriesList(self.__data["EnabledCheckerCategories"]) self.excludeFilesEdit.setText(self.__data["ExcludeFiles"]) self.excludeMessagesEdit.setText(self.__data["ExcludeMessages"]) @@ -807,11 +812,16 @@ self.typedExceptionsCheckBox.isChecked(), } + importsArgs = { + "ApplicationModuleNames": + sorted(self.appModulesEdit.toPlainText().split()), + } + self.__options = [excludeMessages, includeMessages, repeatMessages, fixCodes, noFixCodes, fixIssues, maxLineLength, maxDocLineLength, blankLines, hangClosing, docType, codeComplexityArgs, miscellaneousArgs, - annotationArgs, securityArgs] + annotationArgs, securityArgs, importsArgs] # now go through all the files self.progress = 0 @@ -1239,6 +1249,10 @@ "CheckTypedException": self.typedExceptionsCheckBox.isChecked(), }, + "ImportsChecker": { + "ApplicationModuleNames": + sorted(self.appModulesEdit.toPlainText().split()) + }, } if ( json.dumps(data, sort_keys=True) != @@ -1496,7 +1510,7 @@ ) )) - # type annotations + # Type Annotations Checker self.minAnnotationsCoverageSpinBox.setValue(int( Preferences.getSettings().value( "PEP8/MinimumAnnotationsCoverage", @@ -1538,7 +1552,7 @@ "PEP8/OverloadDecorators", AnnotationsCheckerDefaultArgs["OverloadDecorators"])))) - # security + # Security Checker from .Security.SecurityDefaults import SecurityDefaults self.tmpDirectoriesEdit.setPlainText("\n".join( Preferences.toList(Preferences.getSettings().value( @@ -1581,6 +1595,11 @@ "PEP8/CheckTypedException", SecurityDefaults["check_typed_exception"]))), + # Imports Checker + self.appModulesEdit.setPlainText(" ".join( + sorted(Preferences.toList(Preferences.getSettings().value( + "PEP8/ApplicationModuleNames", []))))) + self.__cleanupData() @pyqtSlot() @@ -1645,7 +1664,7 @@ "PEP8/CommentedCodeWhitelist", self.__getCommentedCodeCheckerWhiteList()) - # type annotations + # Type Annotations Checker Preferences.getSettings().setValue( "PEP8/MinimumAnnotationsCoverage", self.minAnnotationsCoverageSpinBox.value()) @@ -1679,7 +1698,7 @@ [d.strip() for d in self.overloadDecoratorEdit.text().split(",")]) - # security + # Security Checker Preferences.getSettings().setValue( "PEP8/HardcodedTmpDirectories", [t.strip() @@ -1716,6 +1735,11 @@ Preferences.getSettings().setValue( "PEP8/CheckTypedException", self.typedExceptionsCheckBox.isChecked()), + + # Imports Checker + Preferences.getSettings().setValue( + "PEP8/ApplicationModuleNames", + sorted(self.appModulesEdit.toPlainText().split())) @pyqtSlot() def on_resetDefaultButton_clicked(self): @@ -1776,7 +1800,7 @@ "CommentedCodeChecker"]["WhiteList"] ) - # type annotations + # Type Annotations Checker Preferences.getSettings().setValue( "PEP8/MinimumAnnotationsCoverage", AnnotationsCheckerDefaultArgs["MinimumCoverage"]) @@ -1808,7 +1832,7 @@ "PEP8/OverloadDecorators", AnnotationsCheckerDefaultArgs["OverloadDecorators"]) - # security + # Security Checker from .Security.SecurityDefaults import SecurityDefaults Preferences.getSettings().setValue( "PEP8/HardcodedTmpDirectories", @@ -1841,6 +1865,10 @@ "PEP8/CheckTypedException", SecurityDefaults["check_typed_exception"]) + # Imports Checker + Preferences.getSettings().setValue( + "PEP8/ApplicationModuleNames", []) + # Update UI with default values self.on_loadDefaultButton_clicked()
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Wed Dec 01 18:03:11 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Wed Dec 01 20:09:57 2021 +0100 @@ -35,7 +35,7 @@ <item> <widget class="QTabWidget" name="optionsTabWidget"> <property name="currentIndex"> - <number>1</number> + <number>0</number> </property> <widget class="QWidget" name="globalOptionsTab"> <attribute name="title"> @@ -266,7 +266,7 @@ <property name="geometry"> <rect> <x>0</x> - <y>-512</y> + <y>0</y> <width>611</width> <height>1417</height> </rect> @@ -1253,6 +1253,48 @@ </item> </layout> </widget> + <widget class="QWidget" name="importOptionsTab"> + <attribute name="title"> + <string>Imports</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_15"> + <item> + <widget class="QGroupBox" name="groupBox_15"> + <property name="title"> + <string>Application Modules</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> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="appModulesEdit"/> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_7"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>737</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> </widget> </item> <item> @@ -1665,6 +1707,7 @@ <tabstop>ecHighRiskCombo</tabstop> <tabstop>ecMediumRiskCombo</tabstop> <tabstop>typedExceptionsCheckBox</tabstop> + <tabstop>appModulesEdit</tabstop> <tabstop>startButton</tabstop> <tabstop>loadDefaultButton</tabstop> <tabstop>storeDefaultButton</tabstop>
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Wed Dec 01 18:03:11 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Wed Dec 01 20:09:57 2021 +0100 @@ -8,6 +8,7 @@ """ import copy +import sys class ImportsChecker: @@ -15,6 +16,8 @@ Class implementing a checker for import statements. """ Codes = [ + ## Local imports + "I101", "I102", "I103", ] def __init__(self, source, filename, tree, select, ignore, expected, @@ -36,11 +39,11 @@ @type list of str @param repeat flag indicating to report each occurrence of a code @type bool - @param args dictionary of arguments for the miscellaneous checks + @param args dictionary of arguments for the various checks @type dict """ self.__select = tuple(select) - self.__ignore = ('',) if select else tuple(ignore) + self.__ignore = ("",) if select else tuple(ignore) self.__expected = expected[:] self.__repeat = repeat self.__filename = filename @@ -55,6 +58,7 @@ self.errors = [] checkersWithCodes = [ + (self.__checkLocalImports, ("I101", "I102", "I103")), ] self.__checkers = [] @@ -127,3 +131,73 @@ 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 + """ + try: + 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", + } + + ####################################################################### + ## 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: + node = violation[0] + reason = violation[1] + self.__error(node.lineno - 1, node.col_offset, reason)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py Wed Dec 01 20:09:57 2021 +0100 @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a node visitor for checking local import statements. +""" + +import ast + +# +# The visitor is adapted from flake8-local-import v1.0.6 +# + + +class LocalImportVisitor(ast.NodeVisitor): + """ + Class implementing a node visitor for checking local import statements. + """ + def __init__(self, args, checker): + """ + Constructor + + @param args dictionary containing the checker arguments + @type dict + @param checker reference to the checker + @type ImportsChecker + """ + self.__appImportNames = args.get("ApplicationModuleNames", []) + self.__checker = checker + + self.violations = [] + + def visit(self, node): + """ + Public method to traverse the tree of an AST node. + + @param node AST node to parse + @type ast.AST + """ + previous = None + isLocal = ( + isinstance(node, ast.FunctionDef) or + getattr(node, 'is_local', False) + ) + for child in ast.iter_child_nodes(node): + child.parent = node + child.previous = previous + child.is_local = isLocal + previous = child + + super().visit(node) + + def visit_FunctionDef(self, node): + """ + Public method to handle a function definition. + + @param node reference to the node to be processed + @type ast.FunctionDef + """ + children = list(ast.iter_child_nodes(node)) + if len(children) > 1: + firstStatement = children[1] + + if isinstance(firstStatement, ast.Expr): + value = getattr(firstStatement, 'value', None) + if isinstance(value, ast.Constant): + firstStatement.is_doc_str = True + + self.generic_visit(node) + + def visit_Import(self, node): + """ + Public method to handle an import statement. + + @param node reference to the node to be processed + @type ast.Import + """ + if not getattr(node, 'is_local', False): + self.generic_visit(node) + return + + for name in node.names: + self.__assertExternalModule(node, name.name or '') + + self.__visitImportNode(node) + + def visit_ImportFrom(self, node): + """ + Public method to handle an import from statement. + + @param node reference to the node to be processed + @type ast.ImportFrom + """ + if not getattr(node, 'is_local', False): + self.generic_visit(node) + return + + self.__assertExternalModule(node, node.module or '') + + self.__visitImportNode(node) + + def __visitImportNode(self, node): + """ + Private method to handle an import or import from statement. + + @param node reference to the node to be processed + @type ast.Import or ast.ImportFrom + """ + parent = getattr(node, 'parent', None) + if isinstance(parent, ast.Module): + self.generic_visit(node) + return + + previous = getattr(node, 'previous', None) + + isAllowedPrevious = ( + (isinstance(previous, ast.Expr) and + getattr(previous, 'is_doc_str', False)) or + isinstance(previous, (ast.Import, ast.ImportFrom, ast.arguments)) + ) + + if not isinstance(parent, ast.FunctionDef) or not isAllowedPrevious: + self.violations.append((node, "I101")) + + self.generic_visit(node) + + def __assertExternalModule(self, node, module): + """ + Private method to assert the given node. + + @param node reference to the node to be processed + @type ast.stmt + @param module name of the module + @type str + """ + parent = getattr(node, 'parent', None) + if isinstance(parent, ast.Module): + return + + modulePrefix = module + '.' + + if ( + getattr(node, 'level', 0) != 0 or + any(modulePrefix.startswith(appModule + '.') + for appModule in self.__appImportNames) + ): + return + + if module.split('.')[0] not in self.__checker.getStandardModules(): + self.violations.append((node, "I102")) + else: + self.violations.append((node, "I103"))
--- a/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py Wed Dec 01 18:03:11 2021 +0100 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py Wed Dec 01 20:09:57 2021 +0100 @@ -11,7 +11,15 @@ from PyQt6.QtCore import QCoreApplication _importsMessages = { - + "I101": QCoreApplication.translate( + "ImportsChecker", + "local import must be at the beginning of the method body"), + "I102": QCoreApplication.translate( + "ImportsChecker", + "packages from external modules should not be imported locally"), + "I103": QCoreApplication.translate( + "ImportsChecker", + "packages from standard modules should not be imported locally"), } _importsMessagesSampleArgs = {