diff -r cc9ead6d1c46 -r df836ff707fd src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py --- 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): """