Skills › Scientific Agent Skills › matlab › generate_function_scaffold.py
generate_function_scaffold.py
165 lines · skills/matlab/scripts/generate_function_scaffold.py at 5f2bc03
1#!/usr/bin/env python3
2"""Dry-run or write deterministic MATLAB function and unit-test scaffolds."""
4from __future__ import annotations
6import argparse
7import re
8import sys
9from pathlib import Path
10from typing import Any
12from _common import (
13 CliError,
14 checked_input,
15 checked_root,
16 emit_json,
17 fail_json,
18 relative_id,
19 validate_identifier,
20 write_new_text,
21)
23TOOL = "generate_function_scaffold"
24SAFE_DIRECTORY = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]{0,63}$")
27def _directory(root: Path, name: str, *, write: bool) -> Path:
28 if not SAFE_DIRECTORY.fullmatch(name):
29 raise CliError("source/test directory must be one simple local directory name")
30 path = root / name
31 if path.exists():
32 return checked_input(path, root=root, kind="directory")
33 if not write:
34 return path
35 try:
36 path.mkdir()
37 except FileExistsError:
38 return checked_input(path, root=root, kind="directory")
39 except OSError as exc:
40 raise CliError(f"cannot create directory: {path}") from exc
41 return path
44def _title_name(name: str) -> str:
45 return name[0].upper() + name[1:]
48def function_source(name: str) -> str:
49 upper = name.upper()
50 return f"""function result = {name}(data, options)
51%{upper} Scale a finite numeric matrix.
52% RESULT = {upper}(DATA, Scale=VALUE) multiplies DATA by VALUE.
54arguments
55 data (:,:) double {{mustBeFinite}}
56 options.Scale (1,1) double {{mustBeFinite}} = 1
57end
59result = data .* options.Scale;
60end
61"""
64def test_source(name: str, class_name: str) -> str:
65 return f"""classdef {class_name} < matlab.unittest.TestCase
66 methods (Test)
67 function scalesFiniteMatrix(testCase)
68 actual = {name}([1 2; 3 4], Scale=2);
69 expected = [2 4; 6 8];
70 testCase.verifyEqual(actual, expected, AbsTol=1e-14);
71 end
73 function preservesShape(testCase)
74 actual = {name}(zeros(2, 3));
75 testCase.verifySize(actual, [2 3]);
76 end
77 end
78end
79"""
82def generate(args: argparse.Namespace) -> dict[str, Any]:
83 root = checked_root(args.root)
84 name = validate_identifier(args.name, name="function name")
85 class_name = validate_identifier(
86 "Test" + _title_name(name), name="test class name"
87 )
88 if args.source_dir == args.test_dir:
89 raise CliError("source and test directories must be distinct")
90 if not SAFE_DIRECTORY.fullmatch(args.source_dir) or not SAFE_DIRECTORY.fullmatch(
91 args.test_dir
92 ):
93 raise CliError(
94 "source/test directory must be one simple local directory name"
95 )
96 source_dir = _directory(root, args.source_dir, write=args.write)
97 test_dir = _directory(root, args.test_dir, write=args.write)
98 function_path = source_dir / f"{name}.m"
99 test_path = test_dir / f"{class_name}.m"
100 for path in (function_path, test_path):
101 if path.exists() or path.is_symlink():
102 raise CliError(f"refusing to overwrite existing scaffold: {path}")
103 function_text = function_source(name)
104 test_text = test_source(name, class_name)
105 if args.write:
106 write_new_text(function_path, function_text)
107 try:
108 write_new_text(test_path, test_text)
109 except Exception:
110 try:
111 function_path.unlink(missing_ok=True)
112 except OSError:
113 pass
114 raise
115 return {
116 "contents": {
117 "function": function_text,
118 "test": test_text,
119 },
120 "dry_run": not args.write,
121 "executes": False,
122 "files": {
123 "function": relative_id(function_path, root),
124 "test": relative_id(test_path, root),
125 },
126 "network_accessed": False,
127 "ok": True,
128 "requires_matlab_to_run_tests": True,
129 "target_release": "R2026a",
130 "tool": TOOL,
131 "wrote_files": bool(args.write),
132 "warning": (
133 "Generated text is a starting point. Review domain semantics, "
134 "tolerances, products, paths, and safety before execution."
135 ),
136 }
139def parser() -> argparse.ArgumentParser:
140 result = argparse.ArgumentParser(
141 description=(
142 "Generate a MATLAB R2026a function and matlab.unittest class. "
143 "Default is dry-run; --write refuses collisions."
144 )
145 )
146 result.add_argument("name", help="MATLAB function identifier")
147 result.add_argument("--root", default=".", help="allowed project root")
148 result.add_argument("--source-dir", default="src")
149 result.add_argument("--test-dir", default="tests")
150 result.add_argument(
151 "--write", action="store_true", help="create new files and directories"
152 )
153 return result
156def main() -> int:
157 try:
158 emit_json(generate(parser().parse_args()))
159 return 0
160 except CliError as exc:
161 return fail_json(TOOL, exc)
164if __name__ == "__main__":
165 sys.exit(main())