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
« 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
4"""Find functions and classes in Python code."""
6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
8import ast 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
9import dataclasses 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
10from typing import cast 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
12from coverage.plugin import CodeRegion 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
15@dataclasses.dataclass 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
16class Context: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
17 """The nested named context of a function or class."""
19 name: str 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
20 kind: str 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
21 lines: set[int] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
24class RegionFinder: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
25 """An ast visitor that will find and track regions of code.
27 Functions and classes are tracked by name. Results are in the .regions
28 attribute.
30 """
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
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
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
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
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
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
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
104def code_regions(source: str) -> list[CodeRegion]: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
105 """Find function and class regions in source code.
107 Analyzes the code in `source`, and returns a list of :class:`CodeRegion`
108 objects describing functions and classes as regions of the code::
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 ]
116 The line numbers will include comments and blank lines. Later processing
117 will need to ignore those lines as needed.
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.
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