Code Metrics in Python - comprehensive tool for computing various software metrics
—
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.
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
"""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)."""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."""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)."""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')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}")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}")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))}")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}")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}")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}")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')The visitor classes handle various edge cases:
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