136 self.hasTypeComment = hasTypeComment |
155 self.hasTypeComment = hasTypeComment |
137 self.hasOnlyNoneReturns = hasOnlyNoneReturns |
156 self.hasOnlyNoneReturns = hasOnlyNoneReturns |
138 self.isNested = isNested |
157 self.isNested = isNested |
139 self.decoratorList = decoratorList |
158 self.decoratorList = decoratorList |
140 self.args = args |
159 self.args = args |
141 |
160 |
142 def isFullyAnnotated(self): |
161 def isFullyAnnotated(self): |
143 """ |
162 """ |
144 Public method to check, if the function definition is fully type |
163 Public method to check, if the function definition is fully type |
145 annotated. |
164 annotated. |
146 |
165 |
147 Note: self.args will always include an Argument object for return. |
166 Note: self.args will always include an Argument object for return. |
148 |
167 |
149 @return flag indicating a fully annotated function definition |
168 @return flag indicating a fully annotated function definition |
150 @rtype bool |
169 @rtype bool |
151 """ |
170 """ |
152 return all(arg.hasTypeAnnotation for arg in self.args) |
171 return all(arg.hasTypeAnnotation for arg in self.args) |
153 |
172 |
154 def isDynamicallyTyped(self): |
173 def isDynamicallyTyped(self): |
155 """ |
174 """ |
156 Public method to check, if a function definition is dynamically typed |
175 Public method to check, if a function definition is dynamically typed |
157 (i.e. completely lacking hints). |
176 (i.e. completely lacking hints). |
158 |
177 |
159 @return flag indicating a dynamically typed function definition |
178 @return flag indicating a dynamically typed function definition |
160 @rtype bool |
179 @rtype bool |
161 """ |
180 """ |
162 return not any(arg.hasTypeAnnotation for arg in self.args) |
181 return not any(arg.hasTypeAnnotation for arg in self.args) |
163 |
182 |
164 def getMissedAnnotations(self): |
183 def getMissedAnnotations(self): |
165 """ |
184 """ |
166 Public method to provide a list of arguments with missing type |
185 Public method to provide a list of arguments with missing type |
167 annotations. |
186 annotations. |
168 |
187 |
169 @return list of arguments with missing type annotations |
188 @return list of arguments with missing type annotations |
170 @rtype list of Argument |
189 @rtype list of Argument |
171 """ |
190 """ |
172 return [arg for arg in self.args if not arg.hasTypeAnnotation] |
191 return [arg for arg in self.args if not arg.hasTypeAnnotation] |
173 |
192 |
174 def getAnnotatedArguments(self): |
193 def getAnnotatedArguments(self): |
175 """ |
194 """ |
176 Public method to get list of arguments with type annotations. |
195 Public method to get list of arguments with type annotations. |
177 |
196 |
178 @return list of arguments with type annotations. |
197 @return list of arguments with type annotations. |
179 @rtype list of Argument |
198 @rtype list of Argument |
180 """ |
199 """ |
181 return [arg for arg in self.args if arg.hasTypeAnnotation] |
200 return [arg for arg in self.args if arg.hasTypeAnnotation] |
182 |
201 |
183 def hasDecorator(self, checkDecorators): |
202 def hasDecorator(self, checkDecorators): |
184 """ |
203 """ |
185 Public method to check whether the function node is decorated by any of |
204 Public method to check whether the function node is decorated by any of |
186 the provided decorators. |
205 the provided decorators. |
187 |
206 |
188 Decorator matching is done against the provided `checkDecorators` set. |
207 Decorator matching is done against the provided `checkDecorators` set. |
189 Decorators are assumed to be either a module attribute (e.g. |
208 Decorators are assumed to be either a module attribute (e.g. |
190 `@typing.overload`) or name (e.g. `@overload`). For the case of a |
209 `@typing.overload`) or name (e.g. `@overload`). For the case of a |
191 module attribute, only the attribute is checked against |
210 module attribute, only the attribute is checked against |
192 `overload_decorators`. |
211 `overload_decorators`. |
193 |
212 |
194 Note: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly |
213 Note: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly |
195 supported. |
214 supported. |
196 |
215 |
197 @param checkDecorators set of decorators to check against |
216 @param checkDecorators set of decorators to check against |
198 @type set of str |
217 @type set of str |
199 @return flag indicating the presence of any decorators |
218 @return flag indicating the presence of any decorators |
200 @rtype bool |
219 @rtype bool |
201 """ |
220 """ |
203 # Drop to a helper to allow for simpler handling of callable |
222 # Drop to a helper to allow for simpler handling of callable |
204 # decorators |
223 # decorators |
205 return self.__decoratorChecker(decorator, checkDecorators) |
224 return self.__decoratorChecker(decorator, checkDecorators) |
206 else: |
225 else: |
207 return False |
226 return False |
208 |
227 |
209 def __decoratorChecker(self, decorator, checkDecorators): |
228 def __decoratorChecker(self, decorator, checkDecorators): |
210 """ |
229 """ |
211 Private method to check the provided decorator for a match against the |
230 Private method to check the provided decorator for a match against the |
212 provided set of check names. |
231 provided set of check names. |
213 |
232 |
214 Decorators are assumed to be of the following form: |
233 Decorators are assumed to be of the following form: |
215 * `a.name` or `a.name()` |
234 * `a.name` or `a.name()` |
216 * `name` or `name()` |
235 * `name` or `name()` |
217 |
236 |
218 Note: Deeper imports (e.g. `a.b.name`) are not explicitly supported. |
237 Note: Deeper imports (e.g. `a.b.name`) are not explicitly supported. |
219 |
238 |
220 @param decorator decorator node to check |
239 @param decorator decorator node to check |
221 @type ast.Attribute, ast.Call or ast.Name |
240 @type ast.Attribute, ast.Call or ast.Name |
222 @param checkDecorators set of decorators to check against |
241 @param checkDecorators set of decorators to check against |
223 @type set of str |
242 @type set of str |
224 @return flag indicating the presence of any decorators |
243 @return flag indicating the presence of any decorators |
235 elif isinstance(decorator, ast.Call): |
254 elif isinstance(decorator, ast.Call): |
236 # e.g. `@overload()` or `@typing.overload()`, where |
255 # e.g. `@overload()` or `@typing.overload()`, where |
237 # `decorator.func` will be `ast.Name` or `ast.Attribute`, |
256 # `decorator.func` will be `ast.Name` or `ast.Attribute`, |
238 # which we can check recursively |
257 # which we can check recursively |
239 return self.__decoratorChecker(decorator.func, checkDecorators) |
258 return self.__decoratorChecker(decorator.func, checkDecorators) |
240 |
259 |
241 return None |
260 return None |
242 |
261 |
243 @classmethod |
262 @classmethod |
244 def fromNode(cls, node, lines, **kwargs): |
263 def fromNode(cls, node, lines, **kwargs): |
245 """ |
264 """ |
246 Class method to create a Function object from ast.FunctionDef or |
265 Class method to create a Function object from ast.FunctionDef or |
247 ast.AsyncFunctionDef nodes. |
266 ast.AsyncFunctionDef nodes. |
248 |
267 |
249 Accept the source code, as a list of strings, in order to get the |
268 Accept the source code, as a list of strings, in order to get the |
250 column where the function definition ends. |
269 column where the function definition ends. |
251 |
270 |
252 With exceptions, input kwargs are passed straight through to Function's |
271 With exceptions, input kwargs are passed straight through to Function's |
253 __init__. The following kwargs will be overridden: |
272 __init__. The following kwargs will be overridden: |
254 * function_type |
273 * function_type |
255 * class_decorator_type |
274 * class_decorator_type |
256 * args |
275 * args |
257 |
276 |
258 @param node reference to the function definition node |
277 @param node reference to the function definition node |
259 @type ast.AsyncFunctionDef or ast.FunctionDef |
278 @type ast.AsyncFunctionDef or ast.FunctionDef |
260 @param lines list of source code lines |
279 @param lines list of source code lines |
261 @type list of str |
280 @type list of str |
262 @keyparam **kwargs keyword arguments |
281 @keyparam **kwargs keyword arguments |
264 @return created Function object |
283 @return created Function object |
265 @rtype Function |
284 @rtype Function |
266 """ |
285 """ |
267 # Extract function types from function name |
286 # Extract function types from function name |
268 kwargs["functionType"] = cls.getFunctionType(node.name) |
287 kwargs["functionType"] = cls.getFunctionType(node.name) |
269 |
288 |
270 # Identify type of class method, if applicable |
289 # Identify type of class method, if applicable |
271 if kwargs.get("isClassMethod", False): |
290 if kwargs.get("isClassMethod", False): |
272 kwargs["classDecoratorType"] = cls.getClassDecoratorType(node) |
291 kwargs["classDecoratorType"] = cls.getClassDecoratorType(node) |
273 |
292 |
274 # Store raw decorator list for use by property methods |
293 # Store raw decorator list for use by property methods |
275 kwargs["decoratorList"] = node.decorator_list |
294 kwargs["decoratorList"] = node.decorator_list |
276 |
295 |
277 # Instantiate empty args list here since it has no default |
296 # Instantiate empty args list here since it has no default |
278 kwargs["args"] = [] |
297 kwargs["args"] = [] |
279 |
298 |
280 newFunction = cls(node.name, node.lineno, node.col_offset, **kwargs) |
299 newFunction = cls(node.name, node.lineno, node.col_offset, **kwargs) |
281 |
300 |
282 # Iterate over arguments by type & add |
301 # Iterate over arguments by type & add |
283 for argType in AST_ARG_TYPES: |
302 for argType in AST_ARG_TYPES: |
284 args = node.args.__getattribute__(argType) |
303 args = node.args.__getattribute__(argType) |
285 if args: |
304 if args: |
286 if not isinstance(args, list): |
305 if not isinstance(args, list): |
287 args = [args] |
306 args = [args] |
288 |
307 |
289 newFunction.args.extend( |
308 newFunction.args.extend( |
290 [Argument.fromNode(arg, argType.upper()) |
309 [Argument.fromNode(arg, argType.upper()) for arg in args] |
291 for arg in args] |
|
292 ) |
310 ) |
293 |
311 |
294 # Create an Argument object for the return hint |
312 # Create an Argument object for the return hint |
295 defEndLineno, defEndColOffset = cls.colonSeeker(node, lines) |
313 defEndLineno, defEndColOffset = cls.colonSeeker(node, lines) |
296 returnArg = Argument("return", defEndLineno, defEndColOffset, |
314 returnArg = Argument( |
297 AnnotationType.RETURN) |
315 "return", defEndLineno, defEndColOffset, AnnotationType.RETURN |
|
316 ) |
298 if node.returns: |
317 if node.returns: |
299 returnArg.hasTypeAnnotation = True |
318 returnArg.hasTypeAnnotation = True |
300 returnArg.has3107Annotation = True |
319 returnArg.has3107Annotation = True |
301 newFunction.isReturnAnnotated = True |
320 newFunction.isReturnAnnotated = True |
302 |
321 |
303 newFunction.args.append(returnArg) |
322 newFunction.args.append(returnArg) |
304 |
323 |
305 # Type comments in-line with input arguments are handled by the |
324 # Type comments in-line with input arguments are handled by the |
306 # Argument class. If a function-level type comment is present, attempt |
325 # Argument class. If a function-level type comment is present, attempt |
307 # to parse for any missed type hints. |
326 # to parse for any missed type hints. |
308 if node.type_comment: |
327 if node.type_comment: |
309 newFunction.hasTypeComment = True |
328 newFunction.hasTypeComment = True |
310 newFunction = cls.tryTypeComment(newFunction, node) |
329 newFunction = cls.tryTypeComment(newFunction, node) |
311 |
330 |
312 # Check for the presence of non-`None` returns using the special-case |
331 # Check for the presence of non-`None` returns using the special-case |
313 # return node visitor. |
332 # return node visitor. |
314 returnVisitor = ReturnVisitor(node) |
333 returnVisitor = ReturnVisitor(node) |
315 returnVisitor.visit(node) |
334 returnVisitor.visit(node) |
316 newFunction.hasOnlyNoneReturns = returnVisitor.hasOnlyNoneReturns |
335 newFunction.hasOnlyNoneReturns = returnVisitor.hasOnlyNoneReturns |
317 |
336 |
318 return newFunction |
337 return newFunction |
319 |
338 |
320 @staticmethod |
339 @staticmethod |
321 def colonSeeker(node, lines): |
340 def colonSeeker(node, lines): |
322 """ |
341 """ |
323 Static method to find the line & column indices of the function |
342 Static method to find the line & column indices of the function |
324 definition's closing colon. |
343 definition's closing colon. |
325 |
344 |
326 @param node reference to the function definition node |
345 @param node reference to the function definition node |
327 @type ast.AsyncFunctionDef or ast.FunctionDef |
346 @type ast.AsyncFunctionDef or ast.FunctionDef |
328 @param lines list of source code lines |
347 @param lines list of source code lines |
329 @type list of str |
348 @type list of str |
330 @return line and column offset of the colon |
349 @return line and column offset of the colon |
331 @rtype tuple of (int, int) |
350 @rtype tuple of (int, int) |
332 """ |
351 """ |
333 # Special case single line function definitions |
352 # Special case single line function definitions |
334 if node.lineno == node.body[0].lineno: |
353 if node.lineno == node.body[0].lineno: |
335 return Function._singleLineColonSeeker( |
354 return Function._singleLineColonSeeker(node, lines[node.lineno - 1]) |
336 node, lines[node.lineno - 1]) |
355 |
337 |
|
338 # With Python < 3.8, the function node includes the docstring and the |
356 # 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 |
357 # 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 |
358 # present, before looking for the def colon. We should end up with |
341 # lines[defEndLineno - 1] having the colon. |
359 # lines[defEndLineno - 1] having the colon. |
342 defEndLineno = node.body[0].lineno |
360 defEndLineno = node.body[0].lineno |
349 while True: |
367 while True: |
350 defEndLineno -= 1 |
368 defEndLineno -= 1 |
351 if '"""' in lines[defEndLineno - 1]: |
369 if '"""' in lines[defEndLineno - 1]: |
352 # Docstring has closed |
370 # Docstring has closed |
353 break |
371 break |
354 |
372 |
355 # Once we've gotten here, we've found the line where the docstring |
373 # 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 |
374 # begins, so we have to step up one more line to get to the close of |
357 # the def. |
375 # the def. |
358 defEndLineno -= 1 |
376 defEndLineno -= 1 |
359 |
377 |
360 # Use str.rfind() to account for annotations on the same line, |
378 # Use str.rfind() to account for annotations on the same line, |
361 # definition closure should be the last : on the line |
379 # definition closure should be the last : on the line |
362 defEndColOffset = lines[defEndLineno - 1].rfind(":") |
380 defEndColOffset = lines[defEndLineno - 1].rfind(":") |
363 |
381 |
364 return defEndLineno, defEndColOffset |
382 return defEndLineno, defEndColOffset |
365 |
383 |
366 @staticmethod |
384 @staticmethod |
367 def _singleLineColonSeeker(node, line): |
385 def _singleLineColonSeeker(node, line): |
368 """ |
386 """ |
369 Static method to find the line & column indices of a single line |
387 Static method to find the line & column indices of a single line |
370 function definition. |
388 function definition. |
371 |
389 |
372 @param node reference to the function definition node |
390 @param node reference to the function definition node |
373 @type ast.AsyncFunctionDef or ast.FunctionDef |
391 @type ast.AsyncFunctionDef or ast.FunctionDef |
374 @param line source code line |
392 @param line source code line |
375 @type str |
393 @type str |
376 @return line and column offset of the colon |
394 @return line and column offset of the colon |
377 @rtype tuple of (int, int) |
395 @rtype tuple of (int, int) |
378 """ |
396 """ |
379 colStart = node.col_offset |
397 colStart = node.col_offset |
380 colEnd = node.body[0].col_offset |
398 colEnd = node.body[0].col_offset |
381 defEndColOffset = line.rfind(":", colStart, colEnd) |
399 defEndColOffset = line.rfind(":", colStart, colEnd) |
382 |
400 |
383 return node.lineno, defEndColOffset |
401 return node.lineno, defEndColOffset |
384 |
402 |
385 @staticmethod |
403 @staticmethod |
386 def tryTypeComment(funcObj, node): |
404 def tryTypeComment(funcObj, node): |
387 """ |
405 """ |
388 Static method to infer type hints from a function-level type comment. |
406 Static method to infer type hints from a function-level type comment. |
389 |
407 |
390 If a function is type commented it is assumed to have a return |
408 If a function is type commented it is assumed to have a return |
391 annotation, otherwise Python will fail to parse the hint. |
409 annotation, otherwise Python will fail to parse the hint. |
392 |
410 |
393 @param funcObj reference to the Function object |
411 @param funcObj reference to the Function object |
394 @type Function |
412 @type Function |
395 @param node reference to the function definition node |
413 @param node reference to the function definition node |
396 @type ast.AsyncFunctionDef or ast.FunctionDef |
414 @type ast.AsyncFunctionDef or ast.FunctionDef |
397 @return reference to the modified Function object |
415 @return reference to the modified Function object |
398 @rtype Function |
416 @rtype Function |
399 """ |
417 """ |
400 hintTree = ast.parse(node.type_comment, "<func_type>", "func_type") |
418 hintTree = ast.parse(node.type_comment, "<func_type>", "func_type") |
401 hintTree = Function._maybeInjectClassArgument(hintTree, funcObj) |
419 hintTree = Function._maybeInjectClassArgument(hintTree, funcObj) |
402 |
420 |
403 for arg, hintComment in itertools.zip_longest( |
421 for arg, hintComment in itertools.zip_longest(funcObj.args, hintTree.argtypes): |
404 funcObj.args, hintTree.argtypes |
|
405 ): |
|
406 if isinstance(hintComment, ast.Ellipsis): |
422 if isinstance(hintComment, ast.Ellipsis): |
407 continue |
423 continue |
408 |
424 |
409 if arg and hintComment: |
425 if arg and hintComment: |
410 arg.hasTypeAnnotation = True |
426 arg.hasTypeAnnotation = True |
411 arg.hasTypeComment = True |
427 arg.hasTypeComment = True |
412 |
428 |
413 # Return arg is always last |
429 # Return arg is always last |
414 funcObj.args[-1].hasTypeAnnotation = True |
430 funcObj.args[-1].hasTypeAnnotation = True |
415 funcObj.args[-1].hasTypeComment = True |
431 funcObj.args[-1].hasTypeComment = True |
416 funcObj.isReturnAnnotated = True |
432 funcObj.isReturnAnnotated = True |
417 |
433 |
418 return funcObj |
434 return funcObj |
419 |
435 |
420 @staticmethod |
436 @staticmethod |
421 def _maybeInjectClassArgument(hintTree, funcObj): |
437 def _maybeInjectClassArgument(hintTree, funcObj): |
422 """ |
438 """ |
423 Static method to inject `self` or `cls` args into a type comment to |
439 Static method to inject `self` or `cls` args into a type comment to |
424 align with PEP 3107-style annotations. |
440 align with PEP 3107-style annotations. |
425 |
441 |
426 Because PEP 484 does not describe a method to provide partial function- |
442 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 |
443 level type comments, there is a potential for ambiguity in the context |
428 of both class methods and classmethods when aligning type comments to |
444 of both class methods and classmethods when aligning type comments to |
429 method arguments. |
445 method arguments. |
430 |
446 |
431 These two class methods, for example, should lint equivalently: |
447 These two class methods, for example, should lint equivalently: |
432 |
448 |
433 def bar(self, a): |
449 def bar(self, a): |
434 # type: (int) -> int |
450 # type: (int) -> int |
435 ... |
451 ... |
436 |
452 |
437 def bar(self, a: int) -> int |
453 def bar(self, a: int) -> int |
438 ... |
454 ... |
439 |
455 |
440 When this example type comment is parsed by `ast` and then matched with |
456 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 |
457 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 |
458 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 |
459 `self` or `class` are not hinted in the type comment in order to |
444 achieve equivalent linting results to PEP-3107 style annotations. |
460 achieve equivalent linting results to PEP-3107 style annotations. |
445 |
461 |
446 A dummy `ast.Ellipses` constant is injected if the following criteria |
462 A dummy `ast.Ellipses` constant is injected if the following criteria |
447 are met: |
463 are met: |
448 1. The function node is either a class method or classmethod |
464 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 |
465 2. The number of hinted args is at least 1 less than the number |
450 of function args |
466 of function args |
451 |
467 |
452 @param hintTree parsed type hint node |
468 @param hintTree parsed type hint node |
453 @type ast.FunctionType |
469 @type ast.FunctionType |
454 @param funcObj reference to the Function object |
470 @param funcObj reference to the Function object |
455 @type Function |
471 @type Function |
456 @return reference to the hint node |
472 @return reference to the hint node |
491 return FunctionType.PRIVATE |
506 return FunctionType.PRIVATE |
492 elif functionName.startswith("_"): |
507 elif functionName.startswith("_"): |
493 return FunctionType.PROTECTED |
508 return FunctionType.PROTECTED |
494 else: |
509 else: |
495 return FunctionType.PUBLIC |
510 return FunctionType.PUBLIC |
496 |
511 |
497 @staticmethod |
512 @staticmethod |
498 def getClassDecoratorType(functionNode): |
513 def getClassDecoratorType(functionNode): |
499 """ |
514 """ |
500 Static method to get the class method's decorator type from its |
515 Static method to get the class method's decorator type from its |
501 function node. |
516 function node. |
502 |
517 |
503 Only @classmethod and @staticmethod decorators are identified; all |
518 Only @classmethod and @staticmethod decorators are identified; all |
504 other decorators are ignored |
519 other decorators are ignored |
505 |
520 |
506 If @classmethod or @staticmethod decorators are not present, this |
521 If @classmethod or @staticmethod decorators are not present, this |
507 function will return None. |
522 function will return None. |
508 |
523 |
509 @param functionNode reference to the function definition node |
524 @param functionNode reference to the function definition node |
510 @type ast.AsyncFunctionDef or ast.FunctionDef |
525 @type ast.AsyncFunctionDef or ast.FunctionDef |
511 @return class decorator type |
526 @return class decorator type |
512 @rtype ClassDecoratorType or None |
527 @rtype ClassDecoratorType or None |
513 """ |
528 """ |
530 |
545 |
531 class FunctionVisitor(ast.NodeVisitor): |
546 class FunctionVisitor(ast.NodeVisitor): |
532 """ |
547 """ |
533 Class implementing a node visitor to check function annotations. |
548 Class implementing a node visitor to check function annotations. |
534 """ |
549 """ |
|
550 |
535 AstFuncTypes = (ast.FunctionDef, ast.AsyncFunctionDef) |
551 AstFuncTypes = (ast.FunctionDef, ast.AsyncFunctionDef) |
536 |
552 |
537 def __init__(self, lines): |
553 def __init__(self, lines): |
538 """ |
554 """ |
539 Constructor |
555 Constructor |
540 |
556 |
541 @param lines source code lines of the function |
557 @param lines source code lines of the function |
542 @type list of str |
558 @type list of str |
543 """ |
559 """ |
544 self.lines = lines |
560 self.lines = lines |
545 self.functionDefinitions = [] |
561 self.functionDefinitions = [] |
546 self.__context = [] |
562 self.__context = [] |
547 |
563 |
548 def switchContext(self, node): |
564 def switchContext(self, node): |
549 """ |
565 """ |
550 Public method implementing a context switcher as a generic function |
566 Public method implementing a context switcher as a generic function |
551 visitor in order to track function context. |
567 visitor in order to track function context. |
552 |
568 |
553 Without keeping track of context, it's challenging to reliably |
569 Without keeping track of context, it's challenging to reliably |
554 differentiate class methods from "regular" functions, especially in the |
570 differentiate class methods from "regular" functions, especially in the |
555 case of nested classes. |
571 case of nested classes. |
556 |
572 |
557 @param node reference to the function definition node to be analyzed |
573 @param node reference to the function definition node to be analyzed |
558 @type ast.AsyncFunctionDef or ast.FunctionDef |
574 @type ast.AsyncFunctionDef or ast.FunctionDef |
559 """ |
575 """ |
560 if isinstance(node, FunctionVisitor.AstFuncTypes): |
576 if isinstance(node, FunctionVisitor.AstFuncTypes): |
561 # Check for non-empty context first to prevent IndexErrors for |
577 # Check for non-empty context first to prevent IndexErrors for |
565 # Check if current context is a ClassDef node & pass the |
581 # Check if current context is a ClassDef node & pass the |
566 # appropriate flag |
582 # appropriate flag |
567 self.functionDefinitions.append( |
583 self.functionDefinitions.append( |
568 Function.fromNode(node, self.lines, isClassMethod=True) |
584 Function.fromNode(node, self.lines, isClassMethod=True) |
569 ) |
585 ) |
570 elif isinstance( |
586 elif isinstance(self.__context[-1], FunctionVisitor.AstFuncTypes): |
571 self.__context[-1], FunctionVisitor.AstFuncTypes |
|
572 ): |
|
573 # Check for nested function & pass the appropriate flag |
587 # Check for nested function & pass the appropriate flag |
574 self.functionDefinitions.append( |
588 self.functionDefinitions.append( |
575 Function.fromNode(node, self.lines, isNested=True) |
589 Function.fromNode(node, self.lines, isNested=True) |
576 ) |
590 ) |
577 else: |
591 else: |
578 self.functionDefinitions.append( |
592 self.functionDefinitions.append(Function.fromNode(node, self.lines)) |
579 Function.fromNode(node, self.lines)) |
593 |
580 |
|
581 self.__context.append(node) |
594 self.__context.append(node) |
582 self.generic_visit(node) |
595 self.generic_visit(node) |
583 self.__context.pop() |
596 self.__context.pop() |
584 |
597 |
585 visit_FunctionDef = switchContext |
598 visit_FunctionDef = switchContext |
586 visit_AsyncFunctionDef = switchContext |
599 visit_AsyncFunctionDef = switchContext |
587 visit_ClassDef = switchContext |
600 visit_ClassDef = switchContext |
588 |
601 |
589 |
602 |
590 class ReturnVisitor(ast.NodeVisitor): |
603 class ReturnVisitor(ast.NodeVisitor): |
591 """ |
604 """ |
592 Class implementing a node visitor to check the return statements of a |
605 Class implementing a node visitor to check the return statements of a |
593 function node. |
606 function node. |
594 |
607 |
595 If the function node being visited has an explicit return statement of |
608 If the function node being visited has an explicit return statement of |
596 anything other than `None`, the `instance.hasOnlyNoneReturns` flag will |
609 anything other than `None`, the `instance.hasOnlyNoneReturns` flag will |
597 be set to `False`. |
610 be set to `False`. |
598 |
611 |
599 If the function node being visited has no return statement, or contains |
612 If the function node being visited has no return statement, or contains |
600 only return statement(s) that explicitly return `None`, the |
613 only return statement(s) that explicitly return `None`, the |
601 `instance.hasOnlyNoneReturns` flag will be set to `True`. |
614 `instance.hasOnlyNoneReturns` flag will be set to `True`. |
602 |
615 |
603 Due to the generic visiting being done, we need to keep track of the |
616 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 |
617 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 |
618 added to a set that is checked to see whether nor not the parent node is |
606 present. |
619 present. |
607 """ |
620 """ |
|
621 |
608 def __init__(self, parentNode): |
622 def __init__(self, parentNode): |
609 """ |
623 """ |
610 Constructor |
624 Constructor |
611 |
625 |
612 @param parentNode reference to the function definition node to be |
626 @param parentNode reference to the function definition node to be |
613 analyzed |
627 analyzed |
614 @type ast.AsyncFunctionDef or ast.FunctionDef |
628 @type ast.AsyncFunctionDef or ast.FunctionDef |
615 """ |
629 """ |
616 self.parentNode = parentNode |
630 self.parentNode = parentNode |
617 self.__context = [] |
631 self.__context = [] |
618 self.__nonNoneReturnNodes = set() |
632 self.__nonNoneReturnNodes = set() |
619 |
633 |
620 @property |
634 @property |
621 def hasOnlyNoneReturns(self): |
635 def hasOnlyNoneReturns(self): |
622 """ |
636 """ |
623 Public method indicating, that the parent node isn't in the visited |
637 Public method indicating, that the parent node isn't in the visited |
624 nodes that don't return `None`. |
638 nodes that don't return `None`. |
625 |
639 |
626 @return flag indicating, that the parent node isn't in the visited |
640 @return flag indicating, that the parent node isn't in the visited |
627 nodes that don't return `None` |
641 nodes that don't return `None` |
628 @rtype bool |
642 @rtype bool |
629 """ |
643 """ |
630 return self.parentNode not in self.__nonNoneReturnNodes |
644 return self.parentNode not in self.__nonNoneReturnNodes |
631 |
645 |
632 def visit_Return(self, node): |
646 def visit_Return(self, node): |
633 """ |
647 """ |
634 Public method to check each Return node to see if it returns anything |
648 Public method to check each Return node to see if it returns anything |
635 other than `None`. |
649 other than `None`. |
636 |
650 |
637 If the node being visited returns anything other than `None`, its |
651 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 |
652 parent context is added to the set of non-returning child nodes of |
639 the parent node. |
653 the parent node. |
640 |
654 |
641 @param node reference to the AST Return node |
655 @param node reference to the AST Return node |
642 @type ast.Return |
656 @type ast.Return |
643 """ |
657 """ |
644 if node.value is not None: |
658 if node.value is not None: |
645 # In the event of an explicit `None` return (`return None`), the |
659 # 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 |
660 # 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 |
661 # `ast.NameConstant`, which we need to check to see if it's |
648 # actually `None` |
662 # actually `None` |
649 if ( |
663 if ( |
650 isinstance(node.value, (ast.Constant, ast.NameConstant)) and |
664 isinstance(node.value, (ast.Constant, ast.NameConstant)) |
651 node.value.value is None |
665 and node.value.value is None |
652 ): |
666 ): |
653 return |
667 return |
654 |
668 |
655 self.__nonNoneReturnNodes.add(self.__context[-1]) |
669 self.__nonNoneReturnNodes.add(self.__context[-1]) |
656 |
670 |
657 def switchContext(self, node): |
671 def switchContext(self, node): |
658 """ |
672 """ |
659 Public method implementing a context switcher as a generic function |
673 Public method implementing a context switcher as a generic function |
660 visitor in order to track function context. |
674 visitor in order to track function context. |
661 |
675 |
662 Without keeping track of context, it's challenging to reliably |
676 Without keeping track of context, it's challenging to reliably |
663 differentiate class methods from "regular" functions, especially in the |
677 differentiate class methods from "regular" functions, especially in the |
664 case of nested classes. |
678 case of nested classes. |
665 |
679 |
666 @param node reference to the function definition node to be analyzed |
680 @param node reference to the function definition node to be analyzed |
667 @type ast.AsyncFunctionDef or ast.FunctionDef |
681 @type ast.AsyncFunctionDef or ast.FunctionDef |
668 """ |
682 """ |
669 self.__context.append(node) |
683 self.__context.append(node) |
670 self.generic_visit(node) |
684 self.generic_visit(node) |