fix(macros): look devices up in dev, not dev.devices
The tensor-tomography branch of run_cont_grid_scan_for_table_row has never
worked. `dev` is already the device container -- bec_lib binds
dev = device_manager.devices in the client namespace -- so dev.devices[name]
asks DeviceContainer for a device literally called "devices" and its
__getattr__ raises DeviceConfigError before any motor moves.
Observed at the beamline on a tensor table using sgchi/sgphi as the rotation
axes:
--> 72 roty_motor = dev.devices[row["roty_axis"]]
DeviceConfigError: Device devices does not exist.
The failure is deterministic, so @scan_repeat(max_repeats=3, default=True)
retried it three times, and the surrounding except reported only "Error while
moving motors to starting position for sample ..." -- the traceback goes to
SciLog and nowhere else, which is why this survived undetected.
Adds tests/tests_macros, which had no equivalent: macros run in the client
namespace and are not executed by any test, so mistakes in them reach the
beamline unfiltered. The check parses each macro and flags dev.devices
attribute access rather than matching text, so comments and docstrings that
mention the pattern do not trip it. Verified to fail against the unfixed macro.
The rotation axes themselves are not hard-coded: rotx_axis/roty_axis are row
fields holding a device name chosen from the SAXS widget's positioner combo
boxes. Only the four row keys are fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148tn6uK6oiTH25mzLfJcyc
This commit is contained in:
@@ -69,11 +69,14 @@ def run_cont_grid_scan_for_table_row(
|
||||
try:
|
||||
# Move motors to the starting position of the scan
|
||||
move_command = []
|
||||
roty_motor = dev.devices[row["roty_axis"]]
|
||||
# NOTE `dev` IS the device container (bec_lib client binds
|
||||
# dev = device_manager.devices), so `dev.devices[...]` asks the container for a
|
||||
# device named "devices" and raises DeviceConfigError before any motor moves.
|
||||
roty_motor = dev[row["roty_axis"]]
|
||||
setpoint_roty = roty_motor.setpoint.get(cached=True)
|
||||
if not np.isclose(setpoint_roty, row["roty"]):
|
||||
move_command.extend([row["roty_axis"], row["roty"]])
|
||||
setpoint_rotx = dev.devices[row["rotx_axis"]].setpoint.get(cached=True)
|
||||
setpoint_rotx = dev[row["rotx_axis"]].setpoint.get(cached=True)
|
||||
if not np.isclose(setpoint_rotx, row["rotx"]):
|
||||
move_command.extend([row["rotx_axis"], row["rotx"]])
|
||||
# Wait for all motor movements to complete
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Static checks on the user macros.
|
||||
|
||||
Macros run in the BEC client namespace and are not covered by unit tests, so mistakes in
|
||||
them surface only at the beamline -- and, because several macros wrap their body in a
|
||||
try/except that reports to SciLog, often only as a generic message with the traceback
|
||||
discarded. These checks catch the failure modes that are decidable statically.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
MACRO_DIR = pathlib.Path(__file__).resolve().parents[2] / "csaxs_bec" / "macros"
|
||||
MACROS = sorted(p for p in MACRO_DIR.glob("*.py") if p.name != "__init__.py")
|
||||
|
||||
|
||||
def test_macro_directory_is_found():
|
||||
"""Guard against the glob silently matching nothing if the layout changes."""
|
||||
assert MACROS, f"no macros found under {MACRO_DIR}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("macro", MACROS, ids=lambda p: p.name)
|
||||
def test_macros_do_not_go_through_dev_devices(macro: pathlib.Path):
|
||||
"""`dev` is already the device container, so `dev.devices` cannot resolve.
|
||||
|
||||
bec_lib binds `dev = device_manager.devices` in the client namespace, and
|
||||
DeviceContainer.__getattr__ raises DeviceConfigError for an unknown attribute. So
|
||||
`dev.devices[name]` asks for a device literally called "devices" and fails before
|
||||
doing anything useful. Use `dev[name]`.
|
||||
"""
|
||||
tree = ast.parse(macro.read_text(), filename=str(macro))
|
||||
offenders = [
|
||||
node.lineno
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Attribute)
|
||||
and node.attr == "devices"
|
||||
and isinstance(node.value, ast.Name)
|
||||
and node.value.id == "dev"
|
||||
]
|
||||
assert not offenders, (
|
||||
f"{macro.name}: line(s) {offenders} use dev.devices -- `dev` is the device "
|
||||
"container itself, so this raises DeviceConfigError at runtime. Use dev[<name>]."
|
||||
)
|
||||
Reference in New Issue
Block a user