|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a node visitor for function type annotations. |
|
8 """ |
|
9 |
|
10 # |
|
11 # The visitor and associated classes are adapted from flake8-annotations v2.7.0 |
|
12 # |
|
13 |
|
14 import ast |
|
15 import itertools |
|
16 import sys |
|
17 |
|
18 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType |
|
19 |
|
20 # The order of AST_ARG_TYPES must match Python's grammar |
|
21 AST_ARG_TYPES = ("posonlyargs", "args", "vararg", "kwonlyargs", "kwarg") |
|
22 |
|
23 |
|
24 class Argument: |
|
25 """ |
|
26 Class representing a function argument. |
|
27 """ |
|
28 def __init__(self, argname, lineno, col_offset, annotationType, |
|
29 hasTypeAnnotation=False, has3107Annotation=False, |
|
30 hasTypeComment=False): |
|
31 """ |
|
32 Constructor |
|
33 |
|
34 @param argname name of the argument |
|
35 @type str |
|
36 @param lineno line number |
|
37 @type int |
|
38 @param col_offset column number |
|
39 @type int |
|
40 @param annotationType type of annotation |
|
41 @type AnnotationType |
|
42 @param hasTypeAnnotation flag indicating the presence of a type |
|
43 annotation (defaults to False) |
|
44 @type bool (optional) |
|
45 @param has3107Annotation flag indicating the presence of a PEP 3107 |
|
46 annotation (defaults to False) |
|
47 @type bool (optional) |
|
48 @param hasTypeComment flag indicating the presence of a type comment |
|
49 (defaults to False) |
|
50 @type bool (optional) |
|
51 """ |
|
52 self.argname = argname |
|
53 self.lineno = lineno |
|
54 self.col_offset = col_offset |
|
55 self.annotationType = annotationType |
|
56 self.hasTypeAnnotation = hasTypeAnnotation |
|
57 self.has3107Annotation = has3107Annotation |
|
58 self.hasTypeComment = hasTypeComment |
|
59 |
|
60 @classmethod |
|
61 def fromNode(cls, node, annotationTypeName): |
|
62 """ |
|
63 Class method to create an Argument object based on the given node. |
|
64 |
|
65 @param node reference to the node to be converted |
|
66 @type ast.arguments |
|
67 @param annotationTypeName name of the annotation type |
|
68 @type str |
|
69 @return Argument object |
|
70 @rtype Argument |
|
71 """ |
|
72 annotationType = AnnotationType[annotationTypeName] |
|
73 newArg = cls(node.arg, node.lineno, node.col_offset, annotationType) |
|
74 |
|
75 newArg.hasTypeAnnotation = False |
|
76 if node.annotation: |
|
77 newArg.hasTypeAnnotation = True |
|
78 newArg.has3107Annotation = True |
|
79 |
|
80 if node.type_comment: |
|
81 newArg.hasTypeAnnotation = True |
|
82 newArg.hasTypeComment = True |
|
83 |
|
84 return newArg |
|
85 |
|
86 |
|
87 class Function: |
|
88 """ |
|
89 Class representing a function. |
|
90 """ |
|
91 def __init__(self, name, lineno, col_offset, |
|
92 functionType=FunctionType.PUBLIC, isClassMethod=False, |
|
93 classDecoratorType=None, isReturnAnnotated=False, |
|
94 hasTypeComment=False, hasOnlyNoneReturns=True, |
|
95 isNested=False, decoratorList=None, args=None): |
|
96 """ |
|
97 Constructor |
|
98 |
|
99 @param name name of the function |
|
100 @type str |
|
101 @param lineno line number |
|
102 @type int |
|
103 @param col_offset column number |
|
104 @type int |
|
105 @param functionType type of the function (defaults to |
|
106 FunctionType.PUBLIC) |
|
107 @type FunctionType (optional) |
|
108 @param isClassMethod flag indicating a class method (defaults to False) |
|
109 @type bool (optional) |
|
110 @param classDecoratorType type of a function decorator |
|
111 (defaults to None) |
|
112 @type ClassDecoratorType or None (optional) |
|
113 @param isReturnAnnotated flag indicating the presence of a return |
|
114 type annotation (defaults to False) |
|
115 @type bool (optional) |
|
116 @param hasTypeComment flag indicating the presence of a type comment |
|
117 (defaults to False) |
|
118 @type bool (optional) |
|
119 @param hasOnlyNoneReturns flag indicating only None return values |
|
120 (defaults to True) |
|
121 @type bool (optional) |
|
122 @param isNested flag indicating a nested function (defaults to False) |
|
123 @type bool (optional) |
|
124 @param decoratorList list of decorator nodes (defaults to None) |
|
125 @type list of ast.Attribute, ast.Call or ast.Name (optional) |
|
126 @param args list of arguments (defaults to None) |
|
127 @type list of Argument (optional) |
|
128 """ |
|
129 self.name = name |
|
130 self.lineno = lineno |
|
131 self.col_offset = col_offset |
|
132 self.functionType = functionType |
|
133 self.isClassMethod = isClassMethod |
|
134 self.classDecoratorType = classDecoratorType |
|
135 self.isReturnAnnotated = isReturnAnnotated |
|
136 self.hasTypeComment = hasTypeComment |
|
137 self.hasOnlyNoneReturns = hasOnlyNoneReturns |
|
138 self.isNested = isNested |
|
139 self.decoratorList = decoratorList |
|
140 self.args = args |
|
141 |
|
142 def isFullyAnnotated(self): |
|
143 """ |
|
144 Public method to check, if the function definition is fully type |
|
145 annotated. |
|
146 |
|
147 Note: self.args will always include an Argument object for return. |
|
148 |
|
149 @return flag indicating a fully annotated function definition |
|
150 @rtype bool |
|
151 """ |
|
152 return all(arg.hasTypeAnnotation for arg in self.args) |
|
153 |
|
154 def isDynamicallyTyped(self): |
|
155 """ |
|
156 Public method to check, if a function definition is dynamically typed |
|
157 (i.e. completely lacking hints). |
|
158 |
|
159 @return flag indicating a dynamically typed function definition |
|
160 @rtype bool |
|
161 """ |
|
162 return not any(arg.hasTypeAnnotation for arg in self.args) |
|
163 |
|
164 def getMissedAnnotations(self): |
|
165 """ |
|
166 Public method to provide a list of arguments with missing type |
|
167 annotations. |
|
168 |
|
169 @return list of arguments with missing type annotations |
|
170 @rtype list of Argument |
|
171 """ |
|
172 return [arg for arg in self.args if not arg.hasTypeAnnotation] |
|
173 |
|
174 def getAnnotatedArguments(self): |
|
175 """ |
|
176 Public method to get list of arguments with type annotations. |
|
177 |
|
178 @return list of arguments with type annotations. |
|
179 @rtype list of Argument |
|
180 """ |
|
181 return [arg for arg in self.args if arg.hasTypeAnnotation] |
|
182 |
|
183 def hasDecorator(self, checkDecorators): |
|
184 """ |
|
185 Public method to check whether the function node is decorated by any of |
|
186 the provided decorators. |
|
187 |
|
188 Decorator matching is done against the provided `checkDecorators` set. |
|
189 Decorators are assumed to be either a module attribute (e.g. |
|
190 `@typing.overload`) or name (e.g. `@overload`). For the case of a |
|
191 module attribute, only the attribute is checked against |
|
192 `overload_decorators`. |
|
193 |
|
194 Note: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly |
|
195 supported. |
|
196 |
|
197 @param checkDecorators set of decorators to check against |
|
198 @type set of str |
|
199 @return flag indicating the presence of any decorators |
|
200 @rtype bool |
|
201 """ |
|
202 for decorator in self.decoratorList: |
|
203 # Drop to a helper to allow for simpler handling of callable |
|
204 # decorators |
|
205 return self.__decoratorChecker(decorator, checkDecorators) |
|
206 else: |
|
207 return False |
|
208 |
|
209 def __decoratorChecker(self, decorator, checkDecorators): |
|
210 """ |
|
211 Private method to check the provided decorator for a match against the |
|
212 provided set of check names. |
|
213 |
|
214 Decorators are assumed to be of the following form: |
|
215 * `a.name` or `a.name()` |
|
216 * `name` or `name()` |
|
217 |
|
218 Note: Deeper imports (e.g. `a.b.name`) are not explicitly supported. |
|
219 |
|
220 @param decorator decorator node to check |
|
221 @type ast.Attribute, ast.Call or ast.Name |
|
222 @param checkDecorators set of decorators to check against |
|
223 @type set of str |
|
224 @return flag indicating the presence of any decorators |
|
225 @rtype bool |
|
226 """ |
|
227 if isinstance(decorator, ast.Name): |
|
228 # e.g. `@overload`, where `decorator.id` will be the name |
|
229 if decorator.id in checkDecorators: |
|
230 return True |
|
231 elif isinstance(decorator, ast.Attribute): |
|
232 # e.g. `@typing.overload`, where `decorator.attr` will be the name |
|
233 if decorator.attr in checkDecorators: |
|
234 return True |
|
235 elif isinstance(decorator, ast.Call): |
|
236 # e.g. `@overload()` or `@typing.overload()`, where |
|
237 # `decorator.func` will be `ast.Name` or `ast.Attribute`, |
|
238 # which we can check recursively |
|
239 return self.__decoratorChecker(decorator.func, checkDecorators) |
|
240 |
|
241 return None |
|
242 |
|
243 @classmethod |
|
244 def fromNode(cls, node, lines, **kwargs): |
|
245 """ |
|
246 Class method to create a Function object from ast.FunctionDef or |
|
247 ast.AsyncFunctionDef nodes. |
|
248 |
|
249 Accept the source code, as a list of strings, in order to get the |
|
250 column where the function definition ends. |
|
251 |
|
252 With exceptions, input kwargs are passed straight through to Function's |
|
253 __init__. The following kwargs will be overridden: |
|
254 * function_type |
|
255 * class_decorator_type |
|
256 * args |
|
257 |
|
258 @param node reference to the function definition node |
|
259 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
260 @param lines list of source code lines |
|
261 @type list of str |
|
262 @keyparam **kwargs keyword arguments |
|
263 @type dict |
|
264 @return created Function object |
|
265 @rtype Function |
|
266 """ |
|
267 # Extract function types from function name |
|
268 kwargs["functionType"] = cls.getFunctionType(node.name) |
|
269 |
|
270 # Identify type of class method, if applicable |
|
271 if kwargs.get("isClassMethod", False): |
|
272 kwargs["classDecoratorType"] = cls.getClassDecoratorType(node) |
|
273 |
|
274 # Store raw decorator list for use by property methods |
|
275 kwargs["decoratorList"] = node.decorator_list |
|
276 |
|
277 # Instantiate empty args list here since it has no default |
|
278 kwargs["args"] = [] |
|
279 |
|
280 newFunction = cls(node.name, node.lineno, node.col_offset, **kwargs) |
|
281 |
|
282 # Iterate over arguments by type & add |
|
283 for argType in AST_ARG_TYPES: |
|
284 args = node.args.__getattribute__(argType) |
|
285 if args: |
|
286 if not isinstance(args, list): |
|
287 args = [args] |
|
288 |
|
289 newFunction.args.extend( |
|
290 [Argument.fromNode(arg, argType.upper()) |
|
291 for arg in args] |
|
292 ) |
|
293 |
|
294 # Create an Argument object for the return hint |
|
295 defEndLineno, defEndColOffset = cls.colonSeeker(node, lines) |
|
296 returnArg = Argument("return", defEndLineno, defEndColOffset, |
|
297 AnnotationType.RETURN) |
|
298 if node.returns: |
|
299 returnArg.hasTypeAnnotation = True |
|
300 returnArg.has3107Annotation = True |
|
301 newFunction.isReturnAnnotated = True |
|
302 |
|
303 newFunction.args.append(returnArg) |
|
304 |
|
305 # Type comments in-line with input arguments are handled by the |
|
306 # Argument class. If a function-level type comment is present, attempt |
|
307 # to parse for any missed type hints. |
|
308 if node.type_comment: |
|
309 newFunction.hasTypeComment = True |
|
310 newFunction = cls.tryTypeComment(newFunction, node) |
|
311 |
|
312 # Check for the presence of non-`None` returns using the special-case |
|
313 # return node visitor. |
|
314 returnVisitor = ReturnVisitor(node) |
|
315 returnVisitor.visit(node) |
|
316 newFunction.hasOnlyNoneReturns = returnVisitor.hasOnlyNoneReturns |
|
317 |
|
318 return newFunction |
|
319 |
|
320 @staticmethod |
|
321 def colonSeeker(node, lines): |
|
322 """ |
|
323 Static method to find the line & column indices of the function |
|
324 definition's closing colon. |
|
325 |
|
326 @param node reference to the function definition node |
|
327 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
328 @param lines list of source code lines |
|
329 @type list of str |
|
330 @return line and column offset of the colon |
|
331 @rtype tuple of (int, int) |
|
332 """ |
|
333 # Special case single line function definitions |
|
334 if node.lineno == node.body[0].lineno: |
|
335 return Function._singleLineColonSeeker( |
|
336 node, lines[node.lineno - 1]) |
|
337 |
|
338 # With Python < 3.8, the function node includes the docstring and the |
|
339 # body does not, so we have to rewind through any docstrings, if |
|
340 # present, before looking for the def colon. We should end up with |
|
341 # lines[defEndLineno - 1] having the colon. |
|
342 defEndLineno = node.body[0].lineno |
|
343 if sys.version_info < (3, 8, 0): |
|
344 # If the docstring is on one line then no rewinding is necessary. |
|
345 nTripleQuotes = lines[defEndLineno - 1].count('"""') |
|
346 if nTripleQuotes == 1: |
|
347 # Docstring closure, rewind until the opening is found and take |
|
348 # the line prior. |
|
349 while True: |
|
350 defEndLineno -= 1 |
|
351 if '"""' in lines[defEndLineno - 1]: |
|
352 # Docstring has closed |
|
353 break |
|
354 |
|
355 # Once we've gotten here, we've found the line where the docstring |
|
356 # begins, so we have to step up one more line to get to the close of |
|
357 # the def. |
|
358 defEndLineno -= 1 |
|
359 |
|
360 # Use str.rfind() to account for annotations on the same line, |
|
361 # definition closure should be the last : on the line |
|
362 defEndColOffset = lines[defEndLineno - 1].rfind(":") |
|
363 |
|
364 return defEndLineno, defEndColOffset |
|
365 |
|
366 @staticmethod |
|
367 def _singleLineColonSeeker(node, line): |
|
368 """ |
|
369 Static method to find the line & column indices of a single line |
|
370 function definition. |
|
371 |
|
372 @param node reference to the function definition node |
|
373 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
374 @param line source code line |
|
375 @type str |
|
376 @return line and column offset of the colon |
|
377 @rtype tuple of (int, int) |
|
378 """ |
|
379 colStart = node.col_offset |
|
380 colEnd = node.body[0].col_offset |
|
381 defEndColOffset = line.rfind(":", colStart, colEnd) |
|
382 |
|
383 return node.lineno, defEndColOffset |
|
384 |
|
385 @staticmethod |
|
386 def tryTypeComment(funcObj, node): |
|
387 """ |
|
388 Static method to infer type hints from a function-level type comment. |
|
389 |
|
390 If a function is type commented it is assumed to have a return |
|
391 annotation, otherwise Python will fail to parse the hint. |
|
392 |
|
393 @param funcObj reference to the Function object |
|
394 @type Function |
|
395 @param node reference to the function definition node |
|
396 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
397 @return reference to the modified Function object |
|
398 @rtype Function |
|
399 """ |
|
400 hintTree = ast.parse(node.type_comment, "<func_type>", "func_type") |
|
401 hintTree = Function._maybeInjectClassArgument(hintTree, funcObj) |
|
402 |
|
403 for arg, hintComment in itertools.zip_longest( |
|
404 funcObj.args, hintTree.argtypes |
|
405 ): |
|
406 if isinstance(hintComment, ast.Ellipsis): |
|
407 continue |
|
408 |
|
409 if arg and hintComment: |
|
410 arg.hasTypeAnnotation = True |
|
411 arg.hasTypeComment = True |
|
412 |
|
413 # Return arg is always last |
|
414 funcObj.args[-1].hasTypeAnnotation = True |
|
415 funcObj.args[-1].hasTypeComment = True |
|
416 funcObj.isReturnAnnotated = True |
|
417 |
|
418 return funcObj |
|
419 |
|
420 @staticmethod |
|
421 def _maybeInjectClassArgument(hintTree, funcObj): |
|
422 """ |
|
423 Static method to inject `self` or `cls` args into a type comment to |
|
424 align with PEP 3107-style annotations. |
|
425 |
|
426 Because PEP 484 does not describe a method to provide partial function- |
|
427 level type comments, there is a potential for ambiguity in the context |
|
428 of both class methods and classmethods when aligning type comments to |
|
429 method arguments. |
|
430 |
|
431 These two class methods, for example, should lint equivalently: |
|
432 |
|
433 def bar(self, a): |
|
434 # type: (int) -> int |
|
435 ... |
|
436 |
|
437 def bar(self, a: int) -> int |
|
438 ... |
|
439 |
|
440 When this example type comment is parsed by `ast` and then matched with |
|
441 the method's arguments, it associates the `int` hint to `self` rather |
|
442 than `a`, so a dummy hint needs to be provided in situations where |
|
443 `self` or `class` are not hinted in the type comment in order to |
|
444 achieve equivalent linting results to PEP-3107 style annotations. |
|
445 |
|
446 A dummy `ast.Ellipses` constant is injected if the following criteria |
|
447 are met: |
|
448 1. The function node is either a class method or classmethod |
|
449 2. The number of hinted args is at least 1 less than the number |
|
450 of function args |
|
451 |
|
452 @param hintTree parsed type hint node |
|
453 @type ast.FunctionType |
|
454 @param funcObj reference to the Function object |
|
455 @type Function |
|
456 @return reference to the hint node |
|
457 @rtype ast.FunctionType |
|
458 """ |
|
459 if not funcObj.isClassMethod: |
|
460 # Short circuit |
|
461 return hintTree |
|
462 |
|
463 if ( |
|
464 funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and |
|
465 len(hintTree.argtypes) < (len(funcObj.args) - 1) |
|
466 ): |
|
467 # Subtract 1 to skip return arg |
|
468 hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes |
|
469 |
|
470 return hintTree |
|
471 |
|
472 @staticmethod |
|
473 def getFunctionType(functionName): |
|
474 """ |
|
475 Static method to determine the function's FunctionType from its name. |
|
476 |
|
477 MethodType is determined by the following priority: |
|
478 1. Special: function name prefixed & suffixed by "__" |
|
479 2. Private: function name prefixed by "__" |
|
480 3. Protected: function name prefixed by "_" |
|
481 4. Public: everything else |
|
482 |
|
483 @param functionName function name to be checked |
|
484 @type str |
|
485 @return type of function |
|
486 @rtype FunctionType |
|
487 """ |
|
488 if functionName.startswith("__") and functionName.endswith("__"): |
|
489 return FunctionType.SPECIAL |
|
490 elif functionName.startswith("__"): |
|
491 return FunctionType.PRIVATE |
|
492 elif functionName.startswith("_"): |
|
493 return FunctionType.PROTECTED |
|
494 else: |
|
495 return FunctionType.PUBLIC |
|
496 |
|
497 @staticmethod |
|
498 def getClassDecoratorType(functionNode): |
|
499 """ |
|
500 Static method to get the class method's decorator type from its |
|
501 function node. |
|
502 |
|
503 Only @classmethod and @staticmethod decorators are identified; all |
|
504 other decorators are ignored |
|
505 |
|
506 If @classmethod or @staticmethod decorators are not present, this |
|
507 function will return None. |
|
508 |
|
509 @param functionNode reference to the function definition node |
|
510 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
511 @return class decorator type |
|
512 @rtype ClassDecoratorType or None |
|
513 """ |
|
514 # @classmethod and @staticmethod will show up as ast.Name objects, |
|
515 # where callable decorators will show up as ast.Call, which we can |
|
516 # ignore |
|
517 decorators = [ |
|
518 decorator.id |
|
519 for decorator in functionNode.decorator_list |
|
520 if isinstance(decorator, ast.Name) |
|
521 ] |
|
522 |
|
523 if "classmethod" in decorators: |
|
524 return ClassDecoratorType.CLASSMETHOD |
|
525 elif "staticmethod" in decorators: |
|
526 return ClassDecoratorType.STATICMETHOD |
|
527 else: |
|
528 return None |
|
529 |
|
530 |
|
531 class FunctionVisitor(ast.NodeVisitor): |
|
532 """ |
|
533 Class implementing a node visitor to check function annotations. |
|
534 """ |
|
535 AstFuncTypes = (ast.FunctionDef, ast.AsyncFunctionDef) |
|
536 |
|
537 def __init__(self, lines): |
|
538 """ |
|
539 Constructor |
|
540 |
|
541 @param lines source code lines of the function |
|
542 @type list of str |
|
543 """ |
|
544 self.lines = lines |
|
545 self.functionDefinitions = [] |
|
546 self.__context = [] |
|
547 |
|
548 def switchContext(self, node): |
|
549 """ |
|
550 Public method implementing a context switcher as a generic function |
|
551 visitor in order to track function context. |
|
552 |
|
553 Without keeping track of context, it's challenging to reliably |
|
554 differentiate class methods from "regular" functions, especially in the |
|
555 case of nested classes. |
|
556 |
|
557 @param node reference to the function definition node to be analyzed |
|
558 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
559 """ |
|
560 if isinstance(node, FunctionVisitor.AstFuncTypes): |
|
561 # Check for non-empty context first to prevent IndexErrors for |
|
562 # non-nested nodes |
|
563 if self.__context: |
|
564 if isinstance(self.__context[-1], ast.ClassDef): |
|
565 # Check if current context is a ClassDef node & pass the |
|
566 # appropriate flag |
|
567 self.functionDefinitions.append( |
|
568 Function.fromNode(node, self.lines, isClassMethod=True) |
|
569 ) |
|
570 elif isinstance( |
|
571 self.__context[-1], FunctionVisitor.AstFuncTypes |
|
572 ): |
|
573 # Check for nested function & pass the appropriate flag |
|
574 self.functionDefinitions.append( |
|
575 Function.fromNode(node, self.lines, isNested=True) |
|
576 ) |
|
577 else: |
|
578 self.functionDefinitions.append( |
|
579 Function.fromNode(node, self.lines)) |
|
580 |
|
581 self.__context.append(node) |
|
582 self.generic_visit(node) |
|
583 self.__context.pop() |
|
584 |
|
585 visit_FunctionDef = switchContext |
|
586 visit_AsyncFunctionDef = switchContext |
|
587 visit_ClassDef = switchContext |
|
588 |
|
589 |
|
590 class ReturnVisitor(ast.NodeVisitor): |
|
591 """ |
|
592 Class implementing a node visitor to check the return statements of a |
|
593 function node. |
|
594 |
|
595 If the function node being visited has an explicit return statement of |
|
596 anything other than `None`, the `instance.hasOnlyNoneReturns` flag will |
|
597 be set to `False`. |
|
598 |
|
599 If the function node being visited has no return statement, or contains |
|
600 only return statement(s) that explicitly return `None`, the |
|
601 `instance.hasOnlyNoneReturns` flag will be set to `True`. |
|
602 |
|
603 Due to the generic visiting being done, we need to keep track of the |
|
604 context in which a non-`None` return node is found. These functions are |
|
605 added to a set that is checked to see whether nor not the parent node is |
|
606 present. |
|
607 """ |
|
608 def __init__(self, parentNode): |
|
609 """ |
|
610 Constructor |
|
611 |
|
612 @param parentNode reference to the function definition node to be |
|
613 analyzed |
|
614 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
615 """ |
|
616 self.parentNode = parentNode |
|
617 self.__context = [] |
|
618 self.__nonNoneReturnNodes = set() |
|
619 |
|
620 @property |
|
621 def hasOnlyNoneReturns(self): |
|
622 """ |
|
623 Public method indicating, that the parent node isn't in the visited |
|
624 nodes that don't return `None`. |
|
625 |
|
626 @return flag indicating, that the parent node isn't in the visited |
|
627 nodes that don't return `None` |
|
628 @rtype bool |
|
629 """ |
|
630 return self.parentNode not in self.__nonNoneReturnNodes |
|
631 |
|
632 def visit_Return(self, node): |
|
633 """ |
|
634 Public method to check each Return node to see if it returns anything |
|
635 other than `None`. |
|
636 |
|
637 If the node being visited returns anything other than `None`, its |
|
638 parent context is added to the set of non-returning child nodes of |
|
639 the parent node. |
|
640 |
|
641 @param node reference to the AST Return node |
|
642 @type ast.Return |
|
643 """ |
|
644 if node.value is not None: |
|
645 # In the event of an explicit `None` return (`return None`), the |
|
646 # node body will be an instance of either `ast.Constant` (3.8+) or |
|
647 # `ast.NameConstant`, which we need to check to see if it's |
|
648 # actually `None` |
|
649 if ( |
|
650 isinstance(node.value, (ast.Constant, ast.NameConstant)) and |
|
651 node.value.value is None |
|
652 ): |
|
653 return |
|
654 |
|
655 self.__nonNoneReturnNodes.add(self.__context[-1]) |
|
656 |
|
657 def switchContext(self, node): |
|
658 """ |
|
659 Public method implementing a context switcher as a generic function |
|
660 visitor in order to track function context. |
|
661 |
|
662 Without keeping track of context, it's challenging to reliably |
|
663 differentiate class methods from "regular" functions, especially in the |
|
664 case of nested classes. |
|
665 |
|
666 @param node reference to the function definition node to be analyzed |
|
667 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
668 """ |
|
669 self.__context.append(node) |
|
670 self.generic_visit(node) |
|
671 self.__context.pop() |
|
672 |
|
673 visit_FunctionDef = switchContext |
|
674 visit_AsyncFunctionDef = switchContext |