""" 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