Continued implementing a checker for import statements. eric7

Wed, 01 Dec 2021 20:09:57 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 01 Dec 2021 20:09:57 +0100
branch
eric7
changeset 8801
8fbb21be8579
parent 8800
d0d2fa9dbbb7
child 8802
129a973fc33e

Continued implementing a checker for import statements.

eric7.epj file | annotate | diff | comparison | revisions
eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py file | annotate | diff | comparison | revisions
eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py file | annotate | diff | comparison | revisions
eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui file | annotate | diff | comparison | revisions
eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py file | annotate | diff | comparison | revisions
eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py file | annotate | diff | comparison | revisions
eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py file | annotate | diff | comparison | revisions
--- 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 = {

eric ide

mercurial