From d396692de571445537e617ac31627d7bd7ab84aa Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Tue, 9 Jun 2026 22:35:40 -0600 Subject: Fix custom_script execution + add templates for expression/function kinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - custom_script now uses exec-style (assign to `result`) matching the built-in templates — previously auto-wrapped in def compute() which had no return statement, causing state and channel vars to silently fail - function kind unchanged: auto-wraps body as def compute(x,t); use return - Add SCRIPT_TEMPLATES for expression, function, custom_script kinds; shown automatically when user switches to a script kind (replacing stale code from previous kind) - New channels default to template when no saved code exists Co-Authored-By: Claude Sonnet 4.6 --- core/signal_processor.py | 27 ++++++++++++++++++++++----- ui/code_templates.py | 43 ++++++++++++++++++++++++++++++++++++------- ui/windows/channels_window.py | 19 ++++++++++++------- 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/core/signal_processor.py b/core/signal_processor.py index 8ac4d8c..e2752cd 100644 --- a/core/signal_processor.py +++ b/core/signal_processor.py @@ -286,7 +286,8 @@ class DerivedChannel: self._fn = _expr_fn - elif self.kind in ("function", "custom_script"): + elif self.kind == "function": + # Auto-wrap body as def compute(x, t): ... return src = self.script if not src.strip().startswith("def compute"): src = "def compute(x, t):\n" + "\n".join( @@ -297,9 +298,8 @@ class DerivedChannel: _fn_ref = _exec_ns["compute"] _state = self._exec_state - def _script_fn(inputs, t, sv, - _f=_fn_ref, _ns=_exec_ns, _st=_state, _names=src_names): - # refresh mutable globals before each call so scripts see live values + def _fn_fn(inputs, t, sv, + _f=_fn_ref, _ns=_exec_ns, _st=_state, _names=src_names): _ns["vars"] = sv _ns["state"] = _st for i, name in enumerate(_names): @@ -307,7 +307,24 @@ class DerivedChannel: _ns[name] = inputs[i] return float(_f(inputs, t)) - self._fn = _script_fn + self._fn = _fn_fn + + elif self.kind == "custom_script": + # Exec-style: script assigns to `result` + code = compile(self.script, "", "exec") + _state = self._exec_state + + def _cs_fn(inputs, t, sv, _code=code, _names=src_names, _st=_state): + ns = {"x": inputs, "t": t, "math": math, "vars": sv, "state": _st} + for i, name in enumerate(_names): + if i < len(inputs): + ns[name] = inputs[i] + exec(_code, ns) + if "result" not in ns: + raise NameError("custom_script must assign to 'result'") + return float(ns["result"]) + + self._fn = _cs_fn else: # Built-in kinds handled in SignalProcessor._compute_derived diff --git a/ui/code_templates.py b/ui/code_templates.py index 0863e62..32332e9 100644 --- a/ui/code_templates.py +++ b/ui/code_templates.py @@ -1,18 +1,24 @@ """ ui/code_templates.py -Python code templates for built-in derived channel kinds. -Shown as read-only previews in the UI; clicking "Edit as Custom Script" -converts the channel to custom_script kind with the template pre-filled. +Code templates for derived channel kinds. -Templates use the enhanced expression namespace: - x — list of source values in order - — each source's channel_id as a named variable +BUILTIN_TEMPLATES — shown as read-only previews for built-in kinds. + "Edit as Custom Script" copies the template into custom_script. + Uses exec-style: assign to `result`. + +SCRIPT_TEMPLATES — default starter code shown when user picks a script kind. + expression → single Python expression (no assignment) + function → auto-wrapped as def compute(x, t): ... use return + custom_script → exec-style block, assign to `result` + +Namespace available in all kinds: + x — list of source values in source order + — each source's channel_id as a named variable (same as x[i]) t — elapsed time (seconds) math — Python math module vars — shared SignalProcessor._script_vars dict (read/write) state — per-channel persistent dict (survives between evaluations) - result — assign the computed value here (expression-style exec) """ BUILTIN_TEMPLATES: dict = { @@ -68,3 +74,26 @@ result = x[0] - x[1] result = sum(x) """, } + + +# Default starter code shown when user selects a script kind +SCRIPT_TEMPLATES: dict = { + + "expression": "x[0]", + + "function": """\ +# Function body — use 'return' to output the value. +# Named source vars, t, math, vars, state all available. +offset = vars.get("offset", 0) +return x[0] - offset +""", + + "custom_script": """\ +# Assign computed value to 'result'. +# Named source vars, t, math, vars, state all available. + +# Example: running zero-offset +zero = vars.get("zero", 0) +result = x[0] - zero +""", +} diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py index 266432b..597b0b2 100644 --- a/ui/windows/channels_window.py +++ b/ui/windows/channels_window.py @@ -618,8 +618,8 @@ _KIND_HELP = { "Example:\n offset = vars.get('zero', 0)\n return x[0] - offset" ), "custom_script": ( - "Full Python. Must define: def compute(x, t): ...\n" - "Available globals: x, signal names, t, math, vars, state" + "Multi-line Python block. Assign computed value to result.\n" + "Available: x, signal names, t, math, vars, state" ), } @@ -758,9 +758,11 @@ class DerivedBlock(QFrame): from ui.code_templates import BUILTIN_TEMPLATES txt = BUILTIN_TEMPLATES.get(self.dc.kind, "") elif self.dc.kind == "expression": - txt = self.dc.expression + from ui.code_templates import SCRIPT_TEMPLATES + txt = self.dc.expression or SCRIPT_TEMPLATES.get("expression", "") else: - txt = self.dc.script + from ui.code_templates import SCRIPT_TEMPLATES + txt = self.dc.script or SCRIPT_TEMPLATES.get(self.dc.kind, "") self._code.setPlainText(txt) code_lay.addWidget(self._code) # "Edit as Custom Script" button — only visible for built-in kinds @@ -875,16 +877,19 @@ class DerivedBlock(QFrame): # expression kind: label-only, no code injection (single-line expr) def _on_kind(self, idx: int): - from ui.code_templates import BUILTIN_TEMPLATES + from ui.code_templates import BUILTIN_TEMPLATES, SCRIPT_TEMPLATES k = _ALL_KINDS[idx] self.dc.kind = k self._help.setText(_KIND_HELP.get(k, "")) if k in ("derivative", "second_derivative") and not self._src_combos: self._add_src() if k in _BUILTIN: - # Reset to bare template first; _update_vars_hint will prepend comment + # Reset to bare template; _update_vars_hint will prepend variable comment self._code.setPlainText(BUILTIN_TEMPLATES.get(k, "")) - self._update_vars_hint() # also injects variable comment into code + elif k in _SCRIPT: + # Show starter template for script kinds (always replace on kind switch) + self._code.setPlainText(SCRIPT_TEMPLATES.get(k, "")) + self._update_vars_hint() self._update_visibility() def _on_wrt_changed(self, idx: int): -- cgit v1.2.3