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. |
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. |