224 lines
8.0 KiB
Python
224 lines
8.0 KiB
Python
"""
|
||
Exercise 2 – Analyse Class Methods and Attributes
|
||
==================================================
|
||
AISE501 · AST Exercises · Spring Semester 2026
|
||
|
||
Learning goals
|
||
--------------
|
||
* Use ``ast.NodeVisitor`` to build a targeted traversal.
|
||
* Extract method signatures (parameters, return annotations).
|
||
* Identify instance attributes assigned in ``__init__``.
|
||
* Distinguish between regular methods and static methods.
|
||
|
||
Tasks
|
||
-----
|
||
Part A Use a NodeVisitor to collect methods per class (TODOs 1-2).
|
||
Part B Extract the parameter list for each method (TODOs 3-4).
|
||
Part C Find instance attributes set in __init__ (TODOs 5-6).
|
||
Part D Detect @staticmethod decorators (TODOs 7-8).
|
||
"""
|
||
|
||
import ast
|
||
from pathlib import Path
|
||
|
||
SOURCE_FILE = Path(__file__).parent / "sample_stats.py"
|
||
source_code = SOURCE_FILE.read_text()
|
||
|
||
tree = ast.parse(source_code)
|
||
|
||
|
||
# ── Part A: Collect Methods Per Class with NodeVisitor ──────────────────────
|
||
|
||
print("=" * 60)
|
||
print("Part A – Methods per class (NodeVisitor)")
|
||
print("=" * 60)
|
||
|
||
|
||
# TODO 1: Complete the ClassMethodVisitor.
|
||
# In visit_ClassDef, iterate over node.body and collect every
|
||
# FunctionDef into a list. Store the result in self.classes
|
||
# as a dict mapping class_name -> list of method names.
|
||
#
|
||
# Hint: Don't forget to call self.generic_visit(node) at the end
|
||
# so that nested classes (if any) are also visited.
|
||
|
||
class ClassMethodVisitor(ast.NodeVisitor):
|
||
def __init__(self):
|
||
self.classes: dict[str, list[ast.FunctionDef]] = {}
|
||
|
||
def visit_ClassDef(self, node: ast.ClassDef):
|
||
# TODO: collect method names and store in self.classes
|
||
functions = [func for func in node.body if isinstance(func, ast.FunctionDef)]
|
||
self.classes[node.name] = functions
|
||
|
||
self.generic_visit(node)
|
||
|
||
# TODO 2: Instantiate the visitor, call visitor.visit(tree),
|
||
# and print each class with its methods.
|
||
|
||
visitor = ClassMethodVisitor()
|
||
visitor.visit(tree)
|
||
for cls_name, methods in visitor.classes.items():
|
||
print(f"\n class {cls_name}:")
|
||
for m in methods:
|
||
print(f" - {m.name}()")
|
||
|
||
|
||
# ── Part B: Extract Method Signatures ───────────────────────────────────────
|
||
# For each method, extract its parameter names (excluding 'self')
|
||
# and any type annotations.
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Part B – Method signatures")
|
||
print("=" * 60)
|
||
|
||
# TODO 3: Write a function `get_signature(func_node)` that returns a string
|
||
# representation of the function's parameters.
|
||
#
|
||
# For each parameter in func_node.args.args:
|
||
# - Skip 'self'
|
||
# - Get the parameter name from arg.arg
|
||
# - If arg.annotation exists, unparse it with ast.unparse()
|
||
# - Format as "name: type" or just "name"
|
||
#
|
||
# Also check func_node.returns for a return annotation.
|
||
#
|
||
# Example output: "(data: np.ndarray, z_threshold: float) -> np.ndarray"
|
||
|
||
def get_signature(func_node: ast.FunctionDef) -> str:
|
||
"""Return a string like '(param1: Type, param2) -> ReturnType'."""
|
||
params = []
|
||
for arg in func_node.args.args:
|
||
# Skip 'self'
|
||
if arg.arg == "self":
|
||
continue
|
||
|
||
# Get parameter name
|
||
name = arg.arg
|
||
|
||
# Check for type annotation
|
||
if arg.annotation:
|
||
type_hint = ast.unparse(arg.annotation)
|
||
params.append(f"{name}: {type_hint}")
|
||
else:
|
||
params.append(name)
|
||
|
||
return_type = ast.unparse(func_node.returns) if func_node.returns else None
|
||
|
||
return f"({", ".join(params)}) -> {return_type}"
|
||
|
||
# TODO 4: For each class and method, print the full signature.
|
||
# Reuse the visitor results from Part A.
|
||
|
||
for cls_name, methods in visitor.classes.items():
|
||
print(f"\n class {cls_name}:")
|
||
# You need the actual FunctionDef nodes, not just names.
|
||
# Hint: walk the tree again or modify the visitor to store nodes.
|
||
for m in methods:
|
||
print(f" - {get_signature(m)}")
|
||
|
||
|
||
|
||
# ── Part C: Find Instance Attributes ───────────────────────────────────────
|
||
# Instance attributes are typically assigned in __init__ as self.xxx = ...
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Part C – Instance attributes (self.xxx in __init__)")
|
||
print("=" * 60)
|
||
|
||
# TODO 5: Write a function `find_instance_attributes(class_node)` that
|
||
# returns a list of attribute names assigned via self.xxx = ...
|
||
# in the __init__ method.
|
||
#
|
||
# Approach:
|
||
# 1. Find the __init__ method in class_node.body.
|
||
# 2. Walk through the __init__ body looking for ast.Assign or
|
||
# ast.AnnAssign nodes.
|
||
# 3. For Assign: check if any target is an ast.Attribute where
|
||
# target.value is ast.Name(id='self').
|
||
# 4. Collect the attribute names (target.attr).
|
||
|
||
def find_instance_attributes(class_node: ast.ClassDef) -> list[str]:
|
||
"""Return attribute names assigned as self.xxx in __init__."""
|
||
attributes = []
|
||
|
||
# 1. Find __init__
|
||
init_method = None
|
||
for node in class_node.body:
|
||
if isinstance(node, ast.FunctionDef) and node.name == "__init__":
|
||
init_method = node
|
||
break
|
||
|
||
if not init_method:
|
||
return attributes
|
||
|
||
# 2. Walk __init__ body
|
||
for node in ast.walk(init_method):
|
||
|
||
# 3a. Handle regular assignment: self.x = ...
|
||
if isinstance(node, ast.Assign):
|
||
for target in node.targets:
|
||
if (isinstance(target, ast.Attribute) and
|
||
isinstance(target.value, ast.Name) and
|
||
target.value.id == "self"):
|
||
attributes.append(target.attr)
|
||
|
||
# 3b. Handle annotated assignment: self.x: int = ...
|
||
elif isinstance(node, ast.AnnAssign):
|
||
target = node.target
|
||
if (isinstance(target, ast.Attribute) and
|
||
isinstance(target.value, ast.Name) and
|
||
target.value.id == "self"):
|
||
attributes.append(target.attr)
|
||
|
||
return attributes
|
||
|
||
# TODO 6: For each class, print its instance attributes.
|
||
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.ClassDef):
|
||
attrs = find_instance_attributes(node)
|
||
print(f"\n class {node.name}:")
|
||
for attr in attrs:
|
||
print(f" self.{attr}")
|
||
|
||
|
||
# ── Part D: Detect Static Methods ──────────────────────────────────────────
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Part D – Static methods")
|
||
print("=" * 60)
|
||
|
||
# TODO 7: Write a function `is_static_method(func_node)` that returns True
|
||
# if the function has a @staticmethod decorator.
|
||
#
|
||
# Hint: Decorators are in func_node.decorator_list.
|
||
# Each decorator is an ast.Name node (for simple decorators).
|
||
# Check if any decorator has .id == "staticmethod".
|
||
|
||
def is_static_method(func_node: ast.FunctionDef) -> bool:
|
||
"""Return True if *func_node* is decorated with @staticmethod."""
|
||
return any([ast.unparse(dec) == "staticmethod" for dec in func_node.decorator_list])
|
||
|
||
|
||
# TODO 8: For each class, list its static methods separately.
|
||
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.ClassDef):
|
||
statics = [m.name for m in node.body
|
||
if isinstance(m, ast.FunctionDef) and is_static_method(m)]
|
||
regulars = [m.name for m in node.body
|
||
if isinstance(m, ast.FunctionDef) and not is_static_method(m)]
|
||
print(f"\n class {node.name}:")
|
||
print(f" Regular methods : {regulars}")
|
||
print(f" Static methods : {statics}")
|
||
|
||
|
||
# ── Expected Output (abbreviated) ──────────────────────────────────────────
|
||
# Part A: Each class with its method list.
|
||
# Part B: Full signatures, e.g. "remove_outliers(z_threshold: float) -> np.ndarray"
|
||
# Part C: DataCleaner -> self.raw_data, self.cleaned
|
||
# DescriptiveStats -> self.data
|
||
# etc.
|
||
# Part D: CurveFitter has static methods: linear_model, quadratic_model, exponential_model
|