--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Wed Jul 13 14:55:47 2022 +0200 @@ -22,34 +22,37 @@ """ Class implementing a checker for function type annotations. """ + Codes = [ ## Function Annotations - "A001", "A002", "A003", - + "A001", + "A002", + "A003", ## Method Annotations - "A101", "A102", - + "A101", + "A102", ## Return Annotations - "A201", "A202", "A203", "A204", "A205", "A206", - + "A201", + "A202", + "A203", + "A204", + "A205", + "A206", ## Mixed kind of annotations "A301", - ## Annotations Future "A871", - ## Annotation Coverage "A881", - ## Annotation Complexity - "A891", "A892", + "A891", + "A892", ] - 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 @@ -68,7 +71,7 @@ @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 @@ -78,28 +81,38 @@ # statistics counters self.counters = {} - + # collection of detected errors self.errors = [] - + checkersWithCodes = [ ( self.__checkFunctionAnnotations, - ("A001", "A002", "A003", "A101", "A102", - "A201", "A202", "A203", "A204", "A205", "A206", - "A301", ) + ( + "A001", + "A002", + "A003", + "A101", + "A102", + "A201", + "A202", + "A203", + "A204", + "A205", + "A206", + "A301", + ), ), (self.__checkAnnotationsFuture, ("A871",)), (self.__checkAnnotationsCoverage, ("A881",)), (self.__checkAnnotationComplexity, ("A891", "A892")), ] - + 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. @@ -109,13 +122,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 @@ -127,16 +139,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( @@ -148,7 +160,7 @@ "args": args, } ) - + def run(self): """ Public method to check the given source against annotation issues. @@ -156,97 +168,102 @@ 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() - + ####################################################################### ## Annotations ## ## adapted from: flake8-annotations v2.7.0 ####################################################################### - + def __checkFunctionAnnotations(self): """ Private method to check for function annotation issues. """ suppressNoneReturning = self.__args.get( "SuppressNoneReturning", - AnnotationsCheckerDefaultArgs["SuppressNoneReturning"]) + AnnotationsCheckerDefaultArgs["SuppressNoneReturning"], + ) suppressDummyArgs = self.__args.get( - "SuppressDummyArgs", - AnnotationsCheckerDefaultArgs["SuppressDummyArgs"]) + "SuppressDummyArgs", AnnotationsCheckerDefaultArgs["SuppressDummyArgs"] + ) allowUntypedDefs = self.__args.get( - "AllowUntypedDefs", - AnnotationsCheckerDefaultArgs["AllowUntypedDefs"]) + "AllowUntypedDefs", AnnotationsCheckerDefaultArgs["AllowUntypedDefs"] + ) allowUntypedNested = self.__args.get( - "AllowUntypedNested", - AnnotationsCheckerDefaultArgs["AllowUntypedNested"]) + "AllowUntypedNested", AnnotationsCheckerDefaultArgs["AllowUntypedNested"] + ) mypyInitReturn = self.__args.get( - "MypyInitReturn", - AnnotationsCheckerDefaultArgs["MypyInitReturn"]) - + "MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"] + ) + # Store decorator lists as sets for easier lookup - dispatchDecorators = set(self.__args.get( - "DispatchDecorators", - AnnotationsCheckerDefaultArgs["DispatchDecorators"])) - overloadDecorators = set(self.__args.get( - "OverloadDecorators", - AnnotationsCheckerDefaultArgs["OverloadDecorators"])) - + dispatchDecorators = set( + self.__args.get( + "DispatchDecorators", + AnnotationsCheckerDefaultArgs["DispatchDecorators"], + ) + ) + overloadDecorators = set( + self.__args.get( + "OverloadDecorators", + AnnotationsCheckerDefaultArgs["OverloadDecorators"], + ) + ) + from .AnnotationsFunctionVisitor import FunctionVisitor + visitor = FunctionVisitor(self.__source) visitor.visit(self.__tree) - + # Keep track of the last encountered function decorated by # `typing.overload`, if any. Per the `typing` module documentation, # a series of overload-decorated definitions must be followed by # exactly one non-overload-decorated definition of the same function. lastOverloadDecoratedFunctionName = None - + # Iterate over the arguments with missing type hints, by function. for function in visitor.functionDefinitions: - if ( - function.isDynamicallyTyped() and - (allowUntypedDefs or - (function.isNested and allowUntypedNested)) + if function.isDynamicallyTyped() and ( + allowUntypedDefs or (function.isNested and allowUntypedNested) ): # Skip recording errors from dynamically typed functions # or nested functions continue - + # Skip recording errors for configured dispatch functions, such as # (by default) `functools.singledispatch` and # `functools.singledispatchmethod` if function.hasDecorator(dispatchDecorators): continue - + # Create sentinels to check for mixed hint styles hasTypeComment = function.hasTypeComment - + has3107Annotation = False # PEP 3107 annotations are captured by the return arg - + # Iterate over annotated args to detect mixing of type annotations # and type comments. Emit this only once per function definition for arg in function.getAnnotatedArguments(): if arg.hasTypeComment: hasTypeComment = True - + if arg.has3107Annotation: has3107Annotation = True - + if hasTypeComment and has3107Annotation: # Short-circuit check for mixing of type comments & # 3107-style annotations - self.__error(function.lineno - 1, function.col_offset, - "A301") + self.__error(function.lineno - 1, function.col_offset, "A301") break - + # Before we iterate over the function's missing annotations, check # to see if it's the closing function def in a series of # `typing.overload` decorated functions. @@ -257,50 +274,50 @@ # iteration if function.hasDecorator(overloadDecorators): lastOverloadDecoratedFunctionName = function.name - + # Record explicit errors for arguments that are missing annotations for arg in function.getMissedAnnotations(): if arg.argname == "return": # return annotations have multiple possible short-circuit # paths if ( - suppressNoneReturning and - not arg.hasTypeAnnotation and - function.hasOnlyNoneReturns + suppressNoneReturning + and not arg.hasTypeAnnotation + and function.hasOnlyNoneReturns ): # Skip recording return errors if the function has only # `None` returns. This includes the case of no returns. continue - + if ( - mypyInitReturn and - function.isClassMethod and - function.name == "__init__" and - function.getAnnotatedArguments() + mypyInitReturn + and function.isClassMethod + and function.name == "__init__" + and function.getAnnotatedArguments() ): # Skip recording return errors for `__init__` if at # least one argument is annotated continue - + # If the `suppressDummyArgs` flag is `True`, skip recording # errors for any arguments named `_` if arg.argname == "_" and suppressDummyArgs: continue self.__classifyError(function, arg) - + def __classifyError(self, function, arg): """ Private method to classify the missing type annotation based on the Function & Argument metadata. - + For the currently defined rules & program flow, the assumption can be made that an argument passed to this method will match a linting error, and will only match a single linting error - + This function provides an initial classificaton, then passes relevant attributes to cached helper function(s). - + @param function reference to the Function object @type Function @param arg reference to the Argument object @@ -310,29 +327,30 @@ # All return "arguments" have an explicitly defined name "return" if arg.argname == "return": errorCode = self.__returnErrorClassifier( - function.isClassMethod, function.classDecoratorType, - function.functionType + function.isClassMethod, + function.classDecoratorType, + function.functionType, ) else: # Otherwise, classify function argument error isFirstArg = arg == function.args[0] errorCode = self.__argumentErrorClassifier( - function.isClassMethod, isFirstArg, - function.classDecoratorType, arg.annotationType, + function.isClassMethod, + isFirstArg, + function.classDecoratorType, + arg.annotationType, ) - + if errorCode in ("A001", "A002", "A003"): - self.__error(arg.lineno - 1, arg.col_offset, errorCode, - arg.argname) + self.__error(arg.lineno - 1, arg.col_offset, errorCode, arg.argname) else: self.__error(arg.lineno - 1, arg.col_offset, errorCode) - + @lru_cache() - def __returnErrorClassifier(self, isClassMethod, classDecoratorType, - functionType): + def __returnErrorClassifier(self, isClassMethod, classDecoratorType, functionType): """ Private method to classify a return type annotation issue. - + @param isClassMethod flag indicating a classmethod type function @type bool @param classDecoratorType type of class decorator @@ -358,13 +376,14 @@ return "A202" else: return "A201" - + @lru_cache() - def __argumentErrorClassifier(self, isClassMethod, isFirstArg, - classDecoratorType, annotationType): + def __argumentErrorClassifier( + self, isClassMethod, isFirstArg, classDecoratorType, annotationType + ): """ Private method to classify an argument type annotation issue. - + @param isClassMethod flag indicating a classmethod type function @type bool @param isFirstArg flag indicating the first argument @@ -395,106 +414,134 @@ else: # Combine PosOnlyArgs, Args, and KwOnlyArgs return "A001" - + ####################################################################### ## Annotations Coverage ## ## adapted from: flake8-annotations-coverage v0.0.5 ####################################################################### - + def __checkAnnotationsCoverage(self): """ Private method to check for function annotation coverage. """ minAnnotationsCoverage = self.__args.get( - "MinimumCoverage", - AnnotationsCheckerDefaultArgs["MinimumCoverage"]) + "MinimumCoverage", AnnotationsCheckerDefaultArgs["MinimumCoverage"] + ) if minAnnotationsCoverage == 0: # 0 means it is switched off return - + functionDefs = [ - f for f in ast.walk(self.__tree) + f + for f in ast.walk(self.__tree) if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) ] if not functionDefs: # no functions/methods at all return - + functionDefAnnotationsInfo = [ self.__hasTypeAnnotations(f) for f in functionDefs ] annotationsCoverage = int( - len(list(filter(None, functionDefAnnotationsInfo))) / - len(functionDefAnnotationsInfo) * 100 + len(list(filter(None, functionDefAnnotationsInfo))) + / len(functionDefAnnotationsInfo) + * 100 ) if annotationsCoverage < minAnnotationsCoverage: self.__error(0, 0, "A881", annotationsCoverage) - + def __hasTypeAnnotations(self, funcNode): """ Private method to check for type annotations. - + @param funcNode reference to the function definition node to be checked @type ast.AsyncFunctionDef or ast.FunctionDef @return flag indicating the presence of type annotations @rtype bool """ hasReturnAnnotation = funcNode.returns is not None - hasArgsAnnotations = any(a for a in funcNode.args.args - if a.annotation is not None) - hasKwargsAnnotations = (funcNode.args and - funcNode.args.kwarg and - funcNode.args.kwarg.annotation is not None) - hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs - if a.annotation is not None) - - return any((hasReturnAnnotation, hasArgsAnnotations, - hasKwargsAnnotations, hasKwonlyargsAnnotations)) - + hasArgsAnnotations = any( + a for a in funcNode.args.args if a.annotation is not None + ) + hasKwargsAnnotations = ( + funcNode.args + and funcNode.args.kwarg + and funcNode.args.kwarg.annotation is not None + ) + hasKwonlyargsAnnotations = any( + a for a in funcNode.args.kwonlyargs if a.annotation is not None + ) + + return any( + ( + hasReturnAnnotation, + hasArgsAnnotations, + hasKwargsAnnotations, + hasKwonlyargsAnnotations, + ) + ) + ####################################################################### ## Annotations Complexity ## ## adapted from: flake8-annotations-complexity v0.0.6 ####################################################################### - + def __checkAnnotationComplexity(self): """ Private method to check the type annotation complexity. """ maxAnnotationComplexity = self.__args.get( - "MaximumComplexity", - AnnotationsCheckerDefaultArgs["MaximumComplexity"]) + "MaximumComplexity", AnnotationsCheckerDefaultArgs["MaximumComplexity"] + ) maxAnnotationLength = self.__args.get( - "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"]) + "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"] + ) typeAnnotations = [] - + functionDefs = [ - f for f in ast.walk(self.__tree) + f + for f in ast.walk(self.__tree) if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) ] for functionDef in functionDefs: - typeAnnotations += list(filter( - None, [a.annotation for a in functionDef.args.args])) + typeAnnotations += list( + filter(None, [a.annotation for a in functionDef.args.args]) + ) if functionDef.returns: typeAnnotations.append(functionDef.returns) - typeAnnotations += [a.annotation for a in ast.walk(self.__tree) - if isinstance(a, ast.AnnAssign) and a.annotation] + typeAnnotations += [ + a.annotation + for a in ast.walk(self.__tree) + if isinstance(a, ast.AnnAssign) and a.annotation + ] for annotation in typeAnnotations: complexity = self.__getAnnotationComplexity(annotation) if complexity > maxAnnotationComplexity: - self.__error(annotation.lineno - 1, annotation.col_offset, - "A891", complexity, maxAnnotationComplexity) - + self.__error( + annotation.lineno - 1, + annotation.col_offset, + "A891", + complexity, + maxAnnotationComplexity, + ) + annotationLength = self.__getAnnotationLength(annotation) if annotationLength > maxAnnotationLength: - self.__error(annotation.lineno - 1, annotation.col_offset, - "A892", annotationLength, maxAnnotationLength) - + self.__error( + annotation.lineno - 1, + annotation.col_offset, + "A892", + annotationLength, + maxAnnotationLength, + ) + def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1): """ Private method to determine the annotation complexity. - + @param annotationNode reference to the node to determine the annotation complexity for @type ast.AST @@ -510,25 +557,24 @@ return defaultComplexity if isinstance(annotationNode, ast.Subscript): if sys.version_info >= (3, 9): - return (defaultComplexity + - self.__getAnnotationComplexity(annotationNode.slice)) + return defaultComplexity + self.__getAnnotationComplexity( + annotationNode.slice + ) else: - return ( - defaultComplexity + - self.__getAnnotationComplexity(annotationNode.slice.value) + return defaultComplexity + self.__getAnnotationComplexity( + annotationNode.slice.value ) if isinstance(annotationNode, ast.Tuple): return max( - (self.__getAnnotationComplexity(n) - for n in annotationNode.elts), - default=defaultComplexity + (self.__getAnnotationComplexity(n) for n in annotationNode.elts), + default=defaultComplexity, ) return defaultComplexity - + def __getAnnotationLength(self, annotationNode): """ Private method to determine the annotation length. - + @param annotationNode reference to the node to determine the annotation length for @type ast.AST @@ -549,26 +595,24 @@ except AttributeError: return 0 return 0 - + ####################################################################### ## 'from __future__ import annotations' checck ## ## adapted from: flake8-future-annotations v0.0.4 ####################################################################### - + def __checkAnnotationsFuture(self): """ Private method to check the use of __future__ and typing imports. """ from .AnnotationsFutureVisitor import AnnotationsFutureVisitor + visitor = AnnotationsFutureVisitor() visitor.visit(self.__tree) - - if ( - visitor.importsFutureAnnotations() or - not visitor.hasTypingImports() - ): + + if visitor.importsFutureAnnotations() or not visitor.hasTypingImports(): return - + imports = ", ".join(visitor.getTypingImports()) self.__error(0, 0, "A871", imports)