src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py

branch
eric7
changeset 10058
5d965939ab85
parent 9653
e67609152c5e
child 10119
64147a7e6393
equal deleted inserted replaced
10057:1e31ca1078ab 10058:5d965939ab85
5 5
6 """ 6 """
7 Module implementing a node visitor for function type annotations. 7 Module implementing a node visitor for function type annotations.
8 """ 8 """
9 9
10 # 10 #####################################################################################
11 # The visitor and associated classes are adapted from flake8-annotations v2.9.0 11 ## The visitor and associated classes are adapted from flake8-annotations v3.0.1
12 # 12 #####################################################################################
13 13
14 import ast 14 import ast
15 import itertools
16 import sys 15 import sys
17 16
18 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType 17 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
19 18
20 # The order of AST_ARG_TYPES must match Python's grammar 19 # The order of AST_ARG_TYPES must match Python's grammar
21 AST_ARG_TYPES = ("posonlyargs", "args", "vararg", "kwonlyargs", "kwarg") 20 AST_ARG_TYPES = ("args", "vararg", "kwonlyargs", "kwarg")
21 if sys.version_info >= (3, 8, 0):
22 AST_ARG_TYPES = ("posonlyargs",) + AST_ARG_TYPES
22 23
23 24
24 class Argument: 25 class Argument:
25 """ 26 """
26 Class representing a function argument. 27 Class representing a function argument.
31 argname, 32 argname,
32 lineno, 33 lineno,
33 col_offset, 34 col_offset,
34 annotationType, 35 annotationType,
35 hasTypeAnnotation=False, 36 hasTypeAnnotation=False,
36 has3107Annotation=False,
37 hasTypeComment=False, 37 hasTypeComment=False,
38 isDynamicallyTyped=False, 38 isDynamicallyTyped=False,
39 ): 39 ):
40 """ 40 """
41 Constructor 41 Constructor
49 @param annotationType type of annotation 49 @param annotationType type of annotation
50 @type AnnotationType 50 @type AnnotationType
51 @param hasTypeAnnotation flag indicating the presence of a type 51 @param hasTypeAnnotation flag indicating the presence of a type
52 annotation (defaults to False) 52 annotation (defaults to False)
53 @type bool (optional) 53 @type bool (optional)
54 @param has3107Annotation flag indicating the presence of a PEP 3107
55 annotation (defaults to False)
56 @type bool (optional)
57 @param hasTypeComment flag indicating the presence of a type comment 54 @param hasTypeComment flag indicating the presence of a type comment
58 (defaults to False) 55 (defaults to False)
59 @type bool (optional) 56 @type bool (optional)
60 @param isDynamicallyTyped flag indicating dynamic typing (defaults to False) 57 @param isDynamicallyTyped flag indicating dynamic typing (defaults to False)
61 @type bool (optional) 58 @type bool (optional)
63 self.argname = argname 60 self.argname = argname
64 self.lineno = lineno 61 self.lineno = lineno
65 self.col_offset = col_offset 62 self.col_offset = col_offset
66 self.annotationType = annotationType 63 self.annotationType = annotationType
67 self.hasTypeAnnotation = hasTypeAnnotation 64 self.hasTypeAnnotation = hasTypeAnnotation
68 self.has3107Annotation = has3107Annotation
69 self.hasTypeComment = hasTypeComment 65 self.hasTypeComment = hasTypeComment
70 self.isDynamicallyTyped = isDynamicallyTyped 66 self.isDynamicallyTyped = isDynamicallyTyped
71 67
72 @classmethod 68 @classmethod
73 def fromNode(cls, node, annotationTypeName): 69 def fromNode(cls, node, annotationTypeName):
82 @rtype Argument 78 @rtype Argument
83 """ 79 """
84 annotationType = AnnotationType[annotationTypeName] 80 annotationType = AnnotationType[annotationTypeName]
85 newArg = cls(node.arg, node.lineno, node.col_offset, annotationType) 81 newArg = cls(node.arg, node.lineno, node.col_offset, annotationType)
86 82
87 newArg.hasTypeAnnotation = False
88 if node.annotation: 83 if node.annotation:
89 newArg.hasTypeAnnotation = True 84 newArg.hasTypeAnnotation = True
90 newArg.has3107Annotation = True 85
86 if cls._isAnnotatedAny(node.annotation):
87 newArg.isDynamicallyTyped = True
91 88
92 if node.type_comment: 89 if node.type_comment:
93 newArg.hasTypeAnnotation = True
94 newArg.hasTypeComment = True 90 newArg.hasTypeComment = True
95
96 if cls._isAnnotatedAny(node.type_comment):
97 newArg.isDynamicallyTyped = True
98 91
99 return newArg 92 return newArg
100 93
101 @staticmethod 94 @staticmethod
102 def _isAnnotatedAny(argExpr): 95 def _isAnnotatedAny(argExpr):
107 Support is provided for the following patterns: 100 Support is provided for the following patterns:
108 * 'from typing import Any; foo: Any' 101 * 'from typing import Any; foo: Any'
109 * 'import typing; foo: typing.Any' 102 * 'import typing; foo: typing.Any'
110 * 'import typing as <alias>; foo: <alias>.Any' 103 * 'import typing as <alias>; foo: <alias>.Any'
111 104
112 Type comments are also supported. Inline type comments are assumed to be
113 passed here as 'str', and function-level type comments are assumed to be
114 passed as 'ast.expr'.
115
116 @param argExpr DESCRIPTION 105 @param argExpr DESCRIPTION
117 @type ast.expr or str 106 @type ast.expr or str
118 @return flag indicating an annotation with 'typing.Any' 107 @return flag indicating an annotation with 'typing.Any'
119 @rtype bool 108 @rtype bool
120 """ 109 """
121 if isinstance(argExpr, ast.Name) and argExpr.id == "Any": 110 if isinstance(argExpr, ast.Name) and argExpr.id == "Any":
122 return True 111 return True
123 elif isinstance(argExpr, ast.Attribute) and argExpr.attr == "Any": 112 elif isinstance(argExpr, ast.Attribute) and argExpr.attr == "Any":
124 return True
125 elif isinstance(argExpr, str) and argExpr.split(".", maxsplit=1)[-1] == "Any":
126 return True 113 return True
127 114
128 return False 115 return False
129 116
130 117
350 returnArg = Argument( 337 returnArg = Argument(
351 "return", defEndLineno, defEndColOffset, AnnotationType.RETURN 338 "return", defEndLineno, defEndColOffset, AnnotationType.RETURN
352 ) 339 )
353 if node.returns: 340 if node.returns:
354 returnArg.hasTypeAnnotation = True 341 returnArg.hasTypeAnnotation = True
355 returnArg.has3107Annotation = True
356 newFunction.isReturnAnnotated = True 342 newFunction.isReturnAnnotated = True
357 343
358 if Argument._isAnnotatedAny(node.returns): 344 if Argument._isAnnotatedAny(node.returns):
359 returnArg.isDynamicallyTyped = True 345 returnArg.isDynamicallyTyped = True
360 346
361 newFunction.args.append(returnArg) 347 newFunction.args.append(returnArg)
362 348
363 # Type comments in-line with input arguments are handled by the
364 # Argument class. If a function-level type comment is present, attempt
365 # to parse for any missed type hints.
366 if node.type_comment: 349 if node.type_comment:
367 newFunction.hasTypeComment = True 350 newFunction.hasTypeComment = True
368 newFunction = cls.tryTypeComment(newFunction, node)
369 351
370 # Check for the presence of non-`None` returns using the special-case 352 # Check for the presence of non-`None` returns using the special-case
371 # return node visitor. 353 # return node visitor.
372 returnVisitor = ReturnVisitor(node) 354 returnVisitor = ReturnVisitor(node)
373 returnVisitor.visit(node) 355 returnVisitor.visit(node)
436 colStart = node.col_offset 418 colStart = node.col_offset
437 colEnd = node.body[0].col_offset 419 colEnd = node.body[0].col_offset
438 defEndColOffset = line.rfind(":", colStart, colEnd) 420 defEndColOffset = line.rfind(":", colStart, colEnd)
439 421
440 return node.lineno, defEndColOffset 422 return node.lineno, defEndColOffset
441
442 @staticmethod
443 def tryTypeComment(funcObj, node):
444 """
445 Static method to infer type hints from a function-level type comment.
446
447 If a function is type commented it is assumed to have a return
448 annotation, otherwise Python will fail to parse the hint.
449
450 @param funcObj reference to the Function object
451 @type Function
452 @param node reference to the function definition node
453 @type ast.AsyncFunctionDef or ast.FunctionDef
454 @return reference to the modified Function object
455 @rtype Function
456 """
457 hintTree = ast.parse(node.type_comment, "<func_type>", "func_type")
458 hintTree = Function._maybeInjectClassArgument(hintTree, funcObj)
459
460 for arg, hintComment in itertools.zip_longest(funcObj.args, hintTree.argtypes):
461 if isinstance(hintComment, ast.Ellipsis):
462 continue
463
464 if arg and hintComment:
465 arg.hasTypeAnnotation = True
466 arg.hasTypeComment = True
467
468 if Argument._isAnnotatedAny(hintComment):
469 arg.isDynamicallyTyped = True
470
471 # Return arg is always last
472 funcObj.args[-1].hasTypeAnnotation = True
473 funcObj.args[-1].hasTypeComment = True
474 funcObj.isReturnAnnotated = True
475 if Argument._isAnnotatedAny(hintTree.returns):
476 arg.isDynamicallyTyped = True
477
478 return funcObj
479
480 @staticmethod
481 def _maybeInjectClassArgument(hintTree, funcObj):
482 """
483 Static method to inject `self` or `cls` args into a type comment to
484 align with PEP 3107-style annotations.
485
486 Because PEP 484 does not describe a method to provide partial function-
487 level type comments, there is a potential for ambiguity in the context
488 of both class methods and classmethods when aligning type comments to
489 method arguments.
490
491 These two class methods, for example, should lint equivalently:
492
493 def bar(self, a):
494 # type: (int) -> int
495 ...
496
497 def bar(self, a: int) -> int
498 ...
499
500 When this example type comment is parsed by `ast` and then matched with
501 the method's arguments, it associates the `int` hint to `self` rather
502 than `a`, so a dummy hint needs to be provided in situations where
503 `self` or `class` are not hinted in the type comment in order to
504 achieve equivalent linting results to PEP-3107 style annotations.
505
506 A dummy `ast.Ellipses` constant is injected if the following criteria
507 are met:
508 1. The function node is either a class method or classmethod
509 2. The number of hinted args is at least 1 less than the number
510 of function args
511
512 @param hintTree parsed type hint node
513 @type ast.FunctionType
514 @param funcObj reference to the Function object
515 @type Function
516 @return reference to the hint node
517 @rtype ast.FunctionType
518 """
519 if not funcObj.isClassMethod:
520 # Short circuit
521 return hintTree
522
523 if funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and (
524 len(hintTree.argtypes) < (len(funcObj.args) - 1)
525 ):
526 # Subtract 1 to skip return arg
527 hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes
528
529 return hintTree
530 423
531 @staticmethod 424 @staticmethod
532 def getFunctionType(functionName): 425 def getFunctionType(functionName):
533 """ 426 """
534 Static method to determine the function's FunctionType from its name. 427 Static method to determine the function's FunctionType from its name.

eric ide

mercurial