0
# AST Visitor
1
2
AST (Abstract Syntax Tree) visitor functionality for traversing Python source code and collecting docstring coverage information. The visitor pattern is used to examine Python code structure and identify missing docstrings across modules, classes, functions, and methods.
3
4
## Capabilities
5
6
### Coverage Node Data Structure
7
8
Data structure representing coverage information for individual code elements in the AST.
9
10
```python { .api }
11
class CovNode:
12
"""Coverage information for an AST node."""
13
14
name: str # Name of the code element
15
path: str # File path containing the element
16
level: int # Nesting level (0 = module, 1 = class/function, etc.)
17
lineno: int # Line number in source file
18
covered: bool # Whether element has docstring
19
node_type: str # Type of AST node ("module", "class", "function", "method")
20
is_nested_func: bool # Whether function is nested inside another function
21
is_nested_cls: bool # Whether class is nested inside another class
22
parent: object # Parent node (for nested elements)
23
```
24
25
### AST Coverage Visitor
26
27
The main visitor class that traverses Python AST to collect docstring coverage information.
28
29
```python { .api }
30
class CoverageVisitor:
31
"""AST visitor for collecting docstring coverage data."""
32
33
def __init__(self, filename, config):
34
"""
35
Initialize AST visitor.
36
37
Args:
38
filename: Path to file being analyzed
39
config: InterrogateConfig with analysis options
40
"""
41
42
def visit_Module(self, node):
43
"""Visit module-level node."""
44
45
def visit_ClassDef(self, node):
46
"""Visit class definition node."""
47
48
def visit_FunctionDef(self, node):
49
"""Visit function definition node."""
50
51
def visit_AsyncFunctionDef(self, node):
52
"""Visit async function definition node."""
53
```
54
55
### Node Analysis Helpers
56
57
Static and helper methods for analyzing AST nodes and determining coverage rules.
58
59
```python { .api }
60
class CoverageVisitor:
61
@staticmethod
62
def _has_doc(node):
63
"""
64
Check if an AST node has a docstring.
65
66
Args:
67
node: AST node to check
68
69
Returns:
70
bool: True if node has docstring
71
"""
72
73
def _is_nested_func(self, parent, node_type):
74
"""
75
Determine if function is nested inside another function.
76
77
Args:
78
parent: Parent AST node
79
node_type: Type of current node
80
81
Returns:
82
bool: True if function is nested
83
"""
84
85
def _is_nested_cls(self, parent, node_type):
86
"""
87
Determine if class is nested inside another class.
88
89
Args:
90
parent: Parent AST node
91
node_type: Type of current node
92
93
Returns:
94
bool: True if class is nested
95
"""
96
97
def _is_private(self, node):
98
"""
99
Check if node represents a private method/function.
100
101
Args:
102
node: AST node to check
103
104
Returns:
105
bool: True if node is private (starts with __)
106
"""
107
108
def _is_semiprivate(self, node):
109
"""
110
Check if node represents a semiprivate method/function.
111
112
Args:
113
node: AST node to check
114
115
Returns:
116
bool: True if node is semiprivate (starts with single _)
117
"""
118
119
def _has_property_decorators(self, node):
120
"""
121
Check if function has property decorators.
122
123
Args:
124
node: Function AST node
125
126
Returns:
127
bool: True if has @property, @setter, @getter, or @deleter
128
"""
129
130
def _has_setters(self, node):
131
"""
132
Check if function has property setter decorators.
133
134
Args:
135
node: Function AST node
136
137
Returns:
138
bool: True if has @property.setter decorator
139
"""
140
141
def _has_overload_decorator(self, node):
142
"""
143
Check if function has @typing.overload decorator.
144
145
Args:
146
node: Function AST node
147
148
Returns:
149
bool: True if has @overload decorator
150
"""
151
```
152
153
## Usage Examples
154
155
### Basic AST Analysis
156
157
```python
158
import ast
159
from interrogate.visit import CoverageVisitor
160
from interrogate.config import InterrogateConfig
161
162
# Parse Python source code
163
source_code = '''
164
class MyClass:
165
"""Class docstring."""
166
167
def method_with_doc(self):
168
"""Method with docstring."""
169
pass
170
171
def method_without_doc(self):
172
pass
173
'''
174
175
# Create AST and visitor
176
tree = ast.parse(source_code)
177
config = InterrogateConfig()
178
visitor = CoverageVisitor("example.py", config)
179
180
# Visit all nodes
181
visitor.visit(tree)
182
183
# Access collected coverage nodes
184
for node in visitor.covered_nodes:
185
print(f"{node.name} ({node.node_type}): {'COVERED' if node.covered else 'MISSING'}")
186
```
187
188
### Custom Configuration Analysis
189
190
```python
191
import ast
192
from interrogate.visit import CoverageVisitor
193
from interrogate.config import InterrogateConfig
194
195
# Configure analysis options
196
config = InterrogateConfig(
197
ignore_private=True,
198
ignore_magic=True,
199
ignore_init_method=True
200
)
201
202
source = '''
203
class Example:
204
def __init__(self):
205
pass
206
207
def _private_method(self):
208
pass
209
210
def __magic_method__(self):
211
pass
212
213
def public_method(self):
214
pass
215
'''
216
217
tree = ast.parse(source)
218
visitor = CoverageVisitor("example.py", config)
219
visitor.visit(tree)
220
221
# Only public_method should be analyzed due to ignore settings
222
for node in visitor.covered_nodes:
223
if not node.covered:
224
print(f"Missing docstring: {node.name}")
225
```
226
227
### Nested Structure Analysis
228
229
```python
230
import ast
231
from interrogate.visit import CoverageVisitor, CovNode
232
from interrogate.config import InterrogateConfig
233
234
source = '''
235
class OuterClass:
236
"""Outer class docstring."""
237
238
class InnerClass:
239
def inner_method(self):
240
def nested_function():
241
pass
242
pass
243
244
def outer_method(self):
245
def local_function():
246
pass
247
pass
248
'''
249
250
tree = ast.parse(source)
251
config = InterrogateConfig()
252
visitor = CoverageVisitor("nested_example.py", config)
253
visitor.visit(tree)
254
255
# Analyze nested structure
256
for node in visitor.covered_nodes:
257
indent = " " * node.level
258
nested_info = []
259
if node.is_nested_cls:
260
nested_info.append("nested class")
261
if node.is_nested_func:
262
nested_info.append("nested function")
263
264
nested_str = f" ({', '.join(nested_info)})" if nested_info else ""
265
print(f"{indent}{node.name} ({node.node_type}){nested_str}: line {node.lineno}")
266
```
267
268
### Decorator Detection
269
270
```python
271
import ast
272
from interrogate.visit import CoverageVisitor
273
from interrogate.config import InterrogateConfig
274
275
source = '''
276
from typing import overload
277
278
class Properties:
279
@property
280
def value(self):
281
return self._value
282
283
@value.setter
284
def value(self, val):
285
self._value = val
286
287
@overload
288
def process(self, x: int) -> int: ...
289
290
@overload
291
def process(self, x: str) -> str: ...
292
293
def process(self, x):
294
return x
295
'''
296
297
tree = ast.parse(source)
298
visitor = CoverageVisitor("decorators.py", InterrogateConfig())
299
visitor.visit(tree)
300
301
# Check decorator detection
302
for node in visitor.covered_nodes:
303
if node.node_type in ("function", "method"):
304
# Access private methods to check decorator status
305
ast_node = node._ast_node # Hypothetical access to original AST node
306
307
if visitor._has_property_decorators(ast_node):
308
print(f"{node.name}: Has property decorators")
309
if visitor._has_setters(ast_node):
310
print(f"{node.name}: Has setter decorators")
311
if visitor._has_overload_decorator(ast_node):
312
print(f"{node.name}: Has overload decorator")
313
```
314
315
### Integration with Coverage Analysis
316
317
```python
318
import ast
319
from interrogate.visit import CoverageVisitor
320
from interrogate.config import InterrogateConfig
321
from interrogate.coverage import InterrogateFileResult
322
323
def analyze_file_ast(filename, config):
324
"""Analyze a Python file's AST for docstring coverage."""
325
326
with open(filename, 'r') as f:
327
source = f.read()
328
329
try:
330
tree = ast.parse(source)
331
except SyntaxError as e:
332
print(f"Syntax error in {filename}: {e}")
333
return None
334
335
# Create visitor and analyze
336
visitor = CoverageVisitor(filename, config)
337
visitor.visit(tree)
338
339
# Create file result
340
file_result = InterrogateFileResult(
341
filename=filename,
342
nodes=visitor.covered_nodes
343
)
344
file_result.combine() # Calculate totals
345
346
return file_result
347
348
# Usage
349
config = InterrogateConfig(fail_under=80.0)
350
result = analyze_file_ast("my_module.py", config)
351
352
if result:
353
print(f"Coverage: {result.perc_covered:.1f}%")
354
print(f"Missing: {result.missing}/{result.total}")
355
```
356
357
### Custom Node Filtering
358
359
```python
360
from interrogate.visit import CoverageVisitor
361
from interrogate.config import InterrogateConfig
362
363
class CustomCoverageVisitor(CoverageVisitor):
364
"""Extended visitor with custom filtering logic."""
365
366
def _is_ignored_common(self, node):
367
"""Override common ignore logic."""
368
# Call parent implementation
369
if super()._is_ignored_common(node):
370
return True
371
372
# Add custom logic - ignore test methods
373
if hasattr(node, 'name') and node.name.startswith('test_'):
374
return True
375
376
# Ignore methods with specific decorators
377
if hasattr(node, 'decorator_list'):
378
decorator_names = [d.id for d in node.decorator_list if hasattr(d, 'id')]
379
if 'skip_coverage' in decorator_names:
380
return True
381
382
return False
383
384
# Usage with custom visitor
385
source = '''
386
class TestClass:
387
def test_something(self):
388
"""This will be ignored due to test_ prefix."""
389
pass
390
391
@skip_coverage
392
def skip_this(self):
393
"""This will be ignored due to decorator."""
394
pass
395
396
def normal_method(self):
397
"""This will be analyzed normally."""
398
pass
399
'''
400
401
import ast
402
tree = ast.parse(source)
403
config = InterrogateConfig()
404
visitor = CustomCoverageVisitor("test.py", config)
405
visitor.visit(tree)
406
407
# Only normal_method should be in results
408
for node in visitor.covered_nodes:
409
print(f"Analyzed: {node.name}")
410
```