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

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9276
e6748a5e24b9
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
23 23
24 class Argument: 24 class Argument:
25 """ 25 """
26 Class representing a function argument. 26 Class representing a function argument.
27 """ 27 """
28 def __init__(self, argname, lineno, col_offset, annotationType, 28
29 hasTypeAnnotation=False, has3107Annotation=False, 29 def __init__(
30 hasTypeComment=False): 30 self,
31 argname,
32 lineno,
33 col_offset,
34 annotationType,
35 hasTypeAnnotation=False,
36 has3107Annotation=False,
37 hasTypeComment=False,
38 ):
31 """ 39 """
32 Constructor 40 Constructor
33 41
34 @param argname name of the argument 42 @param argname name of the argument
35 @type str 43 @type str
36 @param lineno line number 44 @param lineno line number
37 @type int 45 @type int
38 @param col_offset column number 46 @param col_offset column number
54 self.col_offset = col_offset 62 self.col_offset = col_offset
55 self.annotationType = annotationType 63 self.annotationType = annotationType
56 self.hasTypeAnnotation = hasTypeAnnotation 64 self.hasTypeAnnotation = hasTypeAnnotation
57 self.has3107Annotation = has3107Annotation 65 self.has3107Annotation = has3107Annotation
58 self.hasTypeComment = hasTypeComment 66 self.hasTypeComment = hasTypeComment
59 67
60 @classmethod 68 @classmethod
61 def fromNode(cls, node, annotationTypeName): 69 def fromNode(cls, node, annotationTypeName):
62 """ 70 """
63 Class method to create an Argument object based on the given node. 71 Class method to create an Argument object based on the given node.
64 72
65 @param node reference to the node to be converted 73 @param node reference to the node to be converted
66 @type ast.arguments 74 @type ast.arguments
67 @param annotationTypeName name of the annotation type 75 @param annotationTypeName name of the annotation type
68 @type str 76 @type str
69 @return Argument object 77 @return Argument object
86 94
87 class Function: 95 class Function:
88 """ 96 """
89 Class representing a function. 97 Class representing a function.
90 """ 98 """
91 def __init__(self, name, lineno, col_offset, 99
92 functionType=FunctionType.PUBLIC, isClassMethod=False, 100 def __init__(
93 classDecoratorType=None, isReturnAnnotated=False, 101 self,
94 hasTypeComment=False, hasOnlyNoneReturns=True, 102 name,
95 isNested=False, decoratorList=None, args=None): 103 lineno,
104 col_offset,
105 functionType=FunctionType.PUBLIC,
106 isClassMethod=False,
107 classDecoratorType=None,
108 isReturnAnnotated=False,
109 hasTypeComment=False,
110 hasOnlyNoneReturns=True,
111 isNested=False,
112 decoratorList=None,
113 args=None,
114 ):
96 """ 115 """
97 Constructor 116 Constructor
98 117
99 @param name name of the function 118 @param name name of the function
100 @type str 119 @type str
101 @param lineno line number 120 @param lineno line number
102 @type int 121 @type int
103 @param col_offset column number 122 @param col_offset column number
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
458 """ 474 """
459 if not funcObj.isClassMethod: 475 if not funcObj.isClassMethod:
460 # Short circuit 476 # Short circuit
461 return hintTree 477 return hintTree
462 478
463 if ( 479 if funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and len(
464 funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and 480 hintTree.argtypes
465 len(hintTree.argtypes) < (len(funcObj.args) - 1) 481 ) < (len(funcObj.args) - 1):
466 ):
467 # Subtract 1 to skip return arg 482 # Subtract 1 to skip return arg
468 hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes 483 hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes
469 484
470 return hintTree 485 return hintTree
471 486
472 @staticmethod 487 @staticmethod
473 def getFunctionType(functionName): 488 def getFunctionType(functionName):
474 """ 489 """
475 Static method to determine the function's FunctionType from its name. 490 Static method to determine the function's FunctionType from its name.
476 491
477 MethodType is determined by the following priority: 492 MethodType is determined by the following priority:
478 1. Special: function name prefixed & suffixed by "__" 493 1. Special: function name prefixed & suffixed by "__"
479 2. Private: function name prefixed by "__" 494 2. Private: function name prefixed by "__"
480 3. Protected: function name prefixed by "_" 495 3. Protected: function name prefixed by "_"
481 4. Public: everything else 496 4. Public: everything else
482 497
483 @param functionName function name to be checked 498 @param functionName function name to be checked
484 @type str 499 @type str
485 @return type of function 500 @return type of function
486 @rtype FunctionType 501 @rtype FunctionType
487 """ 502 """
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)

eric ide

mercurial