134 lines
4.8 KiB
Python
134 lines
4.8 KiB
Python
"""
|
||
Exercise 1 – Find All Classes and Top-Level Functions
|
||
=====================================================
|
||
AISE501 · AST Exercises · Spring Semester 2026
|
||
|
||
Learning goals
|
||
--------------
|
||
* Parse a Python source file into an AST using ``ast.parse()``.
|
||
* Walk the tree with ``ast.walk()`` to discover ``ClassDef`` and
|
||
``FunctionDef`` nodes.
|
||
* Extract basic metadata: name, line number, docstring.
|
||
|
||
Tasks
|
||
-----
|
||
Part A Parse sample_stats.py and list every top-level class (TODO 1-2).
|
||
Part B List every top-level (module-level) function (TODO 3-4).
|
||
Part C Extract the docstring of each class and function (TODO 5-6).
|
||
"""
|
||
|
||
import ast
|
||
from pathlib import Path
|
||
|
||
SOURCE_FILE = Path(__file__).parent / "sample_stats.py"
|
||
source_code = SOURCE_FILE.read_text()
|
||
|
||
# Parse the source code into an AST
|
||
tree = ast.parse(source_code)
|
||
|
||
|
||
# ── Part A: Find All Classes ────────────────────────────────────────────────
|
||
# Iterate over the top-level body of the module and collect ClassDef nodes.
|
||
|
||
print("=" * 60)
|
||
print("Part A – Classes in sample_stats.py")
|
||
print("=" * 60)
|
||
|
||
# TODO 1: Create a list called `classes` that contains every ast.ClassDef
|
||
# node found in tree.body (the top-level statements).
|
||
#
|
||
# Hint: Use a list comprehension:
|
||
# [node for node in tree.body if isinstance(node, ???)]
|
||
|
||
classes = [node for node in tree.body if isinstance(node, ast.ClassDef)] # TODO: replace with list comprehension
|
||
|
||
# TODO 2: Print each class name and its line number.
|
||
# Access node.name and node.lineno.
|
||
|
||
max_class_length = str(max([len(cls.name) for cls in classes]))
|
||
for cls in classes:
|
||
# TODO: print class name and line number
|
||
print(f"{cls.name:{max_class_length}} | Line: {cls.lineno}")
|
||
|
||
|
||
# ── Part B: Find All Top-Level Functions ────────────────────────────────────
|
||
# Same approach, but filter for FunctionDef instead of ClassDef.
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Part B – Top-level functions in sample_stats.py")
|
||
print("=" * 60)
|
||
|
||
# TODO 3: Create a list called `functions` that contains every
|
||
# ast.FunctionDef node found in tree.body.
|
||
|
||
functions = [node for node in tree.body if isinstance(node, ast.FunctionDef)] # TODO: replace with list comprehension
|
||
|
||
# TODO 4: Print each function name and its line number.
|
||
|
||
max_function_length = str(max([len(func.name) for func in functions]))
|
||
for func in functions:
|
||
# TODO: print function name and line number
|
||
print(f"{func.name:{max_function_length}} | Line: {func.lineno}")
|
||
|
||
|
||
# ── Part C: Extract Docstrings ──────────────────────────────────────────────
|
||
# In Python's AST a docstring is the first statement in the body of a
|
||
# class or function, if that statement is an ast.Expr whose value is an
|
||
# ast.Constant with a string value.
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Part C – Docstrings")
|
||
print("=" * 60)
|
||
|
||
# TODO 5: Write a helper function get_docstring(node) that returns the
|
||
# docstring of a ClassDef or FunctionDef, or None if there is none.
|
||
#
|
||
# Hint: You can also use ast.get_docstring(node) from the standard library,
|
||
# but try implementing it manually first to understand the tree structure.
|
||
#
|
||
# Manual approach:
|
||
# 1. Check if node.body is non-empty.
|
||
# 2. Check if the first element is an ast.Expr.
|
||
# 3. Check if that Expr's .value is an ast.Constant with a str value.
|
||
# 4. If all checks pass, return the string; otherwise return None.
|
||
|
||
def get_docstring(node):
|
||
"""Return the docstring of *node*, or None."""
|
||
if not node.body:
|
||
return None
|
||
|
||
if isinstance(node.body[0], ast.Expr):
|
||
if isinstance(node.body[0].value, ast.Constant):
|
||
return str(node.body[0].value.s)
|
||
|
||
return None
|
||
|
||
|
||
# TODO 6: For each class and function, print its name and docstring (first line only).
|
||
|
||
print("\nClasses:")
|
||
for cls in classes:
|
||
doc = get_docstring(cls)
|
||
# TODO: print cls.name and the first line of doc (or "No docstring")
|
||
|
||
if not doc:
|
||
doc = "No docstring"
|
||
|
||
print(f"{cls.name:{max_class_length}} | {doc.split('\n')[0]}")
|
||
|
||
print("\nFunctions:")
|
||
for func in functions:
|
||
doc = get_docstring(func)
|
||
# TODO: print func.name and the first line of doc (or "No docstring")
|
||
if not doc:
|
||
doc = "No docstring"
|
||
|
||
print(f"{func.name:{max_function_length}} | {doc.split('\n')[0]}")
|
||
|
||
|
||
# ── Expected Output (abbreviated) ──────────────────────────────────────────
|
||
# Part A should list: DataCleaner, DescriptiveStats, HypothesisTester,
|
||
# CurveFitter, ReportGenerator
|
||
# Part B should list: load_csv, save_json, validate_data, run_analysis_pipeline
|
||
# Part C should show the first line of each docstring.
|