src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py

branch
eric7
changeset 10048
df836ff707fd
parent 9653
e67609152c5e
child 10085
b5808c3a9967
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Sun May 21 16:04:59 2023 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Mon May 22 08:43:23 2023 +0200
@@ -128,6 +128,12 @@
         "M525",
         "M526",
         "M527",
+        "M528",
+        "M529",
+        "M530",
+        "M531",
+        "M532",
+        "M533",
         ## Bugbear++
         "M581",
         "M582",
@@ -313,6 +319,12 @@
                     "M525",
                     "M526",
                     "M527",
+                    "M528",
+                    "M529",
+                    "M530",
+                    "M531",
+                    "M532",
+                    "M533",
                     "M581",
                     "M582",
                 ),
@@ -1532,7 +1544,7 @@
     """
 
     #
-    # This class was implemented along the BugBear flake8 extension (v 22.12.6).
+    # This class was implemented along flake8-bugbear (v 22.12.6).
     # Original: Copyright (c) 2016 Ɓukasz Langa
     #
 
@@ -1567,6 +1579,7 @@
         self.contexts = []
 
         self.__M523Seen = set()
+        self.__M505Imports = set()
 
     @property
     def nodeStack(self):
@@ -1707,6 +1720,101 @@
             for child in ast.iter_child_nodes(node):
                 yield from self.__childrenInScope(child)
 
+    def __flattenExcepthandler(self, node):
+        """
+        Private method to flatten the list of exceptions handled by an except handler.
+
+        @param node reference to the node to be processed
+        @type ast.Node
+        @yield reference to the exception type node
+        @ytype ast.Node
+        """
+        if not isinstance(node, ast.Tuple):
+            yield node
+            return
+
+        exprList = node.elts.copy()
+        while len(exprList):
+            expr = exprList.pop(0)
+            if isinstance(expr, ast.Starred) and isinstance(
+                expr.value, (ast.List, ast.Tuple)
+            ):
+                exprList.extend(expr.value.elts)
+                continue
+            yield expr
+
+    def __checkRedundantExcepthandlers(self, names, node):
+        """
+        Private method to check for redundant exception types in an exception handler.
+
+        @param names list of exception types to be checked
+        @type list of ast.Name
+        @param node reference to the exception handler node
+        @type ast.ExceptionHandler
+        @return tuple containing the error data
+        @rtype tuple of (ast.Node, str, str, str, str)
+        """
+        redundantExceptions = {
+            "OSError": {
+                # All of these are actually aliases of OSError since Python 3.3
+                "IOError",
+                "EnvironmentError",
+                "WindowsError",
+                "mmap.error",
+                "socket.error",
+                "select.error",
+            },
+            "ValueError": {
+                "binascii.Error",
+            },
+        }
+
+        # See if any of the given exception names could be removed, e.g. from:
+        #    (MyError, MyError)  # duplicate names
+        #    (MyError, BaseException)  # everything derives from the Base
+        #    (Exception, TypeError)  # builtins where one subclasses another
+        #    (IOError, OSError)  # IOError is an alias of OSError since Python3.3
+        # but note that other cases are impractical to handle from the AST.
+        # We expect this is mostly useful for users who do not have the
+        # builtin exception hierarchy memorised, and include a 'shadowed'
+        # subtype without realising that it's redundant.
+        good = sorted(set(names), key=names.index)
+        if "BaseException" in good:
+            good = ["BaseException"]
+        # Remove redundant exceptions that the automatic system either handles
+        # poorly (usually aliases) or can't be checked (e.g. it's not an
+        # built-in exception).
+        for primary, equivalents in redundantExceptions.items():
+            if primary in good:
+                good = [g for g in good if g not in equivalents]
+
+        for name, other in itertools.permutations(tuple(good), 2):
+            if (
+                self.__typesafeIssubclass(
+                    getattr(builtins, name, type), getattr(builtins, other, ())
+                )
+                and name in good
+            ):
+                good.remove(name)
+        if good != names:
+            desc = good[0] if len(good) == 1 else "({0})".format(", ".join(good))
+            as_ = " as " + node.name if node.name is not None else ""
+            return (node, "M514", ", ".join(names), as_, desc)
+
+        return None
+
+    def __walkList(self, nodes):
+        """
+        Private method to walk a given list of nodes.
+
+        @param nodes list of nodes to walk
+        @type list of ast.Node
+        @yield node references as determined by the ast.walk() function
+        @ytype ast.Node
+        """
+        for node in nodes:
+            yield from ast.walk(node)
+
     def visit(self, node):
         """
         Public method to traverse a given AST node.
@@ -1738,65 +1846,41 @@
         @param node reference to the node to be processed
         @type ast.ExceptHandler
         """
-        redundantExceptions = {
-            "OSError": {
-                # All of these are actually aliases of OSError since Python 3.3
-                "IOError",
-                "EnvironmentError",
-                "WindowsError",
-                "mmap.error",
-                "socket.error",
-                "select.error",
-            },
-            "ValueError": {
-                "binascii.Error",
-            },
-        }
-
         if node.type is None:
             # bare except is handled by pycodestyle already
             pass
 
-        elif isinstance(node.type, ast.Tuple):
-            names = [self.__toNameStr(e) for e in node.type.elts]
-            as_ = " as " + node.name if node.name is not None else ""
-            if len(names) == 0:
-                self.violations.append((node, "M501", as_))
-            elif len(names) == 1:
+        else:
+            handlers = self.__flattenExcepthandler(node.type)
+            names = []
+            badHandlers = []
+            ignoredHandlers = []
+            for handler in handlers:
+                if isinstance(handler, (ast.Name, ast.Attribute)):
+                    name = self.__toNameStr(handler)
+                    if name is None:
+                        ignoredHandlers.append(handler)
+                    else:
+                        names.append(name)
+                elif isinstance(handler, (ast.Call, ast.Starred)):
+                    ignoredHandlers.append(handler)
+                else:
+                    badHandlers.append(handler)
+            if badHandlers:
+                self.violations.append((node, "M530"))
+            if len(names) == 0 and not badHandlers and not ignoredHandlers:
+                self.violations.append((node, "M529"))
+            elif (
+                len(names) == 1
+                and not badHandlers
+                and not ignoredHandlers
+                and isinstance(node.type, ast.Tuple)
+            ):
                 self.violations.append((node, "M513", *names))
             else:
-                # See if any of the given exception names could be removed, e.g. from:
-                #  (MyError, MyError)  # duplicate names
-                #  (MyError, BaseException)  # everything derives from the Base
-                #  (Exception, TypeError)  # builtins where one subclasses another
-                #  (IOError, OSError)  # IOError is an alias of OSError since Python3.3
-                # but note that other cases are impractical to handle from the AST.
-                # We expect this is mostly useful for users who do not have the
-                # builtin exception hierarchy memorised, and include a 'shadowed'
-                # subtype without realising that it's redundant.
-                good = sorted(set(names), key=names.index)
-                if "BaseException" in good:
-                    good = ["BaseException"]
-                # Remove redundant exceptions that the automatic system either handles
-                # poorly (usually aliases) or can't be checked (e.g. it's not an
-                # built-in exception).
-                for primary, equivalents in redundantExceptions.items():
-                    if primary in good:
-                        good = [g for g in good if g not in equivalents]
-
-                for name, other in itertools.permutations(tuple(good), 2):
-                    if (
-                        self.__typesafeIssubclass(
-                            getattr(builtins, name, type), getattr(builtins, other, ())
-                        )
-                        and name in good
-                    ):
-                        good.remove(name)
-                if good != names:
-                    desc = (
-                        good[0] if len(good) == 1 else "({0})".format(", ".join(good))
-                    )
-                    self.violations.append((node, "M514", ", ".join(names), as_, desc))
+                maybeError = self.__checkRedundantExcepthandlers(names, node)
+                if maybeError is not None:
+                    self.violations.append(maybeError)
 
         self.generic_visit(node)
 
@@ -1860,6 +1944,19 @@
 
             self.__checkForM526(node)
 
+        self.__checkForM528(node)
+
+        self.generic_visit(node)
+
+    def visit_Module(self, node):
+        """
+        Public method to handle a module node.
+
+        @param node reference to the node to be processed
+        @type ast.Module
+        """
+        self.__checkForM518(node)
+
         self.generic_visit(node)
 
     def visit_Assign(self, node):
@@ -1890,6 +1987,7 @@
         self.__checkForM507(node)
         self.__checkForM520(node)
         self.__checkForM523(node)
+        self.__checkForM531(node)
 
         self.generic_visit(node)
 
@@ -1903,6 +2001,7 @@
         self.__checkForM507(node)
         self.__checkForM520(node)
         self.__checkForM523(node)
+        self.__checkForM531(node)
 
         self.generic_visit(node)
 
@@ -2061,6 +2160,39 @@
 
         self.violations.append((node, "M581"))
 
+    def visit_AnnAssign(self, node):
+        """
+        Public method to check annotated assign statements.
+
+        @param node reference to the node to be processed
+        @type ast.AnnAssign
+        """
+        self.__checkForM532(node)
+
+        self.generic_visit(node)
+
+    def visit_Import(self, node):
+        """
+        Public method to check imports.
+
+        @param node reference to the node to be processed
+        @type ast.Import
+        """
+        self.__checkForM505(node)
+
+        self.generic_visit(node)
+
+    def visit_Set(self, node):
+        """
+        Public method to check a set.
+
+        @param node reference to the node to be processed
+        @type ast.Set
+        """
+        self.__checkForM533(node)
+
+        self.generic_visit(node)
+
     def __checkForM505(self, node):
         """
         Private method to check the use of *strip().
@@ -2068,20 +2200,30 @@
         @param node reference to the node to be processed
         @type ast.Call
         """
-        if node.func.attr not in ("lstrip", "rstrip", "strip"):
-            return  # method name doesn't match
-
-        if len(node.args) != 1 or not AstUtilities.isString(node.args[0]):
-            return  # used arguments don't match the builtin strip
-
-        s = AstUtilities.getValue(node.args[0])
-        if len(s) == 1:
-            return  # stripping just one character
-
-        if len(s) == len(set(s)):
-            return  # no characters appear more than once
-
-        self.violations.append((node, "M505"))
+        if isinstance(node, ast.Import):
+            for name in node.names:
+                self.__M505Imports.add(name.asname or name.name)
+        elif isinstance(node, ast.Call):
+            if node.func.attr not in ("lstrip", "rstrip", "strip"):
+                return  # method name doesn't match
+
+            if (
+                isinstance(node.func.value, ast.Name)
+                and node.func.value.id in self.__M505Imports
+            ):
+                return  # method is being run on an imported module
+
+            if len(node.args) != 1 or not AstUtilities.isString(node.args[0]):
+                return  # used arguments don't match the builtin strip
+
+            s = AstUtilities.getValue(node.args[0])
+            if len(s) == 1:
+                return  # stripping just one character
+
+            if len(s) == len(set(s)):
+                return  # no characters appear more than once
+
+            self.violations.append((node, "M505"))
 
     def __checkForM507(self, node):
         """
@@ -2152,7 +2294,7 @@
     def __checkForM517(self, node):
         """
         Private method to check for use of the evil syntax
-        'with assertRaises(Exception):.
+        'with assertRaises(Exception): or 'with pytest.raises(Exception):'.
 
         @param node reference to the node to be processed
         @type ast.With
@@ -2161,8 +2303,16 @@
         itemContext = item.context_expr
         if (
             hasattr(itemContext, "func")
-            and hasattr(itemContext.func, "attr")
-            and itemContext.func.attr == "assertRaises"
+            and isinstance(itemContext.func, ast.Attribute)
+            and (
+                itemContext.func.attr == "assertRaises"
+                or (
+                    itemContext.func.attr == "raises"
+                    and isinstance(itemContext.func.value, ast.Name)
+                    and itemContext.func.value.id == "pytest"
+                    and "match" not in [kwd.arg for kwd in itemContext.keywords]
+                )
+            )
             and len(itemContext.args) == 1
             and isinstance(itemContext.args[0], ast.Name)
             and itemContext.args[0].id == "Exception"
@@ -2177,29 +2327,34 @@
         @param node reference to the node to be processed
         @type ast.FunctionDef
         """
-        subnodeClasses = (
-            (
-                ast.Constant,
-                ast.List,
-                ast.Set,
-                ast.Dict,
-            )
-            if sys.version_info >= (3, 8, 0)
-            else (
-                ast.Num,
-                ast.Bytes,
-                ast.NameConstant,
-                ast.List,
-                ast.Set,
-                ast.Dict,
-            )
-        )
         for subnode in node.body:
             if not isinstance(subnode, ast.Expr):
                 continue
 
-            if isinstance(subnode.value, subnodeClasses) and not AstUtilities.isString(
-                subnode.value
+            if (
+                sys.version_info < (3, 8, 0)
+                and isinstance(
+                    subnode.value,
+                    (ast.Num, ast.Bytes, ast.NameConstant, ast.List, ast.Set, ast.Dict),
+                )
+            ) or (
+                sys.version_info >= (3, 8, 0)
+                and (
+                    isinstance(
+                        subnode.value,
+                        (ast.List, ast.Set, ast.Dict),
+                    )
+                    or (
+                        isinstance(subnode.value, ast.Constant)
+                        and (
+                            isinstance(
+                                subnode.value.value,
+                                (int, float, complex, bytes, bool),
+                            )
+                            or subnode.value.value is None
+                        )
+                    )
+                )
             ):
                 self.violations.append((subnode, "M518"))
 
@@ -2421,6 +2576,7 @@
         if not any(map(isAbcClass, (*node.bases, *node.keywords))):
             return
 
+        hasMethod = False
         hasAbstractMethod = False
 
         if not any(map(isAbcClass, (*node.bases, *node.keywords))):
@@ -2435,6 +2591,7 @@
             # only check function defs
             if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
                 continue
+            hasMethod = True
 
             hasAbstractDecorator = any(map(isAbstractDecorator, stmt.decorator_list))
 
@@ -2447,7 +2604,7 @@
             ):
                 self.violations.append((stmt, "M527", stmt.name))
 
-        if not hasAbstractMethod:
+        if hasMethod and not hasAbstractMethod:
             self.violations.append((node, "M524", node.name))
 
     def __checkForM525(self, node):
@@ -2498,6 +2655,104 @@
             ):
                 self.violations.append((node, "M526"))
 
+    def __checkForM528(self, node):
+        """
+        Private method to check for warn without stacklevel.
+
+        @param node reference to the node to be processed
+        @type ast.Call
+        """
+        if (
+            isinstance(node.func, ast.Attribute)
+            and node.func.attr == "warn"
+            and isinstance(node.func.value, ast.Name)
+            and node.func.value.id == "warnings"
+            and not any(kw.arg == "stacklevel" for kw in node.keywords)
+            and len(node.args) < 3
+        ):
+            self.violations.append((node, "M528"))
+
+    def __checkForM531(self, loopNode):
+        """
+        Private method to check that 'itertools.groupby' isn't iterated over more than
+        once.
+
+        A warning is emitted when the generator returned by 'groupby()' is used
+        more than once inside a loop body or when it's used in a nested loop.
+
+        @param loopNode reference to the node to be processed
+        @type ast.For or ast.AsyncFor
+        """
+        # for <loop_node.target> in <loop_node.iter>: ...
+        if isinstance(loopNode.iter, ast.Call):
+            node = loopNode.iter
+            if (isinstance(node.func, ast.Name) and node.func.id in ("groupby",)) or (
+                isinstance(node.func, ast.Attribute)
+                and node.func.attr == "groupby"
+                and isinstance(node.func.value, ast.Name)
+                and node.func.value.id == "itertools"
+            ):
+                # We have an invocation of groupby which is a simple unpacking
+                if isinstance(loopNode.target, ast.Tuple) and isinstance(
+                    loopNode.target.elts[1], ast.Name
+                ):
+                    groupName = loopNode.target.elts[1].id
+                else:
+                    # Ignore any 'groupby()' invocation that isn't unpacked
+                    return
+
+                numUsages = 0
+                for node in self.__walkList(loopNode.body):
+                    # Handled nested loops
+                    if isinstance(node, ast.For):
+                        for nestedNode in self.__walkList(node.body):
+                            if (
+                                isinstance(nestedNode, ast.Name)
+                                and nestedNode.id == groupName
+                            ):
+                                self.violations.append((nestedNode, "M531"))
+
+                    # Handle multiple uses
+                    if isinstance(node, ast.Name) and node.id == groupName:
+                        numUsages += 1
+                        if numUsages > 1:
+                            self.violations.append((nestedNode, "M531"))
+
+    def __checkForM532(self, node):
+        """
+        Private method to check for possible unintentional typing annotation.
+
+        @param node reference to the node to be processed
+        @type ast.AnnAssign
+        """
+        if (
+            node.value is None
+            and hasattr(node.target, "value")
+            and isinstance(node.target.value, ast.Name)
+            and (
+                isinstance(node.target, ast.Subscript)
+                or (
+                    isinstance(node.target, ast.Attribute)
+                    and node.target.value.id != "self"
+                )
+            )
+        ):
+            self.violations.append((node, "M532"))
+
+    def __checkForM533(self, node):
+        """
+        Private method to check a set for duplicate items.
+
+        @param node reference to the node to be processed
+        @type ast.Set
+        """
+        constants = [
+            item.value
+            for item in filter(lambda x: isinstance(x, ast.Constant), node.elts)
+        ]
+        if len(constants) != len(set(constants)):
+            self.violations.append((node, "M533"))
+
 
 class NameFinder(ast.NodeVisitor):
     """

eric ide

mercurial