Coverage for coverage / regions.py: 100.000%

52 statements  

« prev     ^ index     » next       coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000

1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt 

3 

4"""Find functions and classes in Python code.""" 

5 

6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

7 

8import ast 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

9import dataclasses 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

10from typing import cast 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

11 

12from coverage.plugin import CodeRegion 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

13 

14 

15@dataclasses.dataclass 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

16class Context: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

17 """The nested named context of a function or class.""" 

18 

19 name: str 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

20 kind: str 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

21 lines: set[int] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

22 

23 

24class RegionFinder: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

25 """An ast visitor that will find and track regions of code. 

26 

27 Functions and classes are tracked by name. Results are in the .regions 

28 attribute. 

29 

30 """ 

31 

32 def __init__(self) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

33 self.regions: list[CodeRegion] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

34 self.context: list[Context] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

35 

36 def parse_source(self, source: str) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

37 """Parse `source` and walk the ast to populate the .regions attribute.""" 

38 self.handle_node(ast.parse(source)) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

39 

40 def fq_node_name(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

41 """Get the current fully qualified name we're processing.""" 

42 return ".".join(c.name for c in self.context) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

43 

44 def handle_node(self, node: ast.AST) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

45 """Recursively handle any node.""" 

46 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

47 self.handle_FunctionDef(node) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

48 elif isinstance(node, ast.ClassDef): 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

49 self.handle_ClassDef(node) 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMN6OPQRST7UVWXYZ01234

50 else: 

51 self.handle_node_body(node) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

52 

53 def handle_node_body(self, node: ast.AST) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

54 """Recursively handle the nodes in this node's body, if any.""" 

55 for body_node in getattr(node, "body", ()): 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

56 self.handle_node(body_node) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

57 

58 def handle_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

59 """Called for `def` or `async def`.""" 

60 lines = set(range(node.body[0].lineno, cast(int, node.body[-1].end_lineno) + 1)) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

61 if self.context and self.context[-1].kind == "class": 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

62 # Function bodies are part of their enclosing class. 

63 self.context[-1].lines |= lines 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMN6OPQRST7UVWXYZ01234

64 # Function bodies should be excluded from the nearest enclosing function. 

65 for ancestor in reversed(self.context): 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

66 if ancestor.kind == "function": 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMN6OPQRST7UVWXYZ01234

67 ancestor.lines -= lines 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMN6OPQRST7UVWXYZ01234

68 break 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMN6OPQRST7UVWXYZ01234

69 self.context.append(Context(node.name, "function", lines)) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

70 self.regions.append( 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

71 CodeRegion( 

72 kind="function", 

73 name=self.fq_node_name(), 

74 start=node.lineno, 

75 lines=lines, 

76 ) 

77 ) 

78 self.handle_node_body(node) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

79 self.context.pop() 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

80 

81 def handle_ClassDef(self, node: ast.ClassDef) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

82 """Called for `class`.""" 

83 # The lines for a class are the lines in the methods of the class. 

84 # We start empty, and count on visit_FunctionDef to add the lines it 

85 # finds. 

86 lines: set[int] = set() 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

87 self.context.append(Context(node.name, "class", lines)) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

88 self.regions.append( 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

89 CodeRegion( 

90 kind="class", 

91 name=self.fq_node_name(), 

92 start=node.lineno, 

93 lines=lines, 

94 ) 

95 ) 

96 self.handle_node_body(node) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

97 self.context.pop() 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

98 # Class bodies should be excluded from the enclosing classes. 

99 for ancestor in reversed(self.context): 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

100 if ancestor.kind == "class": 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJ8KLMN6OP9QRST7UV!WXYZ#01234

101 ancestor.lines -= lines 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJ8KLMN6OP9QRST7UV!WXYZ#01234

102 

103 

104def code_regions(source: str) -> list[CodeRegion]: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

105 """Find function and class regions in source code. 

106 

107 Analyzes the code in `source`, and returns a list of :class:`CodeRegion` 

108 objects describing functions and classes as regions of the code:: 

109 

110 [ 

111 CodeRegion(kind="function", name="func1", start=8, lines={10, 11, 12}), 

112 CodeRegion(kind="function", name="MyClass.method", start=30, lines={34, 35, 36}), 

113 CodeRegion(kind="class", name="MyClass", start=25, lines={34, 35, 36}), 

114 ] 

115 

116 The line numbers will include comments and blank lines. Later processing 

117 will need to ignore those lines as needed. 

118 

119 Nested functions and classes are excluded from their enclosing region. No 

120 line should be reported as being part of more than one function, or more 

121 than one class. Lines in methods are reported as being in a function and 

122 in a class. 

123 

124 """ 

125 rf = RegionFinder() 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

126 rf.parse_source(source) 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234

127 return rf.regions 1abcdefghijklmnopqrstuvwxyzABCDEF$GH5IJ8KL%MN6OP9QR'ST7UV!WX(YZ#01)234