Thu, 28 Jul 2022 14:19:57 +0200
Code Style Checker
- updated the annotations checker to support more cases
--- a/docs/changelog Wed Jul 27 18:02:43 2022 +0200 +++ b/docs/changelog Thu Jul 28 14:19:57 2022 +0200 @@ -5,6 +5,7 @@ - Code Style Checker -- extended the Naming style checker to be more PEP8 compliant -- updated imports checker to support banned module patterns + -- updated the annotations checker to support more cases - MicroPython -- added capability to connect to devices for which only the serial port name is available
--- 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")
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsCheckerDefaults.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsCheckerDefaults.py Thu Jul 28 14:19:57 2022 +0200 @@ -14,6 +14,7 @@ "AllowUntypedDefs": False, "AllowUntypedNested": False, "MypyInitReturn": False, + "AllowStarArgAny": False, "DispatchDecorators": ["singledispatch", "singledispatchmethod"], "OverloadDecorators": ["overload"], # Annotation Coverage @@ -21,4 +22,6 @@ # Annotation Complexity "MaximumComplexity": 3, "MaximumLength": 7, + # Annotations Future + "ForceFutureAnnotations": False, }
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py Thu Jul 28 14:19:57 2022 +0200 @@ -8,7 +8,7 @@ """ # -# adapted from flake8-annotations v2.7.0 +# adapted from flake8-annotations v2.9.0 # import enum
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py Thu Jul 28 14:19:57 2022 +0200 @@ -8,7 +8,7 @@ """ # -# The visitor and associated classes are adapted from flake8-annotations v2.7.0 +# The visitor and associated classes are adapted from flake8-annotations v2.9.0 # import ast @@ -35,6 +35,7 @@ hasTypeAnnotation=False, has3107Annotation=False, hasTypeComment=False, + isDynamicallyTyped=False, ): """ Constructor @@ -56,6 +57,8 @@ @param hasTypeComment flag indicating the presence of a type comment (defaults to False) @type bool (optional) + @param isDynamicallyTyped flag indicating dynamic typing (defaults to False) + @type bool (optional) """ self.argname = argname self.lineno = lineno @@ -64,6 +67,7 @@ self.hasTypeAnnotation = hasTypeAnnotation self.has3107Annotation = has3107Annotation self.hasTypeComment = hasTypeComment + self.isDynamicallyTyped = isDynamicallyTyped @classmethod def fromNode(cls, node, annotationTypeName): @@ -89,8 +93,43 @@ newArg.hasTypeAnnotation = True newArg.hasTypeComment = True + if cls._isAnnotatedAny(node.type_comment): + newArg.isDynamicallyTyped = True + return newArg + @staticmethod + def _isAnnotatedAny(argExpr): + """ + Static method to check if the provided expression node is annotated with + 'typing.Any'. + + Support is provided for the following patterns: + * 'from typing import Any; foo: Any' + * 'import typing; foo: typing.Any' + * 'import typing as <alias>; foo: <alias>.Any' + + Type comments are also supported. Inline type comments are assumed to be + passed here as 'str', and function-level type comments are assumed to be + passed as 'ast.expr'. + + @param argExpr DESCRIPTION + @type ast.expr or str + @return flag indicating an annotation with 'typing.Any' + @rtype bool + """ + if isinstance(argExpr, ast.Name): + if argExpr.id == "Any": + return True + elif isinstance(argExpr, ast.Attribute): + if argExpr.attr == "Any": + return True + elif isinstance(argExpr, str): # __IGNORE_WARNING_Y102__ + if argExpr.split(".", maxsplit=1)[-1] == "Any": + return True + + return False + class Function: """ @@ -319,6 +358,9 @@ returnArg.has3107Annotation = True newFunction.isReturnAnnotated = True + if Argument._isAnnotatedAny(node.returns): + returnArg.isDynamicallyTyped = True + newFunction.args.append(returnArg) # Type comments in-line with input arguments are handled by the @@ -426,10 +468,15 @@ arg.hasTypeAnnotation = True arg.hasTypeComment = True + if Argument._isAnnotatedAny(hintComment): + arg.isDynamicallyTyped = True + # Return arg is always last funcObj.args[-1].hasTypeAnnotation = True funcObj.args[-1].hasTypeComment = True funcObj.isReturnAnnotated = True + if Argument._isAnnotatedAny(hintTree.returns): + arg.isDynamicallyTyped = True return funcObj @@ -476,9 +523,9 @@ # Short circuit return hintTree - if funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and len( - hintTree.argtypes - ) < (len(funcObj.args) - 1): + if funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and ( + len(hintTree.argtypes) < (len(funcObj.args) - 1) + ): # Subtract 1 to skip return arg hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFutureVisitor.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFutureVisitor.py Thu Jul 28 14:19:57 2022 +0200 @@ -9,7 +9,7 @@ # # The visitor and associated classes are adapted from flake8-future-annotations -# v0.0.4 +# v0.0.5 # import ast
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py Thu Jul 28 14:19:57 2022 +0200 @@ -49,10 +49,17 @@ "AnnotationsChecker", "PEP 484 disallows both type annotations and type comments", ), + "A401": QCoreApplication.translate( + "AnnotationsChecker", + "Dynamically typed expressions (typing.Any) are disallowed", + ), "A871": QCoreApplication.translate( "AnnotationsChecker", "missing 'from __future__ import annotations' but imports: {0}", ), + "A872": QCoreApplication.translate( + "AnnotationsChecker", "missing 'from __future__ import annotations'" + ), "A881": QCoreApplication.translate( "AnnotationsChecker", "type annotation coverage of {0}% is too low" ),
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Thu Jul 28 14:19:57 2022 +0200 @@ -539,6 +539,18 @@ ], } ) + if "AllowStarArgAny" not in self.__data["AnnotationsChecker"]: + # AllowStarArgAny is the sentinel for the second extension + self.__data["AnnotationsChecker"].update( + { + "AllowStarArgAny": AnnotationsCheckerDefaultArgs[ + "AllowStarArgAny" + ], + "ForceFutureAnnotations": AnnotationsCheckerDefaultArgs[ + "ForceFutureAnnotations" + ], + } + ) if "SecurityChecker" not in self.__data: from .Security.SecurityDefaults import SecurityDefaults @@ -628,6 +640,12 @@ self.mypyInitReturnCheckBox.setChecked( self.__data["AnnotationsChecker"]["MypyInitReturn"] ) + self.allowStarArgAnyCheckBox.setChecked( + self.__data["AnnotationsChecker"]["AllowStarArgAny"] + ) + self.forceFutureAnnotationsCheckBox.setChecked( + self.__data["AnnotationsChecker"]["ForceFutureAnnotations"] + ) self.dispatchDecoratorEdit.setText( ", ".join(self.__data["AnnotationsChecker"]["DispatchDecorators"]) ) @@ -805,6 +823,10 @@ "AllowUntypedDefs": self.allowUntypedDefsCheckBox.isChecked(), "AllowUntypedNested": self.allowUntypedNestedCheckBox.isChecked(), "MypyInitReturn": self.mypyInitReturnCheckBox.isChecked(), + "AllowStarArgAny": self.allowStarArgAnyCheckBox.isChecked(), + "ForceFutureAnnotations": ( + self.forceFutureAnnotationsCheckBox.isChecked() + ), "DispatchDecorators": [ d.strip() for d in self.dispatchDecoratorEdit.text().split(",") ], @@ -1246,6 +1268,10 @@ "AllowUntypedDefs": self.allowUntypedDefsCheckBox.isChecked(), "AllowUntypedNested": self.allowUntypedNestedCheckBox.isChecked(), "MypyInitReturn": self.mypyInitReturnCheckBox.isChecked(), + "AllowStarArgAny": self.allowStarArgAnyCheckBox.isChecked(), + "ForceFutureAnnotations": ( + self.forceFutureAnnotationsCheckBox.isChecked() + ), "DispatchDecorators": [ d.strip() for d in self.dispatchDecoratorEdit.text().split(",") ], @@ -1640,6 +1666,22 @@ ) ) ) + self.allowStarArgAnyCheckBox.setChecked( + Preferences.toBool( + Preferences.getSettings().value( + "PEP8/AllowStarArgAny", + AnnotationsCheckerDefaultArgs["AllowStarArgAny"], + ) + ) + ) + self.forceFutureAnnotationsCheckBox.setChecked( + Preferences.toBool( + Preferences.getSettings().value( + "PEP8/ForceFutureAnnotations", + AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"], + ) + ) + ) self.dispatchDecoratorEdit.setText( ", ".join( Preferences.toList( @@ -1873,6 +1915,13 @@ "PEP8/MypyInitReturn", self.mypyInitReturnCheckBox.isChecked() ) Preferences.getSettings().setValue( + "PEP8/AllowStarArgAny", self.allowStarArgAnyCheckBox.isChecked() + ) + Preferences.getSettings().setValue( + "PEP8/ForceFutureAnnotations", + self.forceFutureAnnotationsCheckBox.isChecked(), + ) + Preferences.getSettings().setValue( "PEP8/DispatchDecorators", [d.strip() for d in self.dispatchDecoratorEdit.text().split(",")], ) @@ -2014,6 +2063,13 @@ "PEP8/MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"] ) Preferences.getSettings().setValue( + "PEP8/AllowStarArgAny", AnnotationsCheckerDefaultArgs["AllowStarArgAny"] + ) + Preferences.getSettings().setValue( + "PEP8/ForceFutureAnnotations", + AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"], + ) + Preferences.getSettings().setValue( "PEP8/DispatchDecorators", AnnotationsCheckerDefaultArgs["DispatchDecorators"], )
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Wed Jul 27 18:02:43 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui Thu Jul 28 14:19:57 2022 +0200 @@ -977,7 +977,7 @@ </property> </widget> </item> - <item row="2" column="0" colspan="2"> + <item row="2" column="0"> <widget class="QCheckBox" name="mypyInitReturnCheckBox"> <property name="toolTip"> <string>Select to not report unhinted '__init__' return</string> @@ -987,6 +987,26 @@ </property> </widget> </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="allowStarArgAnyCheckBox"> + <property name="toolTip"> + <string>Allow dynamically typed *args and **kwargs</string> + </property> + <property name="text"> + <string>Allow dynamically typed * Arguments</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="forceFutureAnnotationsCheckBox"> + <property name="toolTip"> + <string>Enforce the presence of a 'from __future__ import annotations' statement</string> + </property> + <property name="text"> + <string>Enforce '__future__' statement</string> + </property> + </widget> + </item> </layout> </item> <item> @@ -1796,10 +1816,12 @@ <tabstop>maxAnnotationsComplexitySpinBox</tabstop> <tabstop>maxAnnotationsLengthSpinBox</tabstop> <tabstop>suppressNoneReturningCheckBox</tabstop> + <tabstop>suppressDummyArgsCheckBox</tabstop> <tabstop>allowUntypedDefsCheckBox</tabstop> + <tabstop>allowUntypedNestedCheckBox</tabstop> <tabstop>mypyInitReturnCheckBox</tabstop> - <tabstop>suppressDummyArgsCheckBox</tabstop> - <tabstop>allowUntypedNestedCheckBox</tabstop> + <tabstop>allowStarArgAnyCheckBox</tabstop> + <tabstop>forceFutureAnnotationsCheckBox</tabstop> <tabstop>dispatchDecoratorEdit</tabstop> <tabstop>overloadDecoratorEdit</tabstop> <tabstop>tmpDirectoriesEdit</tabstop>