Interrogate a codebase for docstring coverage.
—
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.
Data structure representing coverage information for individual code elements in the AST.
class CovNode:
"""Coverage information for an AST node."""
name: str # Name of the code element
path: str # File path containing the element
level: int # Nesting level (0 = module, 1 = class/function, etc.)
lineno: int # Line number in source file
covered: bool # Whether element has docstring
node_type: str # Type of AST node ("module", "class", "function", "method")
is_nested_func: bool # Whether function is nested inside another function
is_nested_cls: bool # Whether class is nested inside another class
parent: object # Parent node (for nested elements)The main visitor class that traverses Python AST to collect docstring coverage information.
class CoverageVisitor:
"""AST visitor for collecting docstring coverage data."""
def __init__(self, filename, config):
"""
Initialize AST visitor.
Args:
filename: Path to file being analyzed
config: InterrogateConfig with analysis options
"""
def visit_Module(self, node):
"""Visit module-level node."""
def visit_ClassDef(self, node):
"""Visit class definition node."""
def visit_FunctionDef(self, node):
"""Visit function definition node."""
def visit_AsyncFunctionDef(self, node):
"""Visit async function definition node."""Static and helper methods for analyzing AST nodes and determining coverage rules.
class CoverageVisitor:
@staticmethod
def _has_doc(node):
"""
Check if an AST node has a docstring.
Args:
node: AST node to check
Returns:
bool: True if node has docstring
"""
def _is_nested_func(self, parent, node_type):
"""
Determine if function is nested inside another function.
Args:
parent: Parent AST node
node_type: Type of current node
Returns:
bool: True if function is nested
"""
def _is_nested_cls(self, parent, node_type):
"""
Determine if class is nested inside another class.
Args:
parent: Parent AST node
node_type: Type of current node
Returns:
bool: True if class is nested
"""
def _is_private(self, node):
"""
Check if node represents a private method/function.
Args:
node: AST node to check
Returns:
bool: True if node is private (starts with __)
"""
def _is_semiprivate(self, node):
"""
Check if node represents a semiprivate method/function.
Args:
node: AST node to check
Returns:
bool: True if node is semiprivate (starts with single _)
"""
def _has_property_decorators(self, node):
"""
Check if function has property decorators.
Args:
node: Function AST node
Returns:
bool: True if has @property, @setter, @getter, or @deleter
"""
def _has_setters(self, node):
"""
Check if function has property setter decorators.
Args:
node: Function AST node
Returns:
bool: True if has @property.setter decorator
"""
def _has_overload_decorator(self, node):
"""
Check if function has @typing.overload decorator.
Args:
node: Function AST node
Returns:
bool: True if has @overload decorator
"""import ast
from interrogate.visit import CoverageVisitor
from interrogate.config import InterrogateConfig
# Parse Python source code
source_code = '''
class MyClass:
"""Class docstring."""
def method_with_doc(self):
"""Method with docstring."""
pass
def method_without_doc(self):
pass
'''
# Create AST and visitor
tree = ast.parse(source_code)
config = InterrogateConfig()
visitor = CoverageVisitor("example.py", config)
# Visit all nodes
visitor.visit(tree)
# Access collected coverage nodes
for node in visitor.covered_nodes:
print(f"{node.name} ({node.node_type}): {'COVERED' if node.covered else 'MISSING'}")import ast
from interrogate.visit import CoverageVisitor
from interrogate.config import InterrogateConfig
# Configure analysis options
config = InterrogateConfig(
ignore_private=True,
ignore_magic=True,
ignore_init_method=True
)
source = '''
class Example:
def __init__(self):
pass
def _private_method(self):
pass
def __magic_method__(self):
pass
def public_method(self):
pass
'''
tree = ast.parse(source)
visitor = CoverageVisitor("example.py", config)
visitor.visit(tree)
# Only public_method should be analyzed due to ignore settings
for node in visitor.covered_nodes:
if not node.covered:
print(f"Missing docstring: {node.name}")import ast
from interrogate.visit import CoverageVisitor, CovNode
from interrogate.config import InterrogateConfig
source = '''
class OuterClass:
"""Outer class docstring."""
class InnerClass:
def inner_method(self):
def nested_function():
pass
pass
def outer_method(self):
def local_function():
pass
pass
'''
tree = ast.parse(source)
config = InterrogateConfig()
visitor = CoverageVisitor("nested_example.py", config)
visitor.visit(tree)
# Analyze nested structure
for node in visitor.covered_nodes:
indent = " " * node.level
nested_info = []
if node.is_nested_cls:
nested_info.append("nested class")
if node.is_nested_func:
nested_info.append("nested function")
nested_str = f" ({', '.join(nested_info)})" if nested_info else ""
print(f"{indent}{node.name} ({node.node_type}){nested_str}: line {node.lineno}")import ast
from interrogate.visit import CoverageVisitor
from interrogate.config import InterrogateConfig
source = '''
from typing import overload
class Properties:
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = val
@overload
def process(self, x: int) -> int: ...
@overload
def process(self, x: str) -> str: ...
def process(self, x):
return x
'''
tree = ast.parse(source)
visitor = CoverageVisitor("decorators.py", InterrogateConfig())
visitor.visit(tree)
# Check decorator detection
for node in visitor.covered_nodes:
if node.node_type in ("function", "method"):
# Access private methods to check decorator status
ast_node = node._ast_node # Hypothetical access to original AST node
if visitor._has_property_decorators(ast_node):
print(f"{node.name}: Has property decorators")
if visitor._has_setters(ast_node):
print(f"{node.name}: Has setter decorators")
if visitor._has_overload_decorator(ast_node):
print(f"{node.name}: Has overload decorator")import ast
from interrogate.visit import CoverageVisitor
from interrogate.config import InterrogateConfig
from interrogate.coverage import InterrogateFileResult
def analyze_file_ast(filename, config):
"""Analyze a Python file's AST for docstring coverage."""
with open(filename, 'r') as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError as e:
print(f"Syntax error in {filename}: {e}")
return None
# Create visitor and analyze
visitor = CoverageVisitor(filename, config)
visitor.visit(tree)
# Create file result
file_result = InterrogateFileResult(
filename=filename,
nodes=visitor.covered_nodes
)
file_result.combine() # Calculate totals
return file_result
# Usage
config = InterrogateConfig(fail_under=80.0)
result = analyze_file_ast("my_module.py", config)
if result:
print(f"Coverage: {result.perc_covered:.1f}%")
print(f"Missing: {result.missing}/{result.total}")from interrogate.visit import CoverageVisitor
from interrogate.config import InterrogateConfig
class CustomCoverageVisitor(CoverageVisitor):
"""Extended visitor with custom filtering logic."""
def _is_ignored_common(self, node):
"""Override common ignore logic."""
# Call parent implementation
if super()._is_ignored_common(node):
return True
# Add custom logic - ignore test methods
if hasattr(node, 'name') and node.name.startswith('test_'):
return True
# Ignore methods with specific decorators
if hasattr(node, 'decorator_list'):
decorator_names = [d.id for d in node.decorator_list if hasattr(d, 'id')]
if 'skip_coverage' in decorator_names:
return True
return False
# Usage with custom visitor
source = '''
class TestClass:
def test_something(self):
"""This will be ignored due to test_ prefix."""
pass
@skip_coverage
def skip_this(self):
"""This will be ignored due to decorator."""
pass
def normal_method(self):
"""This will be analyzed normally."""
pass
'''
import ast
tree = ast.parse(source)
config = InterrogateConfig()
visitor = CustomCoverageVisitor("test.py", config)
visitor.visit(tree)
# Only normal_method should be in results
for node in visitor.covered_nodes:
print(f"Analyzed: {node.name}")Install with Tessl CLI
npx tessl i tessl/pypi-interrogate