--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Thu Jul 28 14:19:57 2022 +0200 @@ -7,8 +7,9 @@ Module implementing a checker for function type annotations. """ +import ast +import contextlib import copy -import ast import sys from functools import lru_cache @@ -40,8 +41,11 @@ "A206", ## Mixed kind of annotations "A301", + ## Dynamically typed annotations + "A401", ## Annotations Future "A871", + "A872", ## Annotation Coverage "A881", ## Annotation Complexity @@ -101,9 +105,10 @@ "A205", "A206", "A301", + "A401", ), ), - (self.__checkAnnotationsFuture, ("A871",)), + (self.__checkAnnotationsFuture, ("A871", "A872")), (self.__checkAnnotationsCoverage, ("A881",)), (self.__checkAnnotationComplexity, ("A891", "A892")), ] @@ -179,9 +184,8 @@ ####################################################################### ## Annotations ## - ## adapted from: flake8-annotations v2.7.0 + ## adapted from: flake8-annotations v2.9.0 ####################################################################### - # TODO: update to v2.9.0 def __checkFunctionAnnotations(self): """ @@ -203,6 +207,9 @@ mypyInitReturn = self.__args.get( "MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"] ) + allowStarArgAny = self.__args.get( + "AllowStarArgAny", AnnotationsCheckerDefaultArgs["AllowStarArgAny"] + ) # Store decorator lists as sets for easier lookup dispatchDecorators = set( @@ -250,9 +257,11 @@ has3107Annotation = False # PEP 3107 annotations are captured by the return arg + annotatedArgs = function.getAnnotatedArguments() + # 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(): + for arg in annotatedArgs: if arg.hasTypeComment: hasTypeComment = True @@ -265,6 +274,19 @@ self.__error(function.lineno - 1, function.col_offset, "A301") break + # Iterate over the annotated args to look for 'typing.Any' annotations + # We could combine this with the above loop but I'd rather not add even + # more sentinels unless we'd notice a significant enough performance impact + for arg in annotatedArgs: + if arg.isDynamicallyTyped: + if allowStarArgAny and arg.annotationType in { + AnnotationType.VARARG, + AnnotationType.KWARG, + }: + continue + + self.__error(function.lineno - 1, function.col_offset, "A401") + # 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. @@ -294,7 +316,7 @@ mypyInitReturn and function.isClassMethod and function.name == "__init__" - and function.getAnnotatedArguments() + and annotatedArgs ): # Skip recording return errors for `__init__` if at # least one argument is annotated @@ -419,9 +441,8 @@ ####################################################################### ## Annotations Coverage ## - ## adapted from: flake8-annotations-coverage v0.0.5 + ## adapted from: flake8-annotations-coverage v0.0.6 ####################################################################### - # TODO: update to v0.0.6 def __checkAnnotationsCoverage(self): """ @@ -446,6 +467,9 @@ functionDefAnnotationsInfo = [ self.__hasTypeAnnotations(f) for f in functionDefs ] + if not bool(functionDefAnnotationsInfo): + return + annotationsCoverage = int( len(list(filter(None, functionDefAnnotationsInfo))) / len(functionDefAnnotationsInfo) @@ -488,9 +512,8 @@ ####################################################################### ## Annotations Complexity ## - ## adapted from: flake8-annotations-complexity v0.0.6 + ## adapted from: flake8-annotations-complexity v0.0.7 ####################################################################### - # TODO: update to v0.0.7 def __checkAnnotationComplexity(self): """ @@ -511,7 +534,7 @@ ] for functionDef in functionDefs: typeAnnotations += list( - filter(None, [a.annotation for a in functionDef.args.args]) + filter(None, (a.annotation for a in functionDef.args.args)) ) if functionDef.returns: typeAnnotations.append(functionDef.returns) @@ -558,21 +581,25 @@ annotationNode = ast.parse(annotationNode.s).body[0].value except (SyntaxError, IndexError): return defaultComplexity + + complexity = defaultComplexity if isinstance(annotationNode, ast.Subscript): if sys.version_info >= (3, 9): - return defaultComplexity + self.__getAnnotationComplexity( + complexity = defaultComplexity + self.__getAnnotationComplexity( annotationNode.slice ) else: - return defaultComplexity + self.__getAnnotationComplexity( + complexity = defaultComplexity + self.__getAnnotationComplexity( annotationNode.slice.value ) + if isinstance(annotationNode, ast.Tuple): - return max( + complexity = max( (self.__getAnnotationComplexity(n) for n in annotationNode.elts), default=defaultComplexity, ) - return defaultComplexity + + return complexity def __getAnnotationLength(self, annotationNode): """ @@ -584,27 +611,28 @@ @return annotation length @rtype = int """ + annotationLength = 0 if AstUtilities.isString(annotationNode): try: annotationNode = ast.parse(annotationNode.s).body[0].value except (SyntaxError, IndexError): - return 0 + return annotationLength + if isinstance(annotationNode, ast.Subscript): - try: - if sys.version_info >= (3, 9): - return len(annotationNode.slice.elts) - else: - return len(annotationNode.slice.value.elts) - except AttributeError: - return 0 - return 0 + with contextlib.suppress(AttributeError): + annotationLength = ( + len(annotationNode.slice.elts) + if sys.version_info >= (3, 9) + else len(annotationNode.slice.value.elts) + ) + + return annotationLength ####################################################################### ## 'from __future__ import annotations' checck ## - ## adapted from: flake8-future-annotations v0.0.4 + ## adapted from: flake8-future-annotations v0.0.5 ####################################################################### - # TODO: update to v0.0.5 def __checkAnnotationsFuture(self): """ @@ -612,11 +640,19 @@ """ from .AnnotationsFutureVisitor import AnnotationsFutureVisitor + forceFutureAnnotations = self.__args.get( + "ForceFutureAnnotations", + AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"], + ) + visitor = AnnotationsFutureVisitor() visitor.visit(self.__tree) - if visitor.importsFutureAnnotations() or not visitor.hasTypingImports(): + if visitor.importsFutureAnnotations(): return - imports = ", ".join(visitor.getTypingImports()) - self.__error(0, 0, "A871", imports) + if visitor.hasTypingImports(): + imports = ", ".join(visitor.getTypingImports()) + self.__error(0, 0, "A871", imports) + elif forceFutureAnnotations: + self.__error(0, 0, "A872")