CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-radon

Code Metrics in Python - comprehensive tool for computing various software metrics

Pending
Overview
Eval results
Files

visitors.mddocs/

AST Visitors

Low-level AST visitor classes for custom analysis and integration. Provides the foundation for all radon analysis capabilities using the visitor pattern to traverse Python Abstract Syntax Trees (AST) and extract code metrics.

Capabilities

Base Visitor Classes

Foundation classes providing common functionality for AST traversal and analysis.

class CodeVisitor(ast.NodeVisitor):
    """
    Base AST visitor class with common functionality.
    
    Provides factory methods and utilities for creating visitors
    from source code or AST nodes.
    """
    
    @classmethod
    def from_code(cls, code, **kwargs):
        """
        Create visitor instance and analyze source code.
        
        Parameters:
        - code (str): Python source code to analyze
        - **kwargs: Arguments passed to visitor constructor
        
        Returns:
        CodeVisitor: Configured visitor instance after analysis
        """
    
    @classmethod  
    def from_ast(cls, ast_node, **kwargs):
        """
        Create visitor instance and analyze AST node.
        
        Parameters:
        - ast_node: Python AST node to analyze
        - **kwargs: Arguments passed to visitor constructor
        
        Returns:
        CodeVisitor: Configured visitor instance after analysis
        """
    
    @staticmethod
    def get_name(obj):
        """
        Extract name from AST node safely.
        
        Parameters:
        - obj: AST node object
        
        Returns:
        str: Node name or '<unknown>' if no name attribute
        """

def code2ast(source):
    """
    Convert source code string to AST object.
    
    Retained for backwards compatibility. Equivalent to ast.parse().
    
    Parameters:
    - source (str): Python source code
    
    Returns:
    ast.AST: Parsed AST tree
    """

Complexity Analysis Visitor

Specialized visitor for calculating cyclomatic complexity of Python code.

class ComplexityVisitor(CodeVisitor):
    """
    AST visitor for cyclomatic complexity analysis.
    
    Traverses AST nodes and calculates McCabe's cyclomatic complexity
    for functions, methods, and classes.
    """
    
    def __init__(self, to_method=False, classname=None, off=True, no_assert=False):
        """
        Initialize complexity visitor.
        
        Parameters:
        - to_method (bool): Treat top-level functions as methods
        - classname (str): Class name for method analysis context
        - off (bool): Include decorator complexity in calculations
        - no_assert (bool): Exclude assert statements from complexity
        """
    
    # Properties
    @property
    def functions_complexity(self):
        """List of complexities for all functions/methods found."""
    
    @property  
    def classes_complexity(self):
        """List of complexities for all classes found."""
    
    @property
    def total_complexity(self):
        """Total complexity across all functions and classes."""
    
    @property
    def blocks(self):
        """List of all Function and Class objects found."""
    
    @property
    def max_line(self):
        """Maximum line number encountered during analysis."""
    
    # AST visit methods
    def visit_FunctionDef(self, node):
        """Visit function definition and calculate complexity."""
        
    def visit_AsyncFunctionDef(self, node):  
        """Visit async function definition."""
        
    def visit_ClassDef(self, node):
        """Visit class definition and analyze methods."""
        
    def visit_Assert(self, node):
        """Visit assert statement (if not excluded)."""

Halstead Metrics Visitor

Specialized visitor for calculating Halstead software science metrics.

class HalsteadVisitor(CodeVisitor):
    """
    AST visitor for Halstead metrics analysis.
    
    Counts operators and operands to calculate software science metrics
    including volume, difficulty, effort, and estimated bugs.
    """
    
    def __init__(self, context=None):
        """
        Initialize Halstead visitor.
        
        Parameters:
        - context: Analysis context (optional)
        """
    
    # Properties
    @property
    def distinct_operators(self):
        """Set of distinct operators found in the code."""
    
    @property
    def distinct_operands(self):
        """Set of distinct operands found in the code."""
    
    # AST visit methods for operators
    def visit_BinOp(self, node):
        """Visit binary operation (e.g., +, -, *, /)."""
        
    def visit_UnaryOp(self, node):
        """Visit unary operation (e.g., not, -, +)."""
        
    def visit_Compare(self, node):
        """Visit comparison operation (e.g., <, >, ==)."""
        
    def visit_BoolOp(self, node):
        """Visit boolean operation (and, or)."""
        
    def visit_AugAssign(self, node):
        """Visit augmented assignment (+=, -=, etc.)."""
        
    def visit_Name(self, node):
        """Visit name reference (variables, functions)."""
        
    def visit_Num(self, node):
        """Visit numeric literal."""
        
    def visit_Str(self, node):
        """Visit string literal."""

Data Types for Analysis Results

Named tuples representing analyzed code structures.

# Function/method representation
Function = namedtuple('Function', [
    'name',        # Function/method name
    'lineno',      # Starting line number  
    'col_offset',  # Column offset
    'endline',     # Ending line number
    'is_method',   # Boolean: is this a method?
    'classname',   # Class name if method, None if function
    'closures',    # List of nested functions
    'complexity'   # Cyclomatic complexity score
])

# Additional Function properties:
@property
def letter(self):
    """Return 'M' for methods, 'F' for functions."""

@property  
def fullname(self):
    """Return full name including class for methods."""

# Class representation
Class = namedtuple('Class', [
    'name',            # Class name
    'lineno',          # Starting line number
    'col_offset',      # Column offset  
    'endline',         # Ending line number
    'methods',         # List of Method objects
    'inner_classes',   # List of nested Class objects
    'real_complexity'  # Class complexity score
])

# Additional Class properties:
@property
def complexity(self):
    """Average complexity of class methods plus one."""

@property
def fullname(self):
    """Return class name (for consistency with Function)."""

Utility Constants

Helper functions and constants for working with analysis results.

# Attribute getter functions
GET_COMPLEXITY = operator.attrgetter('complexity')
GET_REAL_COMPLEXITY = operator.attrgetter('real_complexity')  
NAMES_GETTER = operator.attrgetter('name', 'asname')
GET_ENDLINE = operator.attrgetter('endline')

Usage Examples

Basic ComplexityVisitor Usage

from radon.visitors import ComplexityVisitor
import ast

code = '''
class Calculator:
    def add(self, a, b):
        return a + b
    
    def complex_divide(self, a, b):
        if b == 0:
            raise ValueError("Division by zero")
        elif isinstance(a, str) or isinstance(b, str):
            raise TypeError("Invalid type")
        else:
            return a / b

def standalone_function(x):
    if x > 0:
        return x * 2
    else:
        return 0
'''

# Method 1: Using class methods
visitor = ComplexityVisitor.from_code(code)

print(f"Total complexity: {visitor.total_complexity}")
print(f"Number of functions: {len(visitor.functions_complexity)}")
print(f"Number of classes: {len(visitor.classes_complexity)}")

# Method 2: Manual AST parsing
ast_tree = ast.parse(code)
manual_visitor = ComplexityVisitor()
manual_visitor.visit(ast_tree)

print("\nDetailed analysis:")
for block in manual_visitor.blocks:
    print(f"{block.letter} {block.fullname}: {block.complexity}")

Custom ComplexityVisitor Configuration

from radon.visitors import ComplexityVisitor

class_code = '''
class DataProcessor:
    @property
    def status(self):
        return self._status
    
    def process(self, data):
        assert data is not None
        if not data:
            return []
        return [item.upper() for item in data if isinstance(item, str)]
'''

# Standard analysis
standard = ComplexityVisitor.from_code(class_code)

# Treat as standalone methods
as_methods = ComplexityVisitor.from_code(
    class_code, 
    to_method=True, 
    classname="DataProcessor"
)

# Exclude assert statements
no_asserts = ComplexityVisitor.from_code(
    class_code,
    no_assert=True
)

print("Standard analysis:")
for block in standard.blocks:
    print(f"  {block.fullname}: {block.complexity}")

print("\nAs methods:")
for block in as_methods.blocks:
    print(f"  {block.fullname}: {block.complexity}")
    
print("\nExcluding asserts:")
for block in no_asserts.blocks:
    print(f"  {block.fullname}: {block.complexity}")

HalsteadVisitor Usage

from radon.visitors import HalsteadVisitor
import ast

code = '''
def calculate_interest(principal, rate, time):
    if rate <= 0 or time <= 0:
        return 0
    return principal * (1 + rate) ** time
'''

# Analyze Halstead metrics
visitor = HalsteadVisitor.from_code(code)

print(f"Distinct operators: {len(visitor.distinct_operators)}")
print(f"Distinct operands: {len(visitor.distinct_operands)}")
print(f"Operators found: {sorted(visitor.distinct_operators)}")
print(f"Operands found: {sorted(visitor.distinct_operands)}")

# Manual visitor usage
ast_tree = ast.parse(code)
manual_visitor = HalsteadVisitor()
manual_visitor.visit(ast_tree)

print(f"\nManual analysis:")
print(f"Total operators: {len(list(manual_visitor.distinct_operators))}")
print(f"Total operands: {len(list(manual_visitor.distinct_operands))}")

Working with Function and Class Objects

from radon.visitors import ComplexityVisitor, GET_COMPLEXITY
import operator

code = '''
class WebService:
    def __init__(self, url):
        self.url = url
    
    def get_data(self, endpoint):
        if not endpoint:
            raise ValueError("Endpoint required")
        return f"{self.url}/{endpoint}"
    
    def post_data(self, endpoint, data):
        if not endpoint or not data:
            raise ValueError("Endpoint and data required")
        return f"POST {self.url}/{endpoint}"
    
    class Config:
        def __init__(self, timeout=30):
            self.timeout = timeout

def helper_function():
    pass
'''

visitor = ComplexityVisitor.from_code(code)

# Analyze functions vs methods
functions = [block for block in visitor.blocks if isinstance(block, type(visitor.blocks[0])) and not block.is_method]
methods = [block for block in visitor.blocks if isinstance(block, type(visitor.blocks[0])) and block.is_method]

print("Functions:")
for func in functions:
    print(f"  {func.name} (line {func.lineno}): {func.complexity}")

print("\nMethods:")  
for method in methods:
    print(f"  {method.fullname} (line {method.lineno}): {method.complexity}")

# Use utility functions
complexities = [GET_COMPLEXITY(block) for block in visitor.blocks]
print(f"\nComplexities: {complexities}")
print(f"Max complexity: {max(complexities)}")
print(f"Average complexity: {sum(complexities) / len(complexities):.2f}")

Extending Visitors for Custom Analysis

from radon.visitors import ComplexityVisitor
import ast

class DetailedComplexityVisitor(ComplexityVisitor):
    """Extended complexity visitor with additional metrics."""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.loop_count = 0
        self.condition_count = 0
        self.exception_count = 0
    
    def visit_For(self, node):
        self.loop_count += 1
        return super().visit_For(node)
    
    def visit_While(self, node):
        self.loop_count += 1
        return super().visit_While(node)
    
    def visit_If(self, node):
        self.condition_count += 1
        return super().visit_If(node)
    
    def visit_Try(self, node):
        self.exception_count += 1
        return super().visit_Try(node)

code = '''
def process_data(items):
    results = []
    try:
        for item in items:
            if isinstance(item, dict):
                if 'value' in item:
                    results.append(item['value'])
            elif isinstance(item, list):
                for sub_item in item:
                    if sub_item is not None:
                        results.append(sub_item)
    except Exception as e:
        return []
    return results
'''

visitor = DetailedComplexityVisitor.from_code(code)

print(f"Complexity: {visitor.total_complexity}")
print(f"Loops: {visitor.loop_count}")
print(f"Conditions: {visitor.condition_count}")
print(f"Exception handlers: {visitor.exception_count}")

Advanced Usage Patterns

Analyzing Code Fragments

from radon.visitors import ComplexityVisitor, code2ast

# Analyze code fragments without full function definitions
fragment = '''
if condition:
    for item in items:
        if item.valid:
            process(item)
        else:
            skip(item)
else:
    handle_empty()
'''

# Wrap in function for analysis
wrapped = f"def fragment():\n" + "\n".join(f"    {line}" for line in fragment.split('\n'))

ast_tree = code2ast(wrapped)
visitor = ComplexityVisitor()
visitor.visit(ast_tree)

print(f"Fragment complexity: {visitor.total_complexity}")

Batch Analysis with Visitors

from radon.visitors import ComplexityVisitor
import os

def analyze_project(directory):
    """Analyze all Python files in a directory."""
    total_complexity = 0
    total_functions = 0
    
    for filename in os.listdir(directory):
        if filename.endswith('.py'):
            try:
                with open(os.path.join(directory, filename)) as f:
                    code = f.read()
                
                visitor = ComplexityVisitor.from_code(code)
                file_complexity = visitor.total_complexity
                file_functions = len(visitor.blocks)
                
                total_complexity += file_complexity
                total_functions += file_functions
                
                print(f"{filename}: {file_complexity} complexity, {file_functions} functions")
                
            except Exception as e:
                print(f"Error analyzing {filename}: {e}")
    
    if total_functions > 0:
        avg_complexity = total_complexity / total_functions
        print(f"\nProject summary:")
        print(f"  Total complexity: {total_complexity}")
        print(f"  Total functions: {total_functions}")
        print(f"  Average complexity: {avg_complexity:.2f}")

# Usage:
# analyze_project('./src')

Error Handling

The visitor classes handle various edge cases:

  • Empty code: Returns empty analysis results
  • Syntax errors: Propagates AST parsing exceptions
  • Invalid AST nodes: Safely handles missing attributes
  • Circular references: Prevents infinite recursion in nested structures

Integration with Higher-Level APIs

The visitor classes serve as the foundation for radon's higher-level APIs:

# High-level functions use visitors internally
from radon.complexity import cc_visit
from radon.metrics import h_visit

# These functions create and use visitors automatically
blocks = cc_visit(code)  # Uses ComplexityVisitor internally
halstead = h_visit(code)  # Uses HalsteadVisitor internally

# You can access the same functionality directly
from radon.visitors import ComplexityVisitor, HalsteadVisitor

complexity_visitor = ComplexityVisitor.from_code(code)
halstead_visitor = HalsteadVisitor.from_code(code)

This low-level access allows for custom analysis, extended functionality, and integration with other static analysis tools.

Install with Tessl CLI

npx tessl i tessl/pypi-radon

docs

cli.md

complexity.md

halstead.md

index.md

maintainability.md

raw-metrics.md

visitors.md

tile.json