From 8e8b8d9252ddb0f91e54b5c3b0cbee9d92b8f8c5 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 8 Jan 2026 15:15:43 +0000 Subject: [PATCH 01/16] first config parser --- .../common/beamlines/config_generator.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/dodal/common/beamlines/config_generator.py diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py new file mode 100644 index 0000000000..4566f07ec6 --- /dev/null +++ b/src/dodal/common/beamlines/config_generator.py @@ -0,0 +1,68 @@ +import yaml + + +def generate_python_from_yaml(yaml_path): + with open(yaml_path) as f: + data = yaml.safe_load(f) + + code = f'"""\nnote:\n{data["note"]}"""\n\n' + + code += ( + "from dodal.common.beamlines.beamline_utils import device_factory\n" + "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline\n" + "from dodal.devices.current_amplifiers import CurrentAmpDet\n" + "from dodal.devices.i10 import (\n" + " I10Diagnostic,\n" + " I10Diagnostic5ADet,\n" + " I10Slits,\n" + " I10SlitsDrainCurrent,\n" + " PiezoMirror,\n" + ")\n" + "from dodal.devices.i10.diagnostics import I10Diagnostic, I10Diagnostic5ADet\n" + "from dodal.devices.i10.rasor.rasor_current_amp import RasorFemto, RasorSR570\n" + "from dodal.devices.i10.rasor.rasor_motors import (\n" + " DetSlits,\n" + " Diffractometer,\n" + " PaStage,\n" + ")\n" + "from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1\n" + "from dodal.devices.motors import XYStage, XYZStage\n" + "from dodal.devices.temperture_controller import (\n" + " Lakeshore340,\n" + ")\n" + "from dodal.log import set_beamline as set_log_beamline\n" + "from dodal.utils import BeamlinePrefix, get_beamline_name\n\n" + ) + + bl = data["beamline"] + code += f'BL = get_beamline_name("{bl}")\n' + code += ( + "set_log_beamline(BL)\nset_utils_beamline(BL)\nPREFIX = BeamlinePrefix(BL)\n" + ) + + for section in data["sections"]: + code += f'\n"""{section["title"]}"""\n' + + for dev in section["devices"]: + f_args = dev.get("factory_args", {}) + factory_params = [] + for k, v in f_args.items(): + val = str(v) if not isinstance(v, str) else f"'{v}'" + factory_params.append(f"{k}={val}") + + factory_line = f"@device_factory({', '.join(factory_params)})" + if "params" in dev: + args = ",\n ".join( + [f"{k}={v}" for k, v in dev["params"].items()] + ) + body = f"{dev['type']}(\n {args},\n )" + else: + body = f'{dev["type"]}(prefix=f"{dev["prefix"]}")' + + code += ( + f"\n{factory_line}\n" + f"def {dev['func']}() -> {dev['type']}:\n" + f" return {body}\n" + ) + + return code From f06f4ac15549066a776d9c690b9993cbf9c32da7 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 8 Jan 2026 16:28:54 +0000 Subject: [PATCH 02/16] split into groups and allow multiple with a test example --- src/dodal/beamlines/i10/detectors.yaml | 24 ++++ src/dodal/beamlines/i10/mirros.yaml | 14 +++ src/dodal/beamlines/i10/test.py | 9 ++ .../common/beamlines/config_generator.py | 110 ++++++++++-------- 4 files changed, 108 insertions(+), 49 deletions(-) create mode 100644 src/dodal/beamlines/i10/detectors.yaml create mode 100644 src/dodal/beamlines/i10/mirros.yaml create mode 100644 src/dodal/beamlines/i10/test.py diff --git a/src/dodal/beamlines/i10/detectors.yaml b/src/dodal/beamlines/i10/detectors.yaml new file mode 100644 index 0000000000..708b8c86ef --- /dev/null +++ b/src/dodal/beamlines/i10/detectors.yaml @@ -0,0 +1,24 @@ +- section: "Rasor Detectors" + func: rasor_det_scalers + type: RasorScalerCard1 + import_from: "dodal.devices.i10.rasor.rasor_scaler_cards" + params: + prefix: "ME01D-EA-SCLR-01:SCALER1" + factory_args: { mock: true } + +- section: "Rasor Detectors" + func: rasor_sr570 + type: RasorSR570 + import_from: "dodal.devices.i10.rasor.rasor_current_amp" + params: + prefix: "ME01D-EA-IAMP" + factory_args: { mock: true } + +- section: "Rasor Detectors" + func: rasor_sr570_pa_scaler_det + type: CurrentAmpDet + import_from: "dodal.devices.current_amplifiers" + params: + current_amp: "rasor_sr570().ca1" + counter: "rasor_det_scalers().det" + factory_args: { mock: true, skip: true } diff --git a/src/dodal/beamlines/i10/mirros.yaml b/src/dodal/beamlines/i10/mirros.yaml new file mode 100644 index 0000000000..f6edb979a9 --- /dev/null +++ b/src/dodal/beamlines/i10/mirros.yaml @@ -0,0 +1,14 @@ +- section: "Mirrors" + func: focusing_mirror + type: PiezoMirror + import_from: "dodal.devices.i10" + params: + prefix: "{PREFIX.beamline_prefix}-OP-FOCS-01:" + factory_args: { mock: true } + +- section: "Mirrors" + func: second_mirror + type: PiezoMirror + import_from: "dodal.devices.i10" + params: + prefix: "{PREFIX.beamline_prefix}-OP-FOCS-02:" diff --git a/src/dodal/beamlines/i10/test.py b/src/dodal/beamlines/i10/test.py new file mode 100644 index 0000000000..58fa5352f4 --- /dev/null +++ b/src/dodal/beamlines/i10/test.py @@ -0,0 +1,9 @@ +from dodal.common.beamlines.config_generator import ( + write_beamline_code_to_file, +) + +if __name__ == "__main__": + write_beamline_code_to_file( + "/workspaces/dodal/src/dodal/beamlines/i10/", + "/workspaces/dodal/src/dodal/beamlines/i10/i10.py", + ) diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 4566f07ec6..bd2509a4c9 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -1,68 +1,80 @@ +import glob +import os +import subprocess +from collections import defaultdict + import yaml -def generate_python_from_yaml(yaml_path): - with open(yaml_path) as f: - data = yaml.safe_load(f) +def format_value(v): + if isinstance(v, str): + if "(" in v or ")." in v or v.startswith("PREFIX"): + return v + return f"'{v}'" + return repr(v) + + +def generate_beamline_code(device_dir): + sections = defaultdict(list) + needed_imports = defaultdict(set) - code = f'"""\nnote:\n{data["note"]}"""\n\n' + yaml_files = glob.glob(os.path.join(device_dir, "*.yaml")) - code += ( + for y_file in yaml_files: + with open(y_file) as f: + content = yaml.safe_load(f) + device_list = content if isinstance(content, list) else [content] + + for dev in device_list: + sections[dev.get("section", "Other")].append(dev) + if dev.get("import_from") and dev.get("type"): + needed_imports[dev["import_from"]].add(dev["type"]) + + import_block = ( "from dodal.common.beamlines.beamline_utils import device_factory\n" "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline\n" - "from dodal.devices.current_amplifiers import CurrentAmpDet\n" - "from dodal.devices.i10 import (\n" - " I10Diagnostic,\n" - " I10Diagnostic5ADet,\n" - " I10Slits,\n" - " I10SlitsDrainCurrent,\n" - " PiezoMirror,\n" - ")\n" - "from dodal.devices.i10.diagnostics import I10Diagnostic, I10Diagnostic5ADet\n" - "from dodal.devices.i10.rasor.rasor_current_amp import RasorFemto, RasorSR570\n" - "from dodal.devices.i10.rasor.rasor_motors import (\n" - " DetSlits,\n" - " Diffractometer,\n" - " PaStage,\n" - ")\n" - "from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1\n" - "from dodal.devices.motors import XYStage, XYZStage\n" - "from dodal.devices.temperture_controller import (\n" - " Lakeshore340,\n" - ")\n" "from dodal.log import set_beamline as set_log_beamline\n" - "from dodal.utils import BeamlinePrefix, get_beamline_name\n\n" + "from dodal.utils import BeamlinePrefix, get_beamline_name\n" ) + for module, classes in sorted(needed_imports.items()): + cls_str = ", ".join(sorted(classes)) + import_block += f"from {module} import {cls_str}\n" - bl = data["beamline"] - code += f'BL = get_beamline_name("{bl}")\n' - code += ( - "set_log_beamline(BL)\nset_utils_beamline(BL)\nPREFIX = BeamlinePrefix(BL)\n" - ) + code = f'"""\nGenerated Beamline Configuration\n"""\n\n{import_block}\n' + code += 'BL = get_beamline_name("i10")\nset_log_beamline(BL)\nset_utils_beamline(BL)\nPREFIX = BeamlinePrefix(BL)\n' - for section in data["sections"]: - code += f'\n"""{section["title"]}"""\n' - - for dev in section["devices"]: + for section_name, devices in sections.items(): + code += f'\n""" {section_name} """\n' + for dev in devices: f_args = dev.get("factory_args", {}) - factory_params = [] - for k, v in f_args.items(): - val = str(v) if not isinstance(v, str) else f"'{v}'" - factory_params.append(f"{k}={val}") - - factory_line = f"@device_factory({', '.join(factory_params)})" - if "params" in dev: - args = ",\n ".join( - [f"{k}={v}" for k, v in dev["params"].items()] - ) - body = f"{dev['type']}(\n {args},\n )" - else: - body = f'{dev["type"]}(prefix=f"{dev["prefix"]}")' + f_str = ", ".join([f"{k}={repr(v)}" for k, v in f_args.items()]) + + args = ",\n ".join( + [f"{k}={format_value(v)}" for k, v in dev["params"].items()] + ) + body = f"{dev['type']}(\n {args}\n )" code += ( - f"\n{factory_line}\n" + f"\n@device_factory({f_str})\n" f"def {dev['func']}() -> {dev['type']}:\n" f" return {body}\n" ) return code + + +def write_beamline_code_to_file(device_dir, output_file): + code = generate_beamline_code(device_dir) + with open(output_file, "w") as f: + f.write(code) + print(f"Ruffing {output_file}...") + try: + subprocess.run( + ["ruff", "check", "--select", "I", "--fix", output_file], check=True + ) + subprocess.run(["ruff", "format", output_file], check=True) + print(f"Done! {output_file} is ready.") + except subprocess.CalledProcessError as e: + print(f"Ruff failed: {e}") + except FileNotFoundError: + print("Ruff not found in environment. Please install with 'pip install ruff'.") From ef5549006142af91a072af82756994ef46a32b24 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 9 Jan 2026 14:22:12 +0000 Subject: [PATCH 03/16] Make a master firle to control what device to generate --- src/dodal/beamlines/i10/test.py | 9 -- src/dodal/beamlines/i10_2.py | 53 ++++++++ src/dodal/beamlines/i10_config/__init__.py | 0 src/dodal/beamlines/i10_config/config.yaml | 20 +++ .../devices}/detectors.yaml | 0 .../devices/mirrors.yaml} | 8 +- src/dodal/beamlines/i10_config/test.py | 8 ++ .../common/beamlines/config_generator.py | 115 ++++++++++++------ 8 files changed, 160 insertions(+), 53 deletions(-) delete mode 100644 src/dodal/beamlines/i10/test.py create mode 100644 src/dodal/beamlines/i10_2.py create mode 100644 src/dodal/beamlines/i10_config/__init__.py create mode 100644 src/dodal/beamlines/i10_config/config.yaml rename src/dodal/beamlines/{i10 => i10_config/devices}/detectors.yaml (100%) rename src/dodal/beamlines/{i10/mirros.yaml => i10_config/devices/mirrors.yaml} (55%) create mode 100644 src/dodal/beamlines/i10_config/test.py diff --git a/src/dodal/beamlines/i10/test.py b/src/dodal/beamlines/i10/test.py deleted file mode 100644 index 58fa5352f4..0000000000 --- a/src/dodal/beamlines/i10/test.py +++ /dev/null @@ -1,9 +0,0 @@ -from dodal.common.beamlines.config_generator import ( - write_beamline_code_to_file, -) - -if __name__ == "__main__": - write_beamline_code_to_file( - "/workspaces/dodal/src/dodal/beamlines/i10/", - "/workspaces/dodal/src/dodal/beamlines/i10/i10.py", - ) diff --git a/src/dodal/beamlines/i10_2.py b/src/dodal/beamlines/i10_2.py new file mode 100644 index 0000000000..84c19773fb --- /dev/null +++ b/src/dodal/beamlines/i10_2.py @@ -0,0 +1,53 @@ +""" +Generated on: 2026-01-09 14:17:48 +Beamline: i10 + +note: + +""" + +from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline +from dodal.device_manager import DeviceManager +from dodal.devices.current_amplifiers import CurrentAmpDet +from dodal.devices.i10 import PiezoMirror +from dodal.devices.i10.rasor.rasor_current_amp import RasorSR570 +from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1 +from dodal.log import set_beamline as set_log_beamline +from dodal.utils import BeamlinePrefix, get_beamline_name + +devices = DeviceManager() +BL = get_beamline_name("i10") +set_log_beamline(BL) +set_utils_beamline(BL) +PREFIX = BeamlinePrefix(BL) + + +""" Mirrors """ + + +@devices.factory() +def focusing_mirror() -> PiezoMirror: + return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-FOCS-01:") + + +@devices.factory() +def switching_mirror() -> PiezoMirror: + return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-SWTCH-01:") + + +""" Rasor Detectors """ + + +@devices.factory(mock=True) +def rasor_det_scalers() -> RasorScalerCard1: + return RasorScalerCard1(prefix="ME01D-EA-SCLR-01:SCALER1") + + +@devices.factory(mock=True) +def rasor_sr570() -> RasorSR570: + return RasorSR570(prefix="ME01D-EA-IAMP") + + +@devices.factory(mock=True, skip=True) +def rasor_sr570_pa_scaler_det() -> CurrentAmpDet: + return CurrentAmpDet(current_amp=rasor_sr570().ca1, counter=rasor_det_scalers().det) diff --git a/src/dodal/beamlines/i10_config/__init__.py b/src/dodal/beamlines/i10_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/dodal/beamlines/i10_config/config.yaml b/src/dodal/beamlines/i10_config/config.yaml new file mode 100644 index 0000000000..0e6ba5646d --- /dev/null +++ b/src/dodal/beamlines/i10_config/config.yaml @@ -0,0 +1,20 @@ +beamline: "i10" +output_file: "/workspaces/dodal/src/dodal/beamlines/i10_2.py" + +base_imports: + - "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline" + - "from dodal.device_manager import DeviceManager" + - "from dodal.log import set_beamline as set_log_beamline" + - "from dodal.utils import BeamlinePrefix, get_beamline_name" + +setup_script: | + devices = DeviceManager() + BL = get_beamline_name('{beamline}') + set_log_beamline(BL) + set_utils_beamline(BL) + PREFIX = BeamlinePrefix(BL) + +# Only these files will be processed +device_files: + - "devices/mirrors.yaml" + - "devices/detectors.yaml" diff --git a/src/dodal/beamlines/i10/detectors.yaml b/src/dodal/beamlines/i10_config/devices/detectors.yaml similarity index 100% rename from src/dodal/beamlines/i10/detectors.yaml rename to src/dodal/beamlines/i10_config/devices/detectors.yaml diff --git a/src/dodal/beamlines/i10/mirros.yaml b/src/dodal/beamlines/i10_config/devices/mirrors.yaml similarity index 55% rename from src/dodal/beamlines/i10/mirros.yaml rename to src/dodal/beamlines/i10_config/devices/mirrors.yaml index f6edb979a9..9f41a4fd95 100644 --- a/src/dodal/beamlines/i10/mirros.yaml +++ b/src/dodal/beamlines/i10_config/devices/mirrors.yaml @@ -3,12 +3,12 @@ type: PiezoMirror import_from: "dodal.devices.i10" params: - prefix: "{PREFIX.beamline_prefix}-OP-FOCS-01:" - factory_args: { mock: true } + prefix: 'f"{PREFIX.beamline_prefix}-OP-FOCS-01:"' - section: "Mirrors" - func: second_mirror + func: switching_mirror type: PiezoMirror import_from: "dodal.devices.i10" params: - prefix: "{PREFIX.beamline_prefix}-OP-FOCS-02:" + prefix: "f'{PREFIX.beamline_prefix}-OP-SWTCH-01:'" + factory_args: {} diff --git a/src/dodal/beamlines/i10_config/test.py b/src/dodal/beamlines/i10_config/test.py new file mode 100644 index 0000000000..cb635a02ad --- /dev/null +++ b/src/dodal/beamlines/i10_config/test.py @@ -0,0 +1,8 @@ +from dodal.common.beamlines.config_generator import ( + generate_beamline, +) + +if __name__ == "__main__": + generate_beamline( + "/workspaces/dodal/src/dodal/beamlines/i10_config/", + ) diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index bd2509a4c9..49ab9bf01a 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -1,80 +1,115 @@ -import glob import os import subprocess from collections import defaultdict +from datetime import datetime +from typing import Any import yaml +from dodal.log import LOGGER -def format_value(v): + +def format_value(v: Any) -> str: + """ + Handles literals, f-strings, and backtick f-strings. + """ if isinstance(v, str): - if "(" in v or ")." in v or v.startswith("PREFIX"): + # Code pass-through (function calls, attribute access, standard f-strings) + if "(" in v or ")." in v or v.startswith('f"') or v.startswith("f'"): return v return f"'{v}'" return repr(v) -def generate_beamline_code(device_dir): +def generate_beamline(config_dir: str): + # Generate beamline configuration code based on YAML files + config_file = os.path.join(config_dir, "config.yaml") + with open(config_file) as f: + master: dict = yaml.safe_load(f) + + beamline = master["beamline"] + output_file = master["output_file"] + device_files = master.get("device_files", []) + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") sections = defaultdict(list) needed_imports = defaultdict(set) - - yaml_files = glob.glob(os.path.join(device_dir, "*.yaml")) - - for y_file in yaml_files: - with open(y_file) as f: - content = yaml.safe_load(f) - device_list = content if isinstance(content, list) else [content] + seen_functions = set() + + # Generate devices from YAML files + for y_path in device_files: + full_device_path = os.path.join(config_dir, y_path) + if not os.path.exists(full_device_path): + LOGGER.warning(msg=f"Defined device file not found: {full_device_path}") + continue + + with open(full_device_path) as f: + content: dict = yaml.safe_load(f) + device_list: list[dict] = ( + content if isinstance(content, list) else [content] + ) for dev in device_list: + fname = dev.get("func") + if fname in seen_functions: + raise ValueError( + f"Duplicate function name detected: '{fname}' in {y_path}" + ) + seen_functions.add(fname) + sections[dev.get("section", "Other")].append(dev) if dev.get("import_from") and dev.get("type"): needed_imports[dev["import_from"]].add(dev["type"]) - import_block = ( - "from dodal.common.beamlines.beamline_utils import device_factory\n" - "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline\n" - "from dodal.log import set_beamline as set_log_beamline\n" - "from dodal.utils import BeamlinePrefix, get_beamline_name\n" + # Build Note and Import Block + code = ( + '"""\n' + f"Generated on: {timestamp}\n" + f"Beamline: {beamline}\n\n" + "note:\n" + f"{master.get('note', '')}\n" + '"""\n\n' ) + + code_lines: list = master["base_imports"][:] for module, classes in sorted(needed_imports.items()): cls_str = ", ".join(sorted(classes)) - import_block += f"from {module} import {cls_str}\n" + code_lines.append(f"from {module} import {cls_str}") + + code += "\n".join(code_lines) + "\n\n" - code = f'"""\nGenerated Beamline Configuration\n"""\n\n{import_block}\n' - code += 'BL = get_beamline_name("i10")\nset_log_beamline(BL)\nset_utils_beamline(BL)\nPREFIX = BeamlinePrefix(BL)\n' + # Build Setup Script + code += master["setup_script"].format(beamline=beamline) + "\n" - for section_name, devices in sections.items(): - code += f'\n""" {section_name} """\n' - for dev in devices: + # Build Devices + for section_name in sorted(sections.keys()): + code += f'\n\n""" {section_name} """\n' + for dev in sections[section_name]: f_args = dev.get("factory_args", {}) f_str = ", ".join([f"{k}={repr(v)}" for k, v in f_args.items()]) - args = ",\n ".join( - [f"{k}={format_value(v)}" for k, v in dev["params"].items()] - ) - body = f"{dev['type']}(\n {args}\n )" + if "params" in dev: + args = ",\n ".join( + [f"{k}={format_value(v)}" for k, v in dev["params"].items()] + ) + body = f"{dev['type']}(\n {args}\n )" + else: + body = f"{dev['type']}()" code += ( - f"\n@device_factory({f_str})\n" - f"def {dev['func']}() -> {dev['type']}:\n" - f" return {body}\n" + f"\n@devices.factory({f_str})" + f"\ndef {dev['func']}() -> {dev['type']}:" + f"\n return {body}\n" ) - return code - - -def write_beamline_code_to_file(device_dir, output_file): - code = generate_beamline_code(device_dir) + # Write and Ruff with open(output_file, "w") as f: f.write(code) - print(f"Ruffing {output_file}...") + try: subprocess.run( ["ruff", "check", "--select", "I", "--fix", output_file], check=True ) subprocess.run(["ruff", "format", output_file], check=True) - print(f"Done! {output_file} is ready.") - except subprocess.CalledProcessError as e: - print(f"Ruff failed: {e}") - except FileNotFoundError: - print("Ruff not found in environment. Please install with 'pip install ruff'.") + print(f"Generated {output_file} successfully.") + except Exception as e: + print(f"File saved to {output_file}, but Ruff failed: {e}") From 96474bf78ed15713834f2787c4bfd28502443c5c Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 9 Jan 2026 15:38:58 +0000 Subject: [PATCH 04/16] moving test out of beamline --- src/dodal/beamlines/i10_2.py | 2 +- src/dodal/common/beamlines/config_generator.py | 2 +- src/dodal/{beamlines => devices/i10}/i10_config/__init__.py | 0 src/dodal/{beamlines => devices/i10}/i10_config/config.yaml | 0 .../i10}/i10_config/devices/detectors.yaml | 0 .../{beamlines => devices/i10}/i10_config/devices/mirrors.yaml | 0 src/dodal/{beamlines => devices/i10}/i10_config/test.py | 2 +- 7 files changed, 3 insertions(+), 3 deletions(-) rename src/dodal/{beamlines => devices/i10}/i10_config/__init__.py (100%) rename src/dodal/{beamlines => devices/i10}/i10_config/config.yaml (100%) rename src/dodal/{beamlines => devices/i10}/i10_config/devices/detectors.yaml (100%) rename src/dodal/{beamlines => devices/i10}/i10_config/devices/mirrors.yaml (100%) rename src/dodal/{beamlines => devices/i10}/i10_config/test.py (68%) diff --git a/src/dodal/beamlines/i10_2.py b/src/dodal/beamlines/i10_2.py index 84c19773fb..7a4de8ee3e 100644 --- a/src/dodal/beamlines/i10_2.py +++ b/src/dodal/beamlines/i10_2.py @@ -1,5 +1,5 @@ """ -Generated on: 2026-01-09 14:17:48 +Generated on: 2026-01-09 15:37:49 Beamline: i10 note: diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 49ab9bf01a..a964c2c32d 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -21,7 +21,7 @@ def format_value(v: Any) -> str: return repr(v) -def generate_beamline(config_dir: str): +def generate_beamline(config_dir: str) -> None: # Generate beamline configuration code based on YAML files config_file = os.path.join(config_dir, "config.yaml") with open(config_file) as f: diff --git a/src/dodal/beamlines/i10_config/__init__.py b/src/dodal/devices/i10/i10_config/__init__.py similarity index 100% rename from src/dodal/beamlines/i10_config/__init__.py rename to src/dodal/devices/i10/i10_config/__init__.py diff --git a/src/dodal/beamlines/i10_config/config.yaml b/src/dodal/devices/i10/i10_config/config.yaml similarity index 100% rename from src/dodal/beamlines/i10_config/config.yaml rename to src/dodal/devices/i10/i10_config/config.yaml diff --git a/src/dodal/beamlines/i10_config/devices/detectors.yaml b/src/dodal/devices/i10/i10_config/devices/detectors.yaml similarity index 100% rename from src/dodal/beamlines/i10_config/devices/detectors.yaml rename to src/dodal/devices/i10/i10_config/devices/detectors.yaml diff --git a/src/dodal/beamlines/i10_config/devices/mirrors.yaml b/src/dodal/devices/i10/i10_config/devices/mirrors.yaml similarity index 100% rename from src/dodal/beamlines/i10_config/devices/mirrors.yaml rename to src/dodal/devices/i10/i10_config/devices/mirrors.yaml diff --git a/src/dodal/beamlines/i10_config/test.py b/src/dodal/devices/i10/i10_config/test.py similarity index 68% rename from src/dodal/beamlines/i10_config/test.py rename to src/dodal/devices/i10/i10_config/test.py index cb635a02ad..c03d1f8df4 100644 --- a/src/dodal/beamlines/i10_config/test.py +++ b/src/dodal/devices/i10/i10_config/test.py @@ -4,5 +4,5 @@ if __name__ == "__main__": generate_beamline( - "/workspaces/dodal/src/dodal/beamlines/i10_config/", + "/workspaces/dodal/src/dodal/devices/i10/i10_config/", ) From 37d704c339a3f5dc904487817b1706788fd1411d Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 9 Jan 2026 17:06:41 +0000 Subject: [PATCH 05/16] make dodal connect works --- src/dodal/beamlines/i10_2.py | 58 +++---------------- .../common/beamlines/config_generator.py | 8 +-- src/dodal/devices/i10/i10_config/test.py | 8 --- 3 files changed, 11 insertions(+), 63 deletions(-) delete mode 100644 src/dodal/devices/i10/i10_config/test.py diff --git a/src/dodal/beamlines/i10_2.py b/src/dodal/beamlines/i10_2.py index 7a4de8ee3e..2ac4151817 100644 --- a/src/dodal/beamlines/i10_2.py +++ b/src/dodal/beamlines/i10_2.py @@ -1,53 +1,13 @@ -""" -Generated on: 2026-01-09 15:37:49 -Beamline: i10 +from dodal.common.beamlines.config_generator import generate_beamline -note: +CONFIG_DIR = "/workspaces/dodal/src/dodal/devices/i10/i10_config/" -""" +try: + code = generate_beamline(CONFIG_DIR) + exec(code, globals()) -from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline -from dodal.device_manager import DeviceManager -from dodal.devices.current_amplifiers import CurrentAmpDet -from dodal.devices.i10 import PiezoMirror -from dodal.devices.i10.rasor.rasor_current_amp import RasorSR570 -from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1 -from dodal.log import set_beamline as set_log_beamline -from dodal.utils import BeamlinePrefix, get_beamline_name +except Exception as e: + import logging -devices = DeviceManager() -BL = get_beamline_name("i10") -set_log_beamline(BL) -set_utils_beamline(BL) -PREFIX = BeamlinePrefix(BL) - - -""" Mirrors """ - - -@devices.factory() -def focusing_mirror() -> PiezoMirror: - return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-FOCS-01:") - - -@devices.factory() -def switching_mirror() -> PiezoMirror: - return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-SWTCH-01:") - - -""" Rasor Detectors """ - - -@devices.factory(mock=True) -def rasor_det_scalers() -> RasorScalerCard1: - return RasorScalerCard1(prefix="ME01D-EA-SCLR-01:SCALER1") - - -@devices.factory(mock=True) -def rasor_sr570() -> RasorSR570: - return RasorSR570(prefix="ME01D-EA-IAMP") - - -@devices.factory(mock=True, skip=True) -def rasor_sr570_pa_scaler_det() -> CurrentAmpDet: - return CurrentAmpDet(current_amp=rasor_sr570().ca1, counter=rasor_det_scalers().det) + logging.getLogger("dodal").error(f"Failed to generate beamline from YAML: {e}") + raise diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index a964c2c32d..5414b93030 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -21,7 +21,7 @@ def format_value(v: Any) -> str: return repr(v) -def generate_beamline(config_dir: str) -> None: +def generate_beamline(config_dir: str) -> str: # Generate beamline configuration code based on YAML files config_file = os.path.join(config_dir, "config.yaml") with open(config_file) as f: @@ -100,11 +100,6 @@ def generate_beamline(config_dir: str) -> None: f"\ndef {dev['func']}() -> {dev['type']}:" f"\n return {body}\n" ) - - # Write and Ruff - with open(output_file, "w") as f: - f.write(code) - try: subprocess.run( ["ruff", "check", "--select", "I", "--fix", output_file], check=True @@ -113,3 +108,4 @@ def generate_beamline(config_dir: str) -> None: print(f"Generated {output_file} successfully.") except Exception as e: print(f"File saved to {output_file}, but Ruff failed: {e}") + return code diff --git a/src/dodal/devices/i10/i10_config/test.py b/src/dodal/devices/i10/i10_config/test.py deleted file mode 100644 index c03d1f8df4..0000000000 --- a/src/dodal/devices/i10/i10_config/test.py +++ /dev/null @@ -1,8 +0,0 @@ -from dodal.common.beamlines.config_generator import ( - generate_beamline, -) - -if __name__ == "__main__": - generate_beamline( - "/workspaces/dodal/src/dodal/devices/i10/i10_config/", - ) From d5e27253c645feadb3d3bdedf93c800730976e8c Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 9 Jan 2026 17:40:18 +0000 Subject: [PATCH 06/16] get path from folder name --- src/dodal/beamlines/configs/i10_2/__init__.py | 0 src/dodal/beamlines/configs/i10_2/config.yaml | 20 ++++++++++++++++ .../configs/i10_2/devices/detectors.yaml | 24 +++++++++++++++++++ .../configs/i10_2/devices/mirrors.yaml | 14 +++++++++++ src/dodal/beamlines/i10_2.py | 6 ++++- 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/dodal/beamlines/configs/i10_2/__init__.py create mode 100644 src/dodal/beamlines/configs/i10_2/config.yaml create mode 100644 src/dodal/beamlines/configs/i10_2/devices/detectors.yaml create mode 100644 src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml diff --git a/src/dodal/beamlines/configs/i10_2/__init__.py b/src/dodal/beamlines/configs/i10_2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/dodal/beamlines/configs/i10_2/config.yaml b/src/dodal/beamlines/configs/i10_2/config.yaml new file mode 100644 index 0000000000..0e6ba5646d --- /dev/null +++ b/src/dodal/beamlines/configs/i10_2/config.yaml @@ -0,0 +1,20 @@ +beamline: "i10" +output_file: "/workspaces/dodal/src/dodal/beamlines/i10_2.py" + +base_imports: + - "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline" + - "from dodal.device_manager import DeviceManager" + - "from dodal.log import set_beamline as set_log_beamline" + - "from dodal.utils import BeamlinePrefix, get_beamline_name" + +setup_script: | + devices = DeviceManager() + BL = get_beamline_name('{beamline}') + set_log_beamline(BL) + set_utils_beamline(BL) + PREFIX = BeamlinePrefix(BL) + +# Only these files will be processed +device_files: + - "devices/mirrors.yaml" + - "devices/detectors.yaml" diff --git a/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml b/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml new file mode 100644 index 0000000000..708b8c86ef --- /dev/null +++ b/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml @@ -0,0 +1,24 @@ +- section: "Rasor Detectors" + func: rasor_det_scalers + type: RasorScalerCard1 + import_from: "dodal.devices.i10.rasor.rasor_scaler_cards" + params: + prefix: "ME01D-EA-SCLR-01:SCALER1" + factory_args: { mock: true } + +- section: "Rasor Detectors" + func: rasor_sr570 + type: RasorSR570 + import_from: "dodal.devices.i10.rasor.rasor_current_amp" + params: + prefix: "ME01D-EA-IAMP" + factory_args: { mock: true } + +- section: "Rasor Detectors" + func: rasor_sr570_pa_scaler_det + type: CurrentAmpDet + import_from: "dodal.devices.current_amplifiers" + params: + current_amp: "rasor_sr570().ca1" + counter: "rasor_det_scalers().det" + factory_args: { mock: true, skip: true } diff --git a/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml b/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml new file mode 100644 index 0000000000..9f41a4fd95 --- /dev/null +++ b/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml @@ -0,0 +1,14 @@ +- section: "Mirrors" + func: focusing_mirror + type: PiezoMirror + import_from: "dodal.devices.i10" + params: + prefix: 'f"{PREFIX.beamline_prefix}-OP-FOCS-01:"' + +- section: "Mirrors" + func: switching_mirror + type: PiezoMirror + import_from: "dodal.devices.i10" + params: + prefix: "f'{PREFIX.beamline_prefix}-OP-SWTCH-01:'" + factory_args: {} diff --git a/src/dodal/beamlines/i10_2.py b/src/dodal/beamlines/i10_2.py index 2ac4151817..920365628a 100644 --- a/src/dodal/beamlines/i10_2.py +++ b/src/dodal/beamlines/i10_2.py @@ -1,6 +1,10 @@ +import os + from dodal.common.beamlines.config_generator import generate_beamline -CONFIG_DIR = "/workspaces/dodal/src/dodal/devices/i10/i10_config/" +current_dir = os.path.dirname(__file__) +beamline_name = os.path.splitext(os.path.basename(__file__))[0] +CONFIG_DIR = os.path.join(current_dir, "configs", beamline_name) try: code = generate_beamline(CONFIG_DIR) From 6ece75b73bb9a7e268028d1d85068a1979d43481 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Mon, 12 Jan 2026 11:50:41 +0000 Subject: [PATCH 07/16] change func to device --- src/dodal/beamlines/configs/i10_2/devices/detectors.yaml | 6 +++--- src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml | 4 ++-- src/dodal/common/beamlines/config_generator.py | 7 ++++--- src/dodal/devices/i10/i10_config/devices/detectors.yaml | 6 +++--- src/dodal/devices/i10/i10_config/devices/mirrors.yaml | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml b/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml index 708b8c86ef..7cf3f4509a 100644 --- a/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml +++ b/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml @@ -1,5 +1,5 @@ - section: "Rasor Detectors" - func: rasor_det_scalers + device: rasor_det_scalers type: RasorScalerCard1 import_from: "dodal.devices.i10.rasor.rasor_scaler_cards" params: @@ -7,7 +7,7 @@ factory_args: { mock: true } - section: "Rasor Detectors" - func: rasor_sr570 + device: rasor_sr570 type: RasorSR570 import_from: "dodal.devices.i10.rasor.rasor_current_amp" params: @@ -15,7 +15,7 @@ factory_args: { mock: true } - section: "Rasor Detectors" - func: rasor_sr570_pa_scaler_det + device: rasor_sr570_pa_scaler_det type: CurrentAmpDet import_from: "dodal.devices.current_amplifiers" params: diff --git a/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml b/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml index 9f41a4fd95..b05209364e 100644 --- a/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml +++ b/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml @@ -1,12 +1,12 @@ - section: "Mirrors" - func: focusing_mirror + device: focusing_mirror type: PiezoMirror import_from: "dodal.devices.i10" params: prefix: 'f"{PREFIX.beamline_prefix}-OP-FOCS-01:"' - section: "Mirrors" - func: switching_mirror + device: switching_mirror type: PiezoMirror import_from: "dodal.devices.i10" params: diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 5414b93030..3fae1a4efd 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -47,9 +47,10 @@ def generate_beamline(config_dir: str) -> str: device_list: list[dict] = ( content if isinstance(content, list) else [content] ) - + print(content, device_list) for dev in device_list: - fname = dev.get("func") + fname = dev.get("device") + print(fname) if fname in seen_functions: raise ValueError( f"Duplicate function name detected: '{fname}' in {y_path}" @@ -97,7 +98,7 @@ def generate_beamline(config_dir: str) -> str: code += ( f"\n@devices.factory({f_str})" - f"\ndef {dev['func']}() -> {dev['type']}:" + f"\ndef {dev['device']}() -> {dev['type']}:" f"\n return {body}\n" ) try: diff --git a/src/dodal/devices/i10/i10_config/devices/detectors.yaml b/src/dodal/devices/i10/i10_config/devices/detectors.yaml index 708b8c86ef..7cf3f4509a 100644 --- a/src/dodal/devices/i10/i10_config/devices/detectors.yaml +++ b/src/dodal/devices/i10/i10_config/devices/detectors.yaml @@ -1,5 +1,5 @@ - section: "Rasor Detectors" - func: rasor_det_scalers + device: rasor_det_scalers type: RasorScalerCard1 import_from: "dodal.devices.i10.rasor.rasor_scaler_cards" params: @@ -7,7 +7,7 @@ factory_args: { mock: true } - section: "Rasor Detectors" - func: rasor_sr570 + device: rasor_sr570 type: RasorSR570 import_from: "dodal.devices.i10.rasor.rasor_current_amp" params: @@ -15,7 +15,7 @@ factory_args: { mock: true } - section: "Rasor Detectors" - func: rasor_sr570_pa_scaler_det + device: rasor_sr570_pa_scaler_det type: CurrentAmpDet import_from: "dodal.devices.current_amplifiers" params: diff --git a/src/dodal/devices/i10/i10_config/devices/mirrors.yaml b/src/dodal/devices/i10/i10_config/devices/mirrors.yaml index 9f41a4fd95..b05209364e 100644 --- a/src/dodal/devices/i10/i10_config/devices/mirrors.yaml +++ b/src/dodal/devices/i10/i10_config/devices/mirrors.yaml @@ -1,12 +1,12 @@ - section: "Mirrors" - func: focusing_mirror + device: focusing_mirror type: PiezoMirror import_from: "dodal.devices.i10" params: prefix: 'f"{PREFIX.beamline_prefix}-OP-FOCS-01:"' - section: "Mirrors" - func: switching_mirror + device: switching_mirror type: PiezoMirror import_from: "dodal.devices.i10" params: From 3597cd2047cc09741d27030566fe6af7f9afc1d6 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Tue, 13 Jan 2026 15:05:49 +0000 Subject: [PATCH 08/16] running it off heap rather than writing file. --- src/dodal/beamlines/configs/i10_2/config.yaml | 1 - src/dodal/beamlines/i10_2.py | 14 ++--- .../common/beamlines/config_generator.py | 62 ++++++------------- 3 files changed, 22 insertions(+), 55 deletions(-) diff --git a/src/dodal/beamlines/configs/i10_2/config.yaml b/src/dodal/beamlines/configs/i10_2/config.yaml index 0e6ba5646d..c86f744ce5 100644 --- a/src/dodal/beamlines/configs/i10_2/config.yaml +++ b/src/dodal/beamlines/configs/i10_2/config.yaml @@ -1,5 +1,4 @@ beamline: "i10" -output_file: "/workspaces/dodal/src/dodal/beamlines/i10_2.py" base_imports: - "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline" diff --git a/src/dodal/beamlines/i10_2.py b/src/dodal/beamlines/i10_2.py index 920365628a..a6011dca25 100644 --- a/src/dodal/beamlines/i10_2.py +++ b/src/dodal/beamlines/i10_2.py @@ -1,17 +1,11 @@ import os -from dodal.common.beamlines.config_generator import generate_beamline +from dodal.common.beamlines.config_generator import get_beamline_code current_dir = os.path.dirname(__file__) beamline_name = os.path.splitext(os.path.basename(__file__))[0] CONFIG_DIR = os.path.join(current_dir, "configs", beamline_name) -try: - code = generate_beamline(CONFIG_DIR) - exec(code, globals()) - -except Exception as e: - import logging - - logging.getLogger("dodal").error(f"Failed to generate beamline from YAML: {e}") - raise +# Generate and inject +code = get_beamline_code(CONFIG_DIR) +exec(code, globals()) diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 3fae1a4efd..967dccbbd1 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -10,78 +10,52 @@ def format_value(v: Any) -> str: - """ - Handles literals, f-strings, and backtick f-strings. - """ if isinstance(v, str): - # Code pass-through (function calls, attribute access, standard f-strings) if "(" in v or ")." in v or v.startswith('f"') or v.startswith("f'"): return v return f"'{v}'" return repr(v) -def generate_beamline(config_dir: str) -> str: - # Generate beamline configuration code based on YAML files +def get_beamline_code(config_dir: str) -> str: config_file = os.path.join(config_dir, "config.yaml") with open(config_file) as f: master: dict = yaml.safe_load(f) beamline = master["beamline"] - output_file = master["output_file"] device_files = master.get("device_files", []) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") sections = defaultdict(list) needed_imports = defaultdict(set) seen_functions = set() - # Generate devices from YAML files for y_path in device_files: full_device_path = os.path.join(config_dir, y_path) if not os.path.exists(full_device_path): - LOGGER.warning(msg=f"Defined device file not found: {full_device_path}") + LOGGER.warning(f"Device file not found: {full_device_path}") continue with open(full_device_path) as f: - content: dict = yaml.safe_load(f) - device_list: list[dict] = ( - content if isinstance(content, list) else [content] - ) - print(content, device_list) + content = yaml.safe_load(f) + device_list = content if isinstance(content, list) else [content] for dev in device_list: fname = dev.get("device") - print(fname) if fname in seen_functions: - raise ValueError( - f"Duplicate function name detected: '{fname}' in {y_path}" - ) + raise ValueError(f"Duplicate function: {fname}") seen_functions.add(fname) - sections[dev.get("section", "Other")].append(dev) if dev.get("import_from") and dev.get("type"): needed_imports[dev["import_from"]].add(dev["type"]) - # Build Note and Import Block - code = ( - '"""\n' - f"Generated on: {timestamp}\n" - f"Beamline: {beamline}\n\n" - "note:\n" - f"{master.get('note', '')}\n" - '"""\n\n' - ) + code = f'"""\nGenerated on: {timestamp}\nBeamline: {beamline}\n\nnote:\n{master.get("note", "")}\n"""\n\n' - code_lines: list = master["base_imports"][:] + code_lines = master["base_imports"][:] for module, classes in sorted(needed_imports.items()): cls_str = ", ".join(sorted(classes)) code_lines.append(f"from {module} import {cls_str}") - code += "\n".join(code_lines) + "\n\n" - - # Build Setup Script code += master["setup_script"].format(beamline=beamline) + "\n" - # Build Devices for section_name in sorted(sections.keys()): code += f'\n\n""" {section_name} """\n' for dev in sections[section_name]: @@ -96,17 +70,17 @@ def generate_beamline(config_dir: str) -> str: else: body = f"{dev['type']}()" - code += ( - f"\n@devices.factory({f_str})" - f"\ndef {dev['device']}() -> {dev['type']}:" - f"\n return {body}\n" - ) + code += f"\n@devices.factory({f_str})\ndef {dev['device']}() -> {dev['type']}:\n return {body}\n" + try: - subprocess.run( - ["ruff", "check", "--select", "I", "--fix", output_file], check=True + fmt = subprocess.run( + ["ruff", "format", "-"], + input=code, + capture_output=True, + text=True, + check=True, ) - subprocess.run(["ruff", "format", output_file], check=True) - print(f"Generated {output_file} successfully.") + return fmt.stdout except Exception as e: - print(f"File saved to {output_file}, but Ruff failed: {e}") - return code + LOGGER.error(f"Ruff formatting failed, returning raw code. Error: {e}") + return code From 50f9a62beebfa433932402355238d51b6d01eb89 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Tue, 13 Jan 2026 15:08:56 +0000 Subject: [PATCH 09/16] remove config in devices --- src/dodal/devices/i10/i10_config/__init__.py | 0 src/dodal/devices/i10/i10_config/config.yaml | 20 ---------------- .../i10/i10_config/devices/detectors.yaml | 24 ------------------- .../i10/i10_config/devices/mirrors.yaml | 14 ----------- 4 files changed, 58 deletions(-) delete mode 100644 src/dodal/devices/i10/i10_config/__init__.py delete mode 100644 src/dodal/devices/i10/i10_config/config.yaml delete mode 100644 src/dodal/devices/i10/i10_config/devices/detectors.yaml delete mode 100644 src/dodal/devices/i10/i10_config/devices/mirrors.yaml diff --git a/src/dodal/devices/i10/i10_config/__init__.py b/src/dodal/devices/i10/i10_config/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/dodal/devices/i10/i10_config/config.yaml b/src/dodal/devices/i10/i10_config/config.yaml deleted file mode 100644 index 0e6ba5646d..0000000000 --- a/src/dodal/devices/i10/i10_config/config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -beamline: "i10" -output_file: "/workspaces/dodal/src/dodal/beamlines/i10_2.py" - -base_imports: - - "from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline" - - "from dodal.device_manager import DeviceManager" - - "from dodal.log import set_beamline as set_log_beamline" - - "from dodal.utils import BeamlinePrefix, get_beamline_name" - -setup_script: | - devices = DeviceManager() - BL = get_beamline_name('{beamline}') - set_log_beamline(BL) - set_utils_beamline(BL) - PREFIX = BeamlinePrefix(BL) - -# Only these files will be processed -device_files: - - "devices/mirrors.yaml" - - "devices/detectors.yaml" diff --git a/src/dodal/devices/i10/i10_config/devices/detectors.yaml b/src/dodal/devices/i10/i10_config/devices/detectors.yaml deleted file mode 100644 index 7cf3f4509a..0000000000 --- a/src/dodal/devices/i10/i10_config/devices/detectors.yaml +++ /dev/null @@ -1,24 +0,0 @@ -- section: "Rasor Detectors" - device: rasor_det_scalers - type: RasorScalerCard1 - import_from: "dodal.devices.i10.rasor.rasor_scaler_cards" - params: - prefix: "ME01D-EA-SCLR-01:SCALER1" - factory_args: { mock: true } - -- section: "Rasor Detectors" - device: rasor_sr570 - type: RasorSR570 - import_from: "dodal.devices.i10.rasor.rasor_current_amp" - params: - prefix: "ME01D-EA-IAMP" - factory_args: { mock: true } - -- section: "Rasor Detectors" - device: rasor_sr570_pa_scaler_det - type: CurrentAmpDet - import_from: "dodal.devices.current_amplifiers" - params: - current_amp: "rasor_sr570().ca1" - counter: "rasor_det_scalers().det" - factory_args: { mock: true, skip: true } diff --git a/src/dodal/devices/i10/i10_config/devices/mirrors.yaml b/src/dodal/devices/i10/i10_config/devices/mirrors.yaml deleted file mode 100644 index b05209364e..0000000000 --- a/src/dodal/devices/i10/i10_config/devices/mirrors.yaml +++ /dev/null @@ -1,14 +0,0 @@ -- section: "Mirrors" - device: focusing_mirror - type: PiezoMirror - import_from: "dodal.devices.i10" - params: - prefix: 'f"{PREFIX.beamline_prefix}-OP-FOCS-01:"' - -- section: "Mirrors" - device: switching_mirror - type: PiezoMirror - import_from: "dodal.devices.i10" - params: - prefix: "f'{PREFIX.beamline_prefix}-OP-SWTCH-01:'" - factory_args: {} From 12de314288aa0f032a8c54f45db9b4ce9e17393a Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Tue, 13 Jan 2026 15:48:19 +0000 Subject: [PATCH 10/16] added a function to generate the yaml from py config --- .../configs/i10_recovered/__init__.py | 0 .../configs/i10_recovered/config.yaml | 27 ++++++ .../configs/i10_recovered/devices.yaml | 86 +++++++++++++++++++ .../configs/i10_recovered/generate_i10.py | 6 ++ src/dodal/beamlines/i10.py | 43 +++++----- src/dodal/beamlines/i10_recovered.py | 11 +++ .../common/beamlines/config_generator.py | 70 +++++++++++++++ 7 files changed, 222 insertions(+), 21 deletions(-) create mode 100644 src/dodal/beamlines/configs/i10_recovered/__init__.py create mode 100644 src/dodal/beamlines/configs/i10_recovered/config.yaml create mode 100644 src/dodal/beamlines/configs/i10_recovered/devices.yaml create mode 100644 src/dodal/beamlines/configs/i10_recovered/generate_i10.py create mode 100644 src/dodal/beamlines/i10_recovered.py diff --git a/src/dodal/beamlines/configs/i10_recovered/__init__.py b/src/dodal/beamlines/configs/i10_recovered/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/dodal/beamlines/configs/i10_recovered/config.yaml b/src/dodal/beamlines/configs/i10_recovered/config.yaml new file mode 100644 index 0000000000..1813dd17c0 --- /dev/null +++ b/src/dodal/beamlines/configs/i10_recovered/config.yaml @@ -0,0 +1,27 @@ +beamline: i10 +output_file: i10.py +base_imports: +- from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline +- from dodal.device_manager import DeviceManager +- from dodal.devices.current_amplifiers import CurrentAmpDet +- from dodal.devices.i10 import I10Diagnostic, I10Diagnostic5ADet, I10Slits, I10SlitsDrainCurrent, + PiezoMirror +- from dodal.devices.i10.diagnostics import I10Diagnostic, I10Diagnostic5ADet +- from dodal.devices.i10.rasor.rasor_current_amp import RasorFemto, RasorSR570 +- from dodal.devices.i10.rasor.rasor_motors import DetSlits, Diffractometer, PaStage +- from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1 +- from dodal.devices.motors import XYStage, XYZStage +- from dodal.devices.temperture_controller import Lakeshore340 +- from dodal.log import set_beamline as set_log_beamline +- from dodal.utils import BeamlinePrefix, get_beamline_name +setup_script: 'devices = DeviceManager() + + BL = get_beamline_name(''{beamline}'') + + set_log_beamline(BL) + + set_utils_beamline(BL) + + PREFIX = BeamlinePrefix(BL)' +device_files: +- devices.yaml diff --git a/src/dodal/beamlines/configs/i10_recovered/devices.yaml b/src/dodal/beamlines/configs/i10_recovered/devices.yaml new file mode 100644 index 0000000000..52201646ed --- /dev/null +++ b/src/dodal/beamlines/configs/i10_recovered/devices.yaml @@ -0,0 +1,86 @@ +- device: focusing_mirror + type: PiezoMirror + params: + prefix: f'{PREFIX.beamline_prefix}-OP-FOCS-01:' +- device: slits + type: I10Slits + params: + prefix: f'{PREFIX.beamline_prefix}-AL-SLITS-' +- device: slits_current + type: I10SlitsDrainCurrent + params: + prefix: f'{PREFIX.beamline_prefix}-' +- device: diagnostics + type: I10Diagnostic + params: + prefix: f'{PREFIX.beamline_prefix}-DI-' +- device: d5a_det + type: I10Diagnostic5ADet + params: + prefix: f'{PREFIX.beamline_prefix}-DI-' +- device: pin_hole + type: XYStage + params: + prefix: 'ME01D-EA-PINH-01:' +- device: det_slits + type: DetSlits + params: + prefix: ME01D-MO-APTR-0 +- device: diffractometer + type: Diffractometer + params: + prefix: 'ME01D-MO-DIFF-01:' +- device: pa_stage + type: PaStage + params: + prefix: 'ME01D-MO-POLAN-01:' +- device: sample_stage + type: XYZStage + params: + prefix: 'ME01D-MO-CRYO-01:' +- device: rasor_temperature_controller + type: Lakeshore340 + params: + prefix: 'ME01D-EA-TCTRL-01:' +- device: rasor_femto + type: RasorFemto + params: + prefix: ME01D-EA-IAMP +- device: rasor_det_scalers + type: RasorScalerCard1 + params: + prefix: ME01D-EA-SCLR-01:SCALER1 +- device: rasor_sr570 + type: RasorSR570 + params: + prefix: ME01D-EA-IAMP +- device: rasor_sr570_pa_scaler_det + type: CurrentAmpDet + params: + current_amp: rasor_sr570().ca1 + counter: rasor_det_scalers().det +- device: rasor_femto_pa_scaler_det + type: CurrentAmpDet + params: + current_amp: rasor_femto().ca1 + counter: rasor_det_scalers().det +- device: rasor_sr570_fluo_scaler_det + type: CurrentAmpDet + params: + current_amp: rasor_sr570().ca2 + counter: rasor_det_scalers().fluo +- device: rasor_femto_fluo_scaler_det + type: CurrentAmpDet + params: + current_amp: rasor_femto().ca2 + counter: rasor_det_scalers().fluo +- device: rasor_sr570_drain_scaler_det + type: CurrentAmpDet + params: + current_amp: rasor_sr570().ca3 + counter: rasor_det_scalers().drain +- device: rasor_femto_drain_scaler_det + type: CurrentAmpDet + params: + current_amp: rasor_femto().ca3 + counter: rasor_det_scalers().drain diff --git a/src/dodal/beamlines/configs/i10_recovered/generate_i10.py b/src/dodal/beamlines/configs/i10_recovered/generate_i10.py new file mode 100644 index 0000000000..c6221bb0b9 --- /dev/null +++ b/src/dodal/beamlines/configs/i10_recovered/generate_i10.py @@ -0,0 +1,6 @@ +from dodal.common.beamlines.config_generator import translate_beamline_py_config_to_yaml + +translate_beamline_py_config_to_yaml( + py_file_path="/workspaces/dodal/src/dodal/beamlines/i10.py", + output_dir="/workspaces/dodal/src/dodal/beamlines/configs/i10_recovered", +) diff --git a/src/dodal/beamlines/i10.py b/src/dodal/beamlines/i10.py index 96ff4c2589..df3d58229e 100644 --- a/src/dodal/beamlines/i10.py +++ b/src/dodal/beamlines/i10.py @@ -6,8 +6,8 @@ idd == id1, idu == id2. """ -from dodal.common.beamlines.beamline_utils import device_factory from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline +from dodal.device_manager import DeviceManager from dodal.devices.current_amplifiers import CurrentAmpDet from dodal.devices.i10 import ( I10Diagnostic, @@ -31,6 +31,7 @@ from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name +devices = DeviceManager() BL = get_beamline_name("i10") set_log_beamline(BL) set_utils_beamline(BL) @@ -39,7 +40,7 @@ """Mirrors""" -@device_factory() +@devices.factory() def focusing_mirror() -> PiezoMirror: return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-FOCS-01:") @@ -47,12 +48,12 @@ def focusing_mirror() -> PiezoMirror: """Optic slits""" -@device_factory() +@devices.factory() def slits() -> I10Slits: return I10Slits(prefix=f"{PREFIX.beamline_prefix}-AL-SLITS-") -@device_factory() +@devices.factory() def slits_current() -> I10SlitsDrainCurrent: return I10SlitsDrainCurrent(prefix=f"{PREFIX.beamline_prefix}-") @@ -60,14 +61,14 @@ def slits_current() -> I10SlitsDrainCurrent: """Diagnostics""" -@device_factory() +@devices.factory() def diagnostics() -> I10Diagnostic: return I10Diagnostic( prefix=f"{PREFIX.beamline_prefix}-DI-", ) -@device_factory() +@devices.factory() def d5a_det() -> I10Diagnostic5ADet: return I10Diagnostic5ADet(prefix=f"{PREFIX.beamline_prefix}-DI-") @@ -75,58 +76,58 @@ def d5a_det() -> I10Diagnostic5ADet: """Rasor devices""" -@device_factory() +@devices.factory() def pin_hole() -> XYStage: return XYStage(prefix="ME01D-EA-PINH-01:") -@device_factory() +@devices.factory() def det_slits() -> DetSlits: return DetSlits(prefix="ME01D-MO-APTR-0") -@device_factory() +@devices.factory() def diffractometer() -> Diffractometer: return Diffractometer(prefix="ME01D-MO-DIFF-01:") -@device_factory() +@devices.factory() def pa_stage() -> PaStage: return PaStage(prefix="ME01D-MO-POLAN-01:") -@device_factory() +@devices.factory() def sample_stage() -> XYZStage: return XYZStage(prefix="ME01D-MO-CRYO-01:") -@device_factory() +@devices.factory() def rasor_temperature_controller() -> Lakeshore340: return Lakeshore340( prefix="ME01D-EA-TCTRL-01:", ) -@device_factory() +@devices.factory() def rasor_femto() -> RasorFemto: return RasorFemto( prefix="ME01D-EA-IAMP", ) -@device_factory() +@devices.factory() def rasor_det_scalers() -> RasorScalerCard1: return RasorScalerCard1(prefix="ME01D-EA-SCLR-01:SCALER1") -@device_factory() +@devices.factory() def rasor_sr570() -> RasorSR570: return RasorSR570( prefix="ME01D-EA-IAMP", ) -@device_factory() +@devices.factory() def rasor_sr570_pa_scaler_det() -> CurrentAmpDet: return CurrentAmpDet( current_amp=rasor_sr570().ca1, @@ -134,7 +135,7 @@ def rasor_sr570_pa_scaler_det() -> CurrentAmpDet: ) -@device_factory() +@devices.factory() def rasor_femto_pa_scaler_det() -> CurrentAmpDet: return CurrentAmpDet( current_amp=rasor_femto().ca1, @@ -142,7 +143,7 @@ def rasor_femto_pa_scaler_det() -> CurrentAmpDet: ) -@device_factory() +@devices.factory() def rasor_sr570_fluo_scaler_det() -> CurrentAmpDet: return CurrentAmpDet( current_amp=rasor_sr570().ca2, @@ -150,7 +151,7 @@ def rasor_sr570_fluo_scaler_det() -> CurrentAmpDet: ) -@device_factory() +@devices.factory() def rasor_femto_fluo_scaler_det() -> CurrentAmpDet: return CurrentAmpDet( current_amp=rasor_femto().ca2, @@ -158,7 +159,7 @@ def rasor_femto_fluo_scaler_det() -> CurrentAmpDet: ) -@device_factory() +@devices.factory() def rasor_sr570_drain_scaler_det() -> CurrentAmpDet: return CurrentAmpDet( current_amp=rasor_sr570().ca3, @@ -166,7 +167,7 @@ def rasor_sr570_drain_scaler_det() -> CurrentAmpDet: ) -@device_factory() +@devices.factory() def rasor_femto_drain_scaler_det() -> CurrentAmpDet: return CurrentAmpDet( current_amp=rasor_femto().ca3, diff --git a/src/dodal/beamlines/i10_recovered.py b/src/dodal/beamlines/i10_recovered.py new file mode 100644 index 0000000000..a6011dca25 --- /dev/null +++ b/src/dodal/beamlines/i10_recovered.py @@ -0,0 +1,11 @@ +import os + +from dodal.common.beamlines.config_generator import get_beamline_code + +current_dir = os.path.dirname(__file__) +beamline_name = os.path.splitext(os.path.basename(__file__))[0] +CONFIG_DIR = os.path.join(current_dir, "configs", beamline_name) + +# Generate and inject +code = get_beamline_code(CONFIG_DIR) +exec(code, globals()) diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 967dccbbd1..f239763c7e 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -1,3 +1,4 @@ +import ast import os import subprocess from collections import defaultdict @@ -84,3 +85,72 @@ def get_beamline_code(config_dir: str) -> str: except Exception as e: LOGGER.error(f"Ruff formatting failed, returning raw code. Error: {e}") return code + + +def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): + with open(py_file_path) as f: + source = f.read() + tree = ast.parse(source) + + devices = [] + base_imports = [] + setup_nodes = [] + + for node in tree.body: + if isinstance(node, (ast.Import, ast.ImportFrom)): + base_imports.append(ast.unparse(node)) + + elif isinstance(node, ast.FunctionDef): + device_meta = { + "device": node.name, + "type": node.returns.id if isinstance(node.returns, ast.Name) else None, + } + + for decorator in node.decorator_list: + if isinstance(decorator, ast.Call) and "factory" in ast.unparse( + decorator + ): + f_args = { + kw.arg: ast.literal_eval(kw.value) for kw in decorator.keywords + } + if f_args: + device_meta["factory_args"] = f_args + + ret_stmt = node.body[0] + if isinstance(ret_stmt, ast.Return) and isinstance( + ret_stmt.value, ast.Call + ): + params = {} + for kw in ret_stmt.value.keywords: + try: + params[kw.arg] = ast.literal_eval(kw.value) + except ValueError: + params[kw.arg] = ast.unparse(kw.value) + if params: + device_meta["params"] = params + + devices.append(device_meta) + + elif not isinstance(node, ast.Expr) or not isinstance(node.value, ast.Constant): + setup_nodes.append(ast.unparse(node)) + + beamline_name = os.path.splitext(os.path.basename(py_file_path))[0] + setup_script = "\n".join(setup_nodes).replace(f"'{beamline_name}'", "'{beamline}'") + + os.makedirs(output_dir, exist_ok=True) + + master_config = { + "beamline": beamline_name, + "output_file": os.path.basename(py_file_path), + "base_imports": base_imports, + "setup_script": setup_script, + "device_files": ["devices.yaml"], + } + + with open(os.path.join(output_dir, "config.yaml"), "w") as f: + yaml.dump(master_config, f, sort_keys=False, default_flow_style=False) + + with open(os.path.join(output_dir, "devices.yaml"), "w") as f: + yaml.dump(devices, f, sort_keys=False, default_flow_style=False) + + print(f"Successfully back-translated {py_file_path} to {output_dir}") From 591330fd0cb4792e2fc7b00683150b1918ed53a4 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Tue, 13 Jan 2026 16:28:41 +0000 Subject: [PATCH 11/16] add pydantic --- .../configs/i10_recovered/config.yaml | 9 -- .../configs/i10_recovered/devices.yaml | 40 ++++++ .../common/beamlines/config_generator.py | 126 +++++++++++------- 3 files changed, 118 insertions(+), 57 deletions(-) diff --git a/src/dodal/beamlines/configs/i10_recovered/config.yaml b/src/dodal/beamlines/configs/i10_recovered/config.yaml index 1813dd17c0..b8620fd507 100644 --- a/src/dodal/beamlines/configs/i10_recovered/config.yaml +++ b/src/dodal/beamlines/configs/i10_recovered/config.yaml @@ -3,15 +3,6 @@ output_file: i10.py base_imports: - from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline - from dodal.device_manager import DeviceManager -- from dodal.devices.current_amplifiers import CurrentAmpDet -- from dodal.devices.i10 import I10Diagnostic, I10Diagnostic5ADet, I10Slits, I10SlitsDrainCurrent, - PiezoMirror -- from dodal.devices.i10.diagnostics import I10Diagnostic, I10Diagnostic5ADet -- from dodal.devices.i10.rasor.rasor_current_amp import RasorFemto, RasorSR570 -- from dodal.devices.i10.rasor.rasor_motors import DetSlits, Diffractometer, PaStage -- from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1 -- from dodal.devices.motors import XYStage, XYZStage -- from dodal.devices.temperture_controller import Lakeshore340 - from dodal.log import set_beamline as set_log_beamline - from dodal.utils import BeamlinePrefix, get_beamline_name setup_script: 'devices = DeviceManager() diff --git a/src/dodal/beamlines/configs/i10_recovered/devices.yaml b/src/dodal/beamlines/configs/i10_recovered/devices.yaml index 52201646ed..173e95f63a 100644 --- a/src/dodal/beamlines/configs/i10_recovered/devices.yaml +++ b/src/dodal/beamlines/configs/i10_recovered/devices.yaml @@ -1,86 +1,126 @@ - device: focusing_mirror type: PiezoMirror + import_from: dodal.devices.i10 + factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-OP-FOCS-01:' - device: slits type: I10Slits + import_from: dodal.devices.i10 + factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-AL-SLITS-' - device: slits_current type: I10SlitsDrainCurrent + import_from: dodal.devices.i10 + factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-' - device: diagnostics type: I10Diagnostic + import_from: dodal.devices.i10.diagnostics + factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-DI-' - device: d5a_det type: I10Diagnostic5ADet + import_from: dodal.devices.i10.diagnostics + factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-DI-' - device: pin_hole type: XYStage + import_from: dodal.devices.motors + factory_args: {} params: prefix: 'ME01D-EA-PINH-01:' - device: det_slits type: DetSlits + import_from: dodal.devices.i10.rasor.rasor_motors + factory_args: {} params: prefix: ME01D-MO-APTR-0 - device: diffractometer type: Diffractometer + import_from: dodal.devices.i10.rasor.rasor_motors + factory_args: {} params: prefix: 'ME01D-MO-DIFF-01:' - device: pa_stage type: PaStage + import_from: dodal.devices.i10.rasor.rasor_motors + factory_args: {} params: prefix: 'ME01D-MO-POLAN-01:' - device: sample_stage type: XYZStage + import_from: dodal.devices.motors + factory_args: {} params: prefix: 'ME01D-MO-CRYO-01:' - device: rasor_temperature_controller type: Lakeshore340 + import_from: dodal.devices.temperture_controller + factory_args: {} params: prefix: 'ME01D-EA-TCTRL-01:' - device: rasor_femto type: RasorFemto + import_from: dodal.devices.i10.rasor.rasor_current_amp + factory_args: {} params: prefix: ME01D-EA-IAMP - device: rasor_det_scalers type: RasorScalerCard1 + import_from: dodal.devices.i10.rasor.rasor_scaler_cards + factory_args: {} params: prefix: ME01D-EA-SCLR-01:SCALER1 - device: rasor_sr570 type: RasorSR570 + import_from: dodal.devices.i10.rasor.rasor_current_amp + factory_args: {} params: prefix: ME01D-EA-IAMP - device: rasor_sr570_pa_scaler_det type: CurrentAmpDet + import_from: dodal.devices.current_amplifiers + factory_args: {} params: current_amp: rasor_sr570().ca1 counter: rasor_det_scalers().det - device: rasor_femto_pa_scaler_det type: CurrentAmpDet + import_from: dodal.devices.current_amplifiers + factory_args: {} params: current_amp: rasor_femto().ca1 counter: rasor_det_scalers().det - device: rasor_sr570_fluo_scaler_det type: CurrentAmpDet + import_from: dodal.devices.current_amplifiers + factory_args: {} params: current_amp: rasor_sr570().ca2 counter: rasor_det_scalers().fluo - device: rasor_femto_fluo_scaler_det type: CurrentAmpDet + import_from: dodal.devices.current_amplifiers + factory_args: {} params: current_amp: rasor_femto().ca2 counter: rasor_det_scalers().fluo - device: rasor_sr570_drain_scaler_det type: CurrentAmpDet + import_from: dodal.devices.current_amplifiers + factory_args: {} params: current_amp: rasor_sr570().ca3 counter: rasor_det_scalers().drain - device: rasor_femto_drain_scaler_det type: CurrentAmpDet + import_from: dodal.devices.current_amplifiers + factory_args: {} params: current_amp: rasor_femto().ca3 counter: rasor_det_scalers().drain diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index f239763c7e..69ee307c94 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -6,10 +6,30 @@ from typing import Any import yaml +from pydantic import BaseModel, Field from dodal.log import LOGGER +class DeviceModel(BaseModel): + device: str = Field(..., description="The name of the function to be generated") + type: str = Field(..., description="The Python class type of the device") + import_from: str | None = Field( + "", description="The module to import the type from" + ) + section: str = "Other" + params: dict[str, Any] = Field(default_factory=dict) + factory_args: dict[str, Any] = Field(default_factory=dict) + + +class MasterConfigModel(BaseModel): + beamline: str + base_imports: list[str] + setup_script: str + device_files: list[str] + note: str | None = "" + + def format_value(v: Any) -> str: if isinstance(v, str): if "(" in v or ")." in v or v.startswith('f"') or v.startswith("f'"): @@ -21,16 +41,16 @@ def format_value(v: Any) -> str: def get_beamline_code(config_dir: str) -> str: config_file = os.path.join(config_dir, "config.yaml") with open(config_file) as f: - master: dict = yaml.safe_load(f) + master_raw: dict = yaml.safe_load(f) + master = MasterConfigModel(**master_raw) - beamline = master["beamline"] - device_files = master.get("device_files", []) + beamline = master.beamline timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") sections = defaultdict(list) needed_imports = defaultdict(set) seen_functions = set() - for y_path in device_files: + for y_path in master.device_files: full_device_path = os.path.join(config_dir, y_path) if not os.path.exists(full_device_path): LOGGER.warning(f"Device file not found: {full_device_path}") @@ -38,41 +58,39 @@ def get_beamline_code(config_dir: str) -> str: with open(full_device_path) as f: content = yaml.safe_load(f) - device_list = content if isinstance(content, list) else [content] - for dev in device_list: - fname = dev.get("device") - if fname in seen_functions: - raise ValueError(f"Duplicate function: {fname}") - seen_functions.add(fname) - sections[dev.get("section", "Other")].append(dev) - if dev.get("import_from") and dev.get("type"): - needed_imports[dev["import_from"]].add(dev["type"]) - - code = f'"""\nGenerated on: {timestamp}\nBeamline: {beamline}\n\nnote:\n{master.get("note", "")}\n"""\n\n' - - code_lines = master["base_imports"][:] + device_list_raw = content if isinstance(content, list) else [content] + for dev_dict in device_list_raw: + dev = DeviceModel(**dev_dict) + if dev.device in seen_functions: + raise ValueError(f"Duplicate function: {dev.device}") + seen_functions.add(dev.device) + sections[dev.section].append(dev) + needed_imports[dev.import_from].add(dev.type) + + code = f'"""\nGenerated on: {timestamp}\nBeamline: {beamline}\n\nnote:\n{master.note}\n"""\n\n' + code_lines = master.base_imports[:] for module, classes in sorted(needed_imports.items()): cls_str = ", ".join(sorted(classes)) code_lines.append(f"from {module} import {cls_str}") code += "\n".join(code_lines) + "\n\n" - code += master["setup_script"].format(beamline=beamline) + "\n" + code += master.setup_script.format(beamline=beamline) + "\n" for section_name in sorted(sections.keys()): code += f'\n\n""" {section_name} """\n' for dev in sections[section_name]: - f_args = dev.get("factory_args", {}) + f_args = dev.factory_args f_str = ", ".join([f"{k}={repr(v)}" for k, v in f_args.items()]) - if "params" in dev: + if dev.params: args = ",\n ".join( - [f"{k}={format_value(v)}" for k, v in dev["params"].items()] + [f"{k}={format_value(v)}" for k, v in dev.params.items()] ) - body = f"{dev['type']}(\n {args}\n )" + body = f"{dev.type}(\n {args}\n )" else: - body = f"{dev['type']}()" - - code += f"\n@devices.factory({f_str})\ndef {dev['device']}() -> {dev['type']}:\n return {body}\n" + body = f"{dev.type}()" + code += f"\n@devices.factory({f_str})\ndef {dev.device}() -> {dev.type}:\n return {body}\n" + print(code) try: fmt = subprocess.run( ["ruff", "format", "-"], @@ -95,54 +113,68 @@ def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): devices = [] base_imports = [] setup_nodes = [] + import_map = {} + + for node in tree.body: + if isinstance(node, ast.ImportFrom): + module = node.module + for alias in node.names: + import_map[alias.name] = module for node in tree.body: if isinstance(node, (ast.Import, ast.ImportFrom)): base_imports.append(ast.unparse(node)) elif isinstance(node, ast.FunctionDef): + ret_type = node.returns.id if isinstance(node.returns, ast.Name) else None + device_meta = { "device": node.name, - "type": node.returns.id if isinstance(node.returns, ast.Name) else None, + "type": ret_type, + "import_from": import_map.get(ret_type, "unknown.module"), } for decorator in node.decorator_list: if isinstance(decorator, ast.Call) and "factory" in ast.unparse( decorator ): - f_args = { + device_meta["factory_args"] = { kw.arg: ast.literal_eval(kw.value) for kw in decorator.keywords } - if f_args: - device_meta["factory_args"] = f_args - - ret_stmt = node.body[0] - if isinstance(ret_stmt, ast.Return) and isinstance( - ret_stmt.value, ast.Call - ): - params = {} - for kw in ret_stmt.value.keywords: - try: - params[kw.arg] = ast.literal_eval(kw.value) - except ValueError: - params[kw.arg] = ast.unparse(kw.value) - if params: - device_meta["params"] = params + + if node.body and isinstance(node.body[0], ast.Return): + ret_val = node.body[0].value + if isinstance(ret_val, ast.Call): + params = {} + for kw in ret_val.keywords: + try: + params[kw.arg] = ast.literal_eval(kw.value) + except (ValueError, TypeError): + params[kw.arg] = ast.unparse(kw.value) + if params: + device_meta["params"] = params devices.append(device_meta) - elif not isinstance(node, ast.Expr) or not isinstance(node.value, ast.Constant): + elif not (isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant)): setup_nodes.append(ast.unparse(node)) beamline_name = os.path.splitext(os.path.basename(py_file_path))[0] - setup_script = "\n".join(setup_nodes).replace(f"'{beamline_name}'", "'{beamline}'") - + setup_raw = "\n".join(setup_nodes) + setup_script = setup_raw.replace(f"'{beamline_name}'", "'{beamline}'") + device_types = {d["type"] for d in devices} + filtered_base_imports = [] + for imp in base_imports: + if not any(f"import {t}" in imp or f", {t}" in imp for t in device_types): + filtered_base_imports.append(imp) + + # Write Output os.makedirs(output_dir, exist_ok=True) master_config = { "beamline": beamline_name, "output_file": os.path.basename(py_file_path), - "base_imports": base_imports, + "base_imports": filtered_base_imports, "setup_script": setup_script, "device_files": ["devices.yaml"], } @@ -152,5 +184,3 @@ def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): with open(os.path.join(output_dir, "devices.yaml"), "w") as f: yaml.dump(devices, f, sort_keys=False, default_flow_style=False) - - print(f"Successfully back-translated {py_file_path} to {output_dir}") From 1b37673993dd57bea1535cef93722f3eb8df95d6 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 16 Jan 2026 10:38:14 +0000 Subject: [PATCH 12/16] name change --- src/dodal/beamlines/configs/i10_recovered/config.yaml | 1 - src/dodal/beamlines/i10_2.py | 4 ++-- src/dodal/beamlines/i10_recovered.py | 4 ++-- src/dodal/common/beamlines/config_generator.py | 4 +--- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/dodal/beamlines/configs/i10_recovered/config.yaml b/src/dodal/beamlines/configs/i10_recovered/config.yaml index b8620fd507..7219685b26 100644 --- a/src/dodal/beamlines/configs/i10_recovered/config.yaml +++ b/src/dodal/beamlines/configs/i10_recovered/config.yaml @@ -1,5 +1,4 @@ beamline: i10 -output_file: i10.py base_imports: - from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline - from dodal.device_manager import DeviceManager diff --git a/src/dodal/beamlines/i10_2.py b/src/dodal/beamlines/i10_2.py index a6011dca25..aa599588f9 100644 --- a/src/dodal/beamlines/i10_2.py +++ b/src/dodal/beamlines/i10_2.py @@ -1,11 +1,11 @@ import os -from dodal.common.beamlines.config_generator import get_beamline_code +from dodal.common.beamlines.config_generator import beamline_config_generator current_dir = os.path.dirname(__file__) beamline_name = os.path.splitext(os.path.basename(__file__))[0] CONFIG_DIR = os.path.join(current_dir, "configs", beamline_name) # Generate and inject -code = get_beamline_code(CONFIG_DIR) +code = beamline_config_generator(CONFIG_DIR) exec(code, globals()) diff --git a/src/dodal/beamlines/i10_recovered.py b/src/dodal/beamlines/i10_recovered.py index a6011dca25..aa599588f9 100644 --- a/src/dodal/beamlines/i10_recovered.py +++ b/src/dodal/beamlines/i10_recovered.py @@ -1,11 +1,11 @@ import os -from dodal.common.beamlines.config_generator import get_beamline_code +from dodal.common.beamlines.config_generator import beamline_config_generator current_dir = os.path.dirname(__file__) beamline_name = os.path.splitext(os.path.basename(__file__))[0] CONFIG_DIR = os.path.join(current_dir, "configs", beamline_name) # Generate and inject -code = get_beamline_code(CONFIG_DIR) +code = beamline_config_generator(CONFIG_DIR) exec(code, globals()) diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 69ee307c94..6079894a0e 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -38,7 +38,7 @@ def format_value(v: Any) -> str: return repr(v) -def get_beamline_code(config_dir: str) -> str: +def beamline_config_generator(config_dir: str) -> str: config_file = os.path.join(config_dir, "config.yaml") with open(config_file) as f: master_raw: dict = yaml.safe_load(f) @@ -168,12 +168,10 @@ def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): if not any(f"import {t}" in imp or f", {t}" in imp for t in device_types): filtered_base_imports.append(imp) - # Write Output os.makedirs(output_dir, exist_ok=True) master_config = { "beamline": beamline_name, - "output_file": os.path.basename(py_file_path), "base_imports": filtered_base_imports, "setup_script": setup_script, "device_files": ["devices.yaml"], From acf00e5c4a95905c3bf95b6eef2e1f9947784571 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 16 Jan 2026 10:45:23 +0000 Subject: [PATCH 13/16] base line test --- .../common/beamlines/test_config_generator.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/common/beamlines/test_config_generator.py diff --git a/tests/common/beamlines/test_config_generator.py b/tests/common/beamlines/test_config_generator.py new file mode 100644 index 0000000000..213a2c89c6 --- /dev/null +++ b/tests/common/beamlines/test_config_generator.py @@ -0,0 +1,56 @@ +from dodal.common.beamlines.config_generator import ( + beamline_config_generator, + translate_beamline_py_config_to_yaml, +) + + +def test_generator_output_contains_expected_code(tmp_path): + # Setup temporary YAML files + conf_dir = tmp_path / "i10" + conf_dir.mkdir() + + config_yaml = conf_dir / "config.yaml" + config_yaml.write_text(""" +beamline: i10 +output_file: i10.py +base_imports: ["import os"] +setup_script: "PREFIX = '{beamline}'" +device_files: ["devices.yaml"] +""") + + devices_yaml = conf_dir / "devices.yaml" + devices_yaml.write_text(""" +- device: my_motor + type: MotorClass + import_from: dodal.devices + params: + prefix: 'f"{PREFIX}-MOT-01"' +""") + + code = beamline_config_generator(str(conf_dir)) + + assert "def my_motor() -> MotorClass:" in code + assert "from dodal.devices import MotorClass" in code + assert 'prefix=f"{PREFIX}-MOT-01"' in code + + +def test_reverse_translate_beamline_py_config_to_yaml(tmp_path): + py_file = tmp_path / "i10.py" + py_file.write_text(""" +from dodal.devices.custom import CustomDevice + +@devices.factory() +def my_device() -> CustomDevice: + return CustomDevice() +""") + + output_dir = tmp_path / "recovered" + translate_beamline_py_config_to_yaml(str(py_file), str(output_dir)) + + import yaml + + with open(output_dir / "devices.yaml") as f: + recovered = yaml.safe_load(f) + + assert recovered[0]["device"] == "my_device" + assert recovered[0]["import_from"] == "dodal.devices.custom" From 5f0e1b3122a14400a180bd7888dca5d26647de96 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 16 Jan 2026 12:47:40 +0000 Subject: [PATCH 14/16] remove print --- src/dodal/common/beamlines/config_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 6079894a0e..57c7b247e0 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -90,7 +90,6 @@ def beamline_config_generator(config_dir: str) -> str: body = f"{dev.type}()" code += f"\n@devices.factory({f_str})\ndef {dev.device}() -> {dev.type}:\n return {body}\n" - print(code) try: fmt = subprocess.run( ["ruff", "format", "-"], From 9153f3971c4b2d193873f40199d408aeeb947ced Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 16 Jan 2026 14:55:07 +0000 Subject: [PATCH 15/16] change so that device(function) can have any or no decorators --- .../configs/i10_2/devices/detectors.yaml | 7 ++-- .../configs/i10_2/devices/mirrors.yaml | 13 ++++++- .../configs/i10_recovered/devices.yaml | 20 ---------- .../common/beamlines/config_generator.py | 37 ++++++++++--------- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml b/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml index 7cf3f4509a..96c72adb97 100644 --- a/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml +++ b/src/dodal/beamlines/configs/i10_2/devices/detectors.yaml @@ -4,7 +4,6 @@ import_from: "dodal.devices.i10.rasor.rasor_scaler_cards" params: prefix: "ME01D-EA-SCLR-01:SCALER1" - factory_args: { mock: true } - section: "Rasor Detectors" device: rasor_sr570 @@ -12,7 +11,6 @@ import_from: "dodal.devices.i10.rasor.rasor_current_amp" params: prefix: "ME01D-EA-IAMP" - factory_args: { mock: true } - section: "Rasor Detectors" device: rasor_sr570_pa_scaler_det @@ -21,4 +19,7 @@ params: current_amp: "rasor_sr570().ca1" counter: "rasor_det_scalers().det" - factory_args: { mock: true, skip: true } + decorators: + - name: "devices.factory" + args: + mock: true diff --git a/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml b/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml index b05209364e..3dcc478cbb 100644 --- a/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml +++ b/src/dodal/beamlines/configs/i10_2/devices/mirrors.yaml @@ -11,4 +11,15 @@ import_from: "dodal.devices.i10" params: prefix: "f'{PREFIX.beamline_prefix}-OP-SWTCH-01:'" - factory_args: {} + decorators: + - name: "devices.factory" + args: + skip: false + +- section: "Mirrors" + device: switching_mirror_backup + type: PiezoMirror + import_from: "dodal.devices.i10" + params: + prefix: "tbsjkfbnskfbnsk" + decorators: [] #no decorators diff --git a/src/dodal/beamlines/configs/i10_recovered/devices.yaml b/src/dodal/beamlines/configs/i10_recovered/devices.yaml index 173e95f63a..d3c0919058 100644 --- a/src/dodal/beamlines/configs/i10_recovered/devices.yaml +++ b/src/dodal/beamlines/configs/i10_recovered/devices.yaml @@ -1,126 +1,106 @@ - device: focusing_mirror type: PiezoMirror import_from: dodal.devices.i10 - factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-OP-FOCS-01:' - device: slits type: I10Slits import_from: dodal.devices.i10 - factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-AL-SLITS-' - device: slits_current type: I10SlitsDrainCurrent import_from: dodal.devices.i10 - factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-' - device: diagnostics type: I10Diagnostic import_from: dodal.devices.i10.diagnostics - factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-DI-' - device: d5a_det type: I10Diagnostic5ADet import_from: dodal.devices.i10.diagnostics - factory_args: {} params: prefix: f'{PREFIX.beamline_prefix}-DI-' - device: pin_hole type: XYStage import_from: dodal.devices.motors - factory_args: {} params: prefix: 'ME01D-EA-PINH-01:' - device: det_slits type: DetSlits import_from: dodal.devices.i10.rasor.rasor_motors - factory_args: {} params: prefix: ME01D-MO-APTR-0 - device: diffractometer type: Diffractometer import_from: dodal.devices.i10.rasor.rasor_motors - factory_args: {} params: prefix: 'ME01D-MO-DIFF-01:' - device: pa_stage type: PaStage import_from: dodal.devices.i10.rasor.rasor_motors - factory_args: {} params: prefix: 'ME01D-MO-POLAN-01:' - device: sample_stage type: XYZStage import_from: dodal.devices.motors - factory_args: {} params: prefix: 'ME01D-MO-CRYO-01:' - device: rasor_temperature_controller type: Lakeshore340 import_from: dodal.devices.temperture_controller - factory_args: {} params: prefix: 'ME01D-EA-TCTRL-01:' - device: rasor_femto type: RasorFemto import_from: dodal.devices.i10.rasor.rasor_current_amp - factory_args: {} params: prefix: ME01D-EA-IAMP - device: rasor_det_scalers type: RasorScalerCard1 import_from: dodal.devices.i10.rasor.rasor_scaler_cards - factory_args: {} params: prefix: ME01D-EA-SCLR-01:SCALER1 - device: rasor_sr570 type: RasorSR570 import_from: dodal.devices.i10.rasor.rasor_current_amp - factory_args: {} params: prefix: ME01D-EA-IAMP - device: rasor_sr570_pa_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers - factory_args: {} params: current_amp: rasor_sr570().ca1 counter: rasor_det_scalers().det - device: rasor_femto_pa_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers - factory_args: {} params: current_amp: rasor_femto().ca1 counter: rasor_det_scalers().det - device: rasor_sr570_fluo_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers - factory_args: {} params: current_amp: rasor_sr570().ca2 counter: rasor_det_scalers().fluo - device: rasor_femto_fluo_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers - factory_args: {} params: current_amp: rasor_femto().ca2 counter: rasor_det_scalers().fluo - device: rasor_sr570_drain_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers - factory_args: {} params: current_amp: rasor_sr570().ca3 counter: rasor_det_scalers().drain - device: rasor_femto_drain_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers - factory_args: {} params: current_amp: rasor_femto().ca3 counter: rasor_det_scalers().drain diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 57c7b247e0..40129afa77 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -11,15 +11,23 @@ from dodal.log import LOGGER -class DeviceModel(BaseModel): - device: str = Field(..., description="The name of the function to be generated") - type: str = Field(..., description="The Python class type of the device") - import_from: str | None = Field( - "", description="The module to import the type from" +class DecoratorModel(BaseModel): + name: str = Field(..., description="The decorator name, e.g., 'devices.factory'") + args: dict[str, Any] = Field( + default_factory=dict, description="Keyword arguments for the decorator" ) + + +class DeviceModel(BaseModel): + device: str + type: str + import_from: str | None = "" section: str = "Other" params: dict[str, Any] = Field(default_factory=dict) - factory_args: dict[str, Any] = Field(default_factory=dict) + # Default to devices.factory() if none provided + decorators: list[DecoratorModel] = Field( + default_factory=lambda: [DecoratorModel(name="devices.factory")] + ) class MasterConfigModel(BaseModel): @@ -77,9 +85,11 @@ def beamline_config_generator(config_dir: str) -> str: for section_name in sorted(sections.keys()): code += f'\n\n""" {section_name} """\n' + for dev in sections[section_name]: - f_args = dev.factory_args - f_str = ", ".join([f"{k}={repr(v)}" for k, v in f_args.items()]) + for dec in dev.decorators: + arg_str = ", ".join([f"{k}={repr(v)}" for k, v in dec.args.items()]) + code += f"\n@{dec.name}({arg_str})" if dev.params: args = ",\n ".join( @@ -89,7 +99,7 @@ def beamline_config_generator(config_dir: str) -> str: else: body = f"{dev.type}()" - code += f"\n@devices.factory({f_str})\ndef {dev.device}() -> {dev.type}:\n return {body}\n" + code += f"\ndef {dev.device}() -> {dev.type}:\n return {body}\n" try: fmt = subprocess.run( ["ruff", "format", "-"], @@ -132,15 +142,6 @@ def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): "type": ret_type, "import_from": import_map.get(ret_type, "unknown.module"), } - - for decorator in node.decorator_list: - if isinstance(decorator, ast.Call) and "factory" in ast.unparse( - decorator - ): - device_meta["factory_args"] = { - kw.arg: ast.literal_eval(kw.value) for kw in decorator.keywords - } - if node.body and isinstance(node.body[0], ast.Return): ret_val = node.body[0].value if isinstance(ret_val, ast.Call): From ae5ed488abba9a66c8b4daa58a38e71a6d8a4949 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Mon, 19 Jan 2026 12:09:57 +0000 Subject: [PATCH 16/16] remove ruff as it is not needed --- .../configs/i10_recovered/devices.yaml | 60 +++++++++++++++++ .../common/beamlines/config_generator.py | 28 ++++---- .../common/beamlines/test_config_generator.py | 65 +++++++++++++++---- 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/src/dodal/beamlines/configs/i10_recovered/devices.yaml b/src/dodal/beamlines/configs/i10_recovered/devices.yaml index d3c0919058..005b390bff 100644 --- a/src/dodal/beamlines/configs/i10_recovered/devices.yaml +++ b/src/dodal/beamlines/configs/i10_recovered/devices.yaml @@ -1,106 +1,166 @@ - device: focusing_mirror type: PiezoMirror import_from: dodal.devices.i10 + decorators: + - name: devices.factory + args: {} params: prefix: f'{PREFIX.beamline_prefix}-OP-FOCS-01:' - device: slits type: I10Slits import_from: dodal.devices.i10 + decorators: + - name: devices.factory + args: {} params: prefix: f'{PREFIX.beamline_prefix}-AL-SLITS-' - device: slits_current type: I10SlitsDrainCurrent import_from: dodal.devices.i10 + decorators: + - name: devices.factory + args: {} params: prefix: f'{PREFIX.beamline_prefix}-' - device: diagnostics type: I10Diagnostic import_from: dodal.devices.i10.diagnostics + decorators: + - name: devices.factory + args: {} params: prefix: f'{PREFIX.beamline_prefix}-DI-' - device: d5a_det type: I10Diagnostic5ADet import_from: dodal.devices.i10.diagnostics + decorators: + - name: devices.factory + args: {} params: prefix: f'{PREFIX.beamline_prefix}-DI-' - device: pin_hole type: XYStage import_from: dodal.devices.motors + decorators: + - name: devices.factory + args: {} params: prefix: 'ME01D-EA-PINH-01:' - device: det_slits type: DetSlits import_from: dodal.devices.i10.rasor.rasor_motors + decorators: + - name: devices.factory + args: {} params: prefix: ME01D-MO-APTR-0 - device: diffractometer type: Diffractometer import_from: dodal.devices.i10.rasor.rasor_motors + decorators: + - name: devices.factory + args: {} params: prefix: 'ME01D-MO-DIFF-01:' - device: pa_stage type: PaStage import_from: dodal.devices.i10.rasor.rasor_motors + decorators: + - name: devices.factory + args: {} params: prefix: 'ME01D-MO-POLAN-01:' - device: sample_stage type: XYZStage import_from: dodal.devices.motors + decorators: + - name: devices.factory + args: {} params: prefix: 'ME01D-MO-CRYO-01:' - device: rasor_temperature_controller type: Lakeshore340 import_from: dodal.devices.temperture_controller + decorators: + - name: devices.factory + args: {} params: prefix: 'ME01D-EA-TCTRL-01:' - device: rasor_femto type: RasorFemto import_from: dodal.devices.i10.rasor.rasor_current_amp + decorators: + - name: devices.factory + args: {} params: prefix: ME01D-EA-IAMP - device: rasor_det_scalers type: RasorScalerCard1 import_from: dodal.devices.i10.rasor.rasor_scaler_cards + decorators: + - name: devices.factory + args: {} params: prefix: ME01D-EA-SCLR-01:SCALER1 - device: rasor_sr570 type: RasorSR570 import_from: dodal.devices.i10.rasor.rasor_current_amp + decorators: + - name: devices.factory + args: {} params: prefix: ME01D-EA-IAMP - device: rasor_sr570_pa_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers + decorators: + - name: devices.factory + args: {} params: current_amp: rasor_sr570().ca1 counter: rasor_det_scalers().det - device: rasor_femto_pa_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers + decorators: + - name: devices.factory + args: {} params: current_amp: rasor_femto().ca1 counter: rasor_det_scalers().det - device: rasor_sr570_fluo_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers + decorators: + - name: devices.factory + args: {} params: current_amp: rasor_sr570().ca2 counter: rasor_det_scalers().fluo - device: rasor_femto_fluo_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers + decorators: + - name: devices.factory + args: {} params: current_amp: rasor_femto().ca2 counter: rasor_det_scalers().fluo - device: rasor_sr570_drain_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers + decorators: + - name: devices.factory + args: {} params: current_amp: rasor_sr570().ca3 counter: rasor_det_scalers().drain - device: rasor_femto_drain_scaler_det type: CurrentAmpDet import_from: dodal.devices.current_amplifiers + decorators: + - name: devices.factory + args: {} params: current_amp: rasor_femto().ca3 counter: rasor_det_scalers().drain diff --git a/src/dodal/common/beamlines/config_generator.py b/src/dodal/common/beamlines/config_generator.py index 40129afa77..382da8c28c 100644 --- a/src/dodal/common/beamlines/config_generator.py +++ b/src/dodal/common/beamlines/config_generator.py @@ -1,6 +1,5 @@ import ast import os -import subprocess from collections import defaultdict from datetime import datetime from typing import Any @@ -100,18 +99,8 @@ def beamline_config_generator(config_dir: str) -> str: body = f"{dev.type}()" code += f"\ndef {dev.device}() -> {dev.type}:\n return {body}\n" - try: - fmt = subprocess.run( - ["ruff", "format", "-"], - input=code, - capture_output=True, - text=True, - check=True, - ) - return fmt.stdout - except Exception as e: - LOGGER.error(f"Ruff formatting failed, returning raw code. Error: {e}") - return code + + return code def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): @@ -137,10 +126,23 @@ def translate_beamline_py_config_to_yaml(py_file_path: str, output_dir: str): elif isinstance(node, ast.FunctionDef): ret_type = node.returns.id if isinstance(node.returns, ast.Name) else None + recovered_decs = [] + for dec_node in node.decorator_list: + if isinstance(dec_node, ast.Call): + name = ast.unparse(dec_node.func) + args = { + kw.arg: ast.literal_eval(kw.value) for kw in dec_node.keywords + } + recovered_decs.append({"name": name, "args": args}) + else: + # Handle decorators without parentheses, e.g., @devices.factory + recovered_decs.append({"name": ast.unparse(dec_node), "args": {}}) + device_meta = { "device": node.name, "type": ret_type, "import_from": import_map.get(ret_type, "unknown.module"), + "decorators": recovered_decs, } if node.body and isinstance(node.body[0], ast.Return): ret_val = node.body[0].value diff --git a/tests/common/beamlines/test_config_generator.py b/tests/common/beamlines/test_config_generator.py index 213a2c89c6..f9e5d55c3f 100644 --- a/tests/common/beamlines/test_config_generator.py +++ b/tests/common/beamlines/test_config_generator.py @@ -1,9 +1,31 @@ +import pytest +import yaml + from dodal.common.beamlines.config_generator import ( beamline_config_generator, translate_beamline_py_config_to_yaml, ) +@pytest.fixture +def mock_config_dir(tmp_path): + config_dir = tmp_path / "i10" + config_dir.mkdir() + + config_data = { + "beamline": "i10", + "base_imports": ["import os"], + "setup_script": "PREFIX = '{beamline}'", + "device_files": ["devices.yaml"], + "note": "Test Beamline", + } + + with open(config_dir / "config.yaml", "w") as f: + yaml.dump(config_data, f) + + return config_dir + + def test_generator_output_contains_expected_code(tmp_path): # Setup temporary YAML files conf_dir = tmp_path / "i10" @@ -34,23 +56,40 @@ def test_generator_output_contains_expected_code(tmp_path): assert 'prefix=f"{PREFIX}-MOT-01"' in code -def test_reverse_translate_beamline_py_config_to_yaml(tmp_path): +def test_decorators_with_args(tmp_path, mock_config_dir): + # YAML representation + devices_file = mock_config_dir / "devices.yaml" + devices_file.write_text(""" +- device: fancy_motor + type: Motor + import_from: dodal.devices + decorators: + - name: "devices.factory" + args: + skip: True + reason: "Testing" + - name: "other.wrapper" + args: + priority: 10 +""") + + code = beamline_config_generator(str(mock_config_dir)) + + assert "@devices.factory(skip=True, reason='Testing')" in code + assert "@other.wrapper(priority=10)" in code + + +def test_reverse_translator_recovers_decorator_args(tmp_path): py_file = tmp_path / "i10.py" py_file.write_text(""" -from dodal.devices.custom import CustomDevice - -@devices.factory() -def my_device() -> CustomDevice: - return CustomDevice() +@devices.factory(skip=False) +def my_dev() -> Motor: + return Motor() """) - output_dir = tmp_path / "recovered" translate_beamline_py_config_to_yaml(str(py_file), str(output_dir)) - import yaml - with open(output_dir / "devices.yaml") as f: - recovered = yaml.safe_load(f) - - assert recovered[0]["device"] == "my_device" - assert recovered[0]["import_from"] == "dodal.devices.custom" + devs = yaml.safe_load(f) + assert devs[0]["decorators"][0]["name"] == "devices.factory" + assert devs[0]["decorators"][0]["args"]["skip"] is False