--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py Wed Jul 13 14:55:47 2022 +0200 @@ -25,12 +25,20 @@ """ Class representing a function argument. """ - def __init__(self, argname, lineno, col_offset, annotationType, - hasTypeAnnotation=False, has3107Annotation=False, - hasTypeComment=False): + + def __init__( + self, + argname, + lineno, + col_offset, + annotationType, + hasTypeAnnotation=False, + has3107Annotation=False, + hasTypeComment=False, + ): """ Constructor - + @param argname name of the argument @type str @param lineno line number @@ -56,12 +64,12 @@ self.hasTypeAnnotation = hasTypeAnnotation self.has3107Annotation = has3107Annotation self.hasTypeComment = hasTypeComment - + @classmethod def fromNode(cls, node, annotationTypeName): """ Class method to create an Argument object based on the given node. - + @param node reference to the node to be converted @type ast.arguments @param annotationTypeName name of the annotation type @@ -88,14 +96,25 @@ """ Class representing a function. """ - def __init__(self, name, lineno, col_offset, - functionType=FunctionType.PUBLIC, isClassMethod=False, - classDecoratorType=None, isReturnAnnotated=False, - hasTypeComment=False, hasOnlyNoneReturns=True, - isNested=False, decoratorList=None, args=None): + + def __init__( + self, + name, + lineno, + col_offset, + functionType=FunctionType.PUBLIC, + isClassMethod=False, + classDecoratorType=None, + isReturnAnnotated=False, + hasTypeComment=False, + hasOnlyNoneReturns=True, + isNested=False, + decoratorList=None, + args=None, + ): """ Constructor - + @param name name of the function @type str @param lineno line number @@ -138,24 +157,24 @@ self.isNested = isNested self.decoratorList = decoratorList self.args = args - + def isFullyAnnotated(self): """ Public method to check, if the function definition is fully type annotated. Note: self.args will always include an Argument object for return. - + @return flag indicating a fully annotated function definition @rtype bool """ return all(arg.hasTypeAnnotation for arg in self.args) - + def isDynamicallyTyped(self): """ Public method to check, if a function definition is dynamically typed (i.e. completely lacking hints). - + @return flag indicating a dynamically typed function definition @rtype bool """ @@ -165,7 +184,7 @@ """ Public method to provide a list of arguments with missing type annotations. - + @return list of arguments with missing type annotations @rtype list of Argument """ @@ -174,26 +193,26 @@ def getAnnotatedArguments(self): """ Public method to get list of arguments with type annotations. - + @return list of arguments with type annotations. @rtype list of Argument """ return [arg for arg in self.args if arg.hasTypeAnnotation] - + def hasDecorator(self, checkDecorators): """ Public method to check whether the function node is decorated by any of the provided decorators. - + Decorator matching is done against the provided `checkDecorators` set. Decorators are assumed to be either a module attribute (e.g. `@typing.overload`) or name (e.g. `@overload`). For the case of a module attribute, only the attribute is checked against `overload_decorators`. - + Note: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly supported. - + @param checkDecorators set of decorators to check against @type set of str @return flag indicating the presence of any decorators @@ -205,18 +224,18 @@ return self.__decoratorChecker(decorator, checkDecorators) else: return False - + def __decoratorChecker(self, decorator, checkDecorators): """ Private method to check the provided decorator for a match against the provided set of check names. - + Decorators are assumed to be of the following form: * `a.name` or `a.name()` * `name` or `name()` - + Note: Deeper imports (e.g. `a.b.name`) are not explicitly supported. - + @param decorator decorator node to check @type ast.Attribute, ast.Call or ast.Name @param checkDecorators set of decorators to check against @@ -237,24 +256,24 @@ # `decorator.func` will be `ast.Name` or `ast.Attribute`, # which we can check recursively return self.__decoratorChecker(decorator.func, checkDecorators) - + return None - + @classmethod def fromNode(cls, node, lines, **kwargs): """ Class method to create a Function object from ast.FunctionDef or ast.AsyncFunctionDef nodes. - + Accept the source code, as a list of strings, in order to get the column where the function definition ends. - + With exceptions, input kwargs are passed straight through to Function's __init__. The following kwargs will be overridden: * function_type * class_decorator_type * args - + @param node reference to the function definition node @type ast.AsyncFunctionDef or ast.FunctionDef @param lines list of source code lines @@ -266,63 +285,63 @@ """ # Extract function types from function name kwargs["functionType"] = cls.getFunctionType(node.name) - + # Identify type of class method, if applicable if kwargs.get("isClassMethod", False): kwargs["classDecoratorType"] = cls.getClassDecoratorType(node) - + # Store raw decorator list for use by property methods kwargs["decoratorList"] = node.decorator_list - + # Instantiate empty args list here since it has no default kwargs["args"] = [] newFunction = cls(node.name, node.lineno, node.col_offset, **kwargs) - + # Iterate over arguments by type & add for argType in AST_ARG_TYPES: args = node.args.__getattribute__(argType) if args: if not isinstance(args, list): args = [args] - + newFunction.args.extend( - [Argument.fromNode(arg, argType.upper()) - for arg in args] + [Argument.fromNode(arg, argType.upper()) for arg in args] ) - + # Create an Argument object for the return hint defEndLineno, defEndColOffset = cls.colonSeeker(node, lines) - returnArg = Argument("return", defEndLineno, defEndColOffset, - AnnotationType.RETURN) + returnArg = Argument( + "return", defEndLineno, defEndColOffset, AnnotationType.RETURN + ) if node.returns: returnArg.hasTypeAnnotation = True returnArg.has3107Annotation = True newFunction.isReturnAnnotated = True - + newFunction.args.append(returnArg) - + # Type comments in-line with input arguments are handled by the # Argument class. If a function-level type comment is present, attempt # to parse for any missed type hints. if node.type_comment: newFunction.hasTypeComment = True newFunction = cls.tryTypeComment(newFunction, node) - + # Check for the presence of non-`None` returns using the special-case # return node visitor. returnVisitor = ReturnVisitor(node) returnVisitor.visit(node) newFunction.hasOnlyNoneReturns = returnVisitor.hasOnlyNoneReturns - + return newFunction - + @staticmethod def colonSeeker(node, lines): """ Static method to find the line & column indices of the function definition's closing colon. - + @param node reference to the function definition node @type ast.AsyncFunctionDef or ast.FunctionDef @param lines list of source code lines @@ -332,9 +351,8 @@ """ # Special case single line function definitions if node.lineno == node.body[0].lineno: - return Function._singleLineColonSeeker( - node, lines[node.lineno - 1]) - + return Function._singleLineColonSeeker(node, lines[node.lineno - 1]) + # With Python < 3.8, the function node includes the docstring and the # body does not, so we have to rewind through any docstrings, if # present, before looking for the def colon. We should end up with @@ -351,24 +369,24 @@ if '"""' in lines[defEndLineno - 1]: # Docstring has closed break - + # Once we've gotten here, we've found the line where the docstring # begins, so we have to step up one more line to get to the close of # the def. defEndLineno -= 1 - + # Use str.rfind() to account for annotations on the same line, # definition closure should be the last : on the line defEndColOffset = lines[defEndLineno - 1].rfind(":") - + return defEndLineno, defEndColOffset - + @staticmethod def _singleLineColonSeeker(node, line): """ Static method to find the line & column indices of a single line function definition. - + @param node reference to the function definition node @type ast.AsyncFunctionDef or ast.FunctionDef @param line source code line @@ -379,17 +397,17 @@ colStart = node.col_offset colEnd = node.body[0].col_offset defEndColOffset = line.rfind(":", colStart, colEnd) - + return node.lineno, defEndColOffset - + @staticmethod def tryTypeComment(funcObj, node): """ Static method to infer type hints from a function-level type comment. - + If a function is type commented it is assumed to have a return annotation, otherwise Python will fail to parse the hint. - + @param funcObj reference to the Function object @type Function @param node reference to the function definition node @@ -399,56 +417,54 @@ """ hintTree = ast.parse(node.type_comment, "<func_type>", "func_type") hintTree = Function._maybeInjectClassArgument(hintTree, funcObj) - - for arg, hintComment in itertools.zip_longest( - funcObj.args, hintTree.argtypes - ): + + for arg, hintComment in itertools.zip_longest(funcObj.args, hintTree.argtypes): if isinstance(hintComment, ast.Ellipsis): continue - + if arg and hintComment: arg.hasTypeAnnotation = True arg.hasTypeComment = True - + # Return arg is always last funcObj.args[-1].hasTypeAnnotation = True funcObj.args[-1].hasTypeComment = True funcObj.isReturnAnnotated = True - + return funcObj - + @staticmethod def _maybeInjectClassArgument(hintTree, funcObj): """ Static method to inject `self` or `cls` args into a type comment to align with PEP 3107-style annotations. - + Because PEP 484 does not describe a method to provide partial function- level type comments, there is a potential for ambiguity in the context of both class methods and classmethods when aligning type comments to method arguments. - + These two class methods, for example, should lint equivalently: - + def bar(self, a): # type: (int) -> int ... - + def bar(self, a: int) -> int ... - + When this example type comment is parsed by `ast` and then matched with the method's arguments, it associates the `int` hint to `self` rather than `a`, so a dummy hint needs to be provided in situations where `self` or `class` are not hinted in the type comment in order to achieve equivalent linting results to PEP-3107 style annotations. - + A dummy `ast.Ellipses` constant is injected if the following criteria are met: 1. The function node is either a class method or classmethod 2. The number of hinted args is at least 1 less than the number of function args - + @param hintTree parsed type hint node @type ast.FunctionType @param funcObj reference to the Function object @@ -460,15 +476,14 @@ # 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 - + return hintTree - + @staticmethod def getFunctionType(functionName): """ @@ -479,7 +494,7 @@ 2. Private: function name prefixed by "__" 3. Protected: function name prefixed by "_" 4. Public: everything else - + @param functionName function name to be checked @type str @return type of function @@ -493,19 +508,19 @@ return FunctionType.PROTECTED else: return FunctionType.PUBLIC - + @staticmethod def getClassDecoratorType(functionNode): """ Static method to get the class method's decorator type from its function node. - + Only @classmethod and @staticmethod decorators are identified; all other decorators are ignored - + If @classmethod or @staticmethod decorators are not present, this function will return None. - + @param functionNode reference to the function definition node @type ast.AsyncFunctionDef or ast.FunctionDef @return class decorator type @@ -532,28 +547,29 @@ """ Class implementing a node visitor to check function annotations. """ + AstFuncTypes = (ast.FunctionDef, ast.AsyncFunctionDef) - + def __init__(self, lines): """ Constructor - + @param lines source code lines of the function @type list of str """ self.lines = lines self.functionDefinitions = [] self.__context = [] - + def switchContext(self, node): """ Public method implementing a context switcher as a generic function visitor in order to track function context. - + Without keeping track of context, it's challenging to reliably differentiate class methods from "regular" functions, especially in the case of nested classes. - + @param node reference to the function definition node to be analyzed @type ast.AsyncFunctionDef or ast.FunctionDef """ @@ -567,21 +583,18 @@ self.functionDefinitions.append( Function.fromNode(node, self.lines, isClassMethod=True) ) - elif isinstance( - self.__context[-1], FunctionVisitor.AstFuncTypes - ): + elif isinstance(self.__context[-1], FunctionVisitor.AstFuncTypes): # Check for nested function & pass the appropriate flag self.functionDefinitions.append( Function.fromNode(node, self.lines, isNested=True) ) else: - self.functionDefinitions.append( - Function.fromNode(node, self.lines)) - + self.functionDefinitions.append(Function.fromNode(node, self.lines)) + self.__context.append(node) self.generic_visit(node) self.__context.pop() - + visit_FunctionDef = switchContext visit_AsyncFunctionDef = switchContext visit_ClassDef = switchContext @@ -591,24 +604,25 @@ """ Class implementing a node visitor to check the return statements of a function node. - + If the function node being visited has an explicit return statement of anything other than `None`, the `instance.hasOnlyNoneReturns` flag will be set to `False`. - + If the function node being visited has no return statement, or contains only return statement(s) that explicitly return `None`, the `instance.hasOnlyNoneReturns` flag will be set to `True`. - + Due to the generic visiting being done, we need to keep track of the context in which a non-`None` return node is found. These functions are added to a set that is checked to see whether nor not the parent node is present. """ + def __init__(self, parentNode): """ Constructor - + @param parentNode reference to the function definition node to be analyzed @type ast.AsyncFunctionDef or ast.FunctionDef @@ -616,28 +630,28 @@ self.parentNode = parentNode self.__context = [] self.__nonNoneReturnNodes = set() - + @property def hasOnlyNoneReturns(self): """ Public method indicating, that the parent node isn't in the visited nodes that don't return `None`. - + @return flag indicating, that the parent node isn't in the visited nodes that don't return `None` @rtype bool """ return self.parentNode not in self.__nonNoneReturnNodes - + def visit_Return(self, node): """ Public method to check each Return node to see if it returns anything other than `None`. - + If the node being visited returns anything other than `None`, its parent context is added to the set of non-returning child nodes of the parent node. - + @param node reference to the AST Return node @type ast.Return """ @@ -647,22 +661,22 @@ # `ast.NameConstant`, which we need to check to see if it's # actually `None` if ( - isinstance(node.value, (ast.Constant, ast.NameConstant)) and - node.value.value is None + isinstance(node.value, (ast.Constant, ast.NameConstant)) + and node.value.value is None ): return - + self.__nonNoneReturnNodes.add(self.__context[-1]) def switchContext(self, node): """ Public method implementing a context switcher as a generic function visitor in order to track function context. - + Without keeping track of context, it's challenging to reliably differentiate class methods from "regular" functions, especially in the case of nested classes. - + @param node reference to the function definition node to be analyzed @type ast.AsyncFunctionDef or ast.FunctionDef """