Skills › Scientific Agent Skills › matlab › plan_python_compatibility.py
plan_python_compatibility.py
176 lines · skills/matlab/scripts/plan_python_compatibility.py at 5f2bc03
1#!/usr/bin/env python3
2"""Plan MATLAB R2026a Python compatibility without importing or starting Engine."""
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 load_json,
19 validate_release,
20)
22TOOL = "plan_python_compatibility"
23PYTHON_VERSION = re.compile(r"^([0-9]{1,2})\.([0-9]{1,2})(?:\.[0-9]{1,3})?$")
24PACKAGE_VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")
27def compatibility_data() -> dict[str, Any]:
28 skill_root = checked_root(Path(__file__).resolve().parents[1])
29 path = checked_input(
30 skill_root / "assets" / "python_compatibility_r2026a.json",
31 root=skill_root,
32 suffixes={".json"},
33 )
34 value = load_json(path)
35 if not isinstance(value, dict) or value.get("schema_version") != "1.0":
36 raise CliError("bundled compatibility data is invalid")
37 return value
40def normalize_python(value: str) -> tuple[str, str]:
41 match = PYTHON_VERSION.fullmatch(value)
42 if not match:
43 raise CliError("Python version must look like 3.13 or 3.13.5")
44 major_minor = f"{int(match.group(1))}.{int(match.group(2))}"
45 return value, major_minor
48def plan(args: argparse.Namespace) -> tuple[dict[str, Any], int]:
49 release = validate_release(args.matlab_release)
50 data = compatibility_data()
51 if release != data["matlab_release"]:
52 raise CliError(
53 "bundled compatibility data is pinned only to R2026a; consult the "
54 "official support table for another release"
55 )
56 requested_python, major_minor = normalize_python(args.python_version)
57 engine_version = args.engine_version
58 if not PACKAGE_VERSION.fullmatch(engine_version):
59 raise CliError("Engine version must be a three-part numeric version")
60 supported = major_minor in data["supported_python_versions"]
61 exact_engine = engine_version == data["matlab_engine_package"]["version"]
62 bits_ok = args.python_bits == data["python_architecture_bits"]
63 implementation_ok = args.implementation == data["python_implementation"]
64 software_compatible = supported and exact_engine and bits_ok and implementation_ok
65 installed_ok = args.matlab_installed == "yes"
66 license_ok = args.license_status == "confirmed"
67 ready_to_launch = software_compatible and installed_ok and license_ok
68 warnings = [
69 "This static plan does not inspect PATH, PYTHONPATH, sys.path, the "
70 "environment, installations, credentials, or licenses.",
71 "Package installation does not install MATLAB or grant a license.",
72 "MATLAB Runtime cannot host MATLAB Engine for Python.",
73 ]
74 if args.matlab_installed == "unknown":
75 warnings.append("Installed MATLAB R2026a has not been confirmed.")
76 if args.license_status == "unknown":
77 warnings.append("MATLAB and required-product license availability is unknown.")
78 if not supported:
79 warnings.append(
80 f"CPython {major_minor} is outside the R2026a supported set."
81 )
82 if not exact_engine:
83 warnings.append(
84 "Engine package version does not match the reviewed R2026a package."
85 )
86 if not bits_ok:
87 warnings.append("R2026a Engine requires matching 64-bit Python.")
88 if not implementation_ok:
89 warnings.append("The reviewed interface supports CPython.")
90 launch_snippet = None
91 if args.include_launch_snippet:
92 launch_snippet = [
93 "import matlab.engine",
94 "engine = matlab.engine.start_matlab() # explicit MATLAB launch/license action",
95 "try:",
96 " pass # call one reviewed function with explicit nargout",
97 "finally:",
98 " engine.quit()",
99 ]
100 warnings.append(
101 "Launch snippet is informational and was not executed; review MATLAB "
102 "startup code and obtain explicit approval before using it."
103 )
104 report = {
105 "as_of": data["as_of"],
106 "engine_launch_explicit": bool(args.include_launch_snippet),
107 "engine_launch_snippet": launch_snippet,
108 "executes": False,
109 "implementation": args.implementation,
110 "install_plan_argv": [
111 "uv",
112 "pip",
113 "install",
114 f"matlabengine=={data['matlab_engine_package']['version']}",
115 ],
116 "license_status": args.license_status,
117 "matlab_installed": args.matlab_installed,
118 "matlab_release": release,
119 "network_accessed": False,
120 "ok": software_compatible,
121 "preinstalled_engine_path": (
122 "<matlabroot>/" + data["preinstalled_engine_relative_path"]
123 ),
124 "python_architecture_bits": args.python_bits,
125 "python_version_requested": requested_python,
126 "ready_to_launch": ready_to_launch,
127 "reviewed_engine_package": data["matlab_engine_package"],
128 "software_compatible": software_compatible,
129 "supported_python_versions": data["supported_python_versions"],
130 "tool": TOOL,
131 "warnings": warnings,
132 }
133 return report, 0 if software_compatible else 1
136def parser() -> argparse.ArgumentParser:
137 result = argparse.ArgumentParser(
138 description=(
139 "Check a requested CPython and MATLAB Engine version against the "
140 "bundled R2026a support record. No environment probing or launch occurs."
141 )
142 )
143 result.add_argument("--matlab-release", default="R2026a")
144 result.add_argument("--python-version", required=True)
145 result.add_argument("--engine-version", default="26.1.12")
146 result.add_argument("--python-bits", type=int, choices=(32, 64), default=64)
147 result.add_argument("--implementation", choices=("CPython",), default="CPython")
148 result.add_argument(
149 "--matlab-installed",
150 choices=("yes", "no", "unknown"),
151 default="unknown",
152 )
153 result.add_argument(
154 "--license-status",
155 choices=("confirmed", "unavailable", "unknown"),
156 default="unknown",
157 )
158 result.add_argument(
159 "--include-launch-snippet",
160 action="store_true",
161 help="include, but do not execute, an explicit Engine lifecycle snippet",
162 )
163 return result
166def main() -> int:
167 try:
168 report, status = plan(parser().parse_args())
169 emit_json(report)
170 return status
171 except CliError as exc:
172 return fail_json(TOOL, exc)
175if __name__ == "__main__":
176 sys.exit(main())