From 4f62344a149e409be9e52fd33d77e7bcb080b2b9 Mon Sep 17 00:00:00 2001 From: Teresia Olsson Date: Fri, 10 Jul 2026 16:48:49 +0200 Subject: [PATCH 1/6] Update asserts in text to printout the value of the error message. --- tests/common/test_errors.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/common/test_errors.py b/tests/common/test_errors.py index cd8546b7..1912847e 100644 --- a/tests/common/test_errors.py +++ b/tests/common/test_errors.py @@ -14,36 +14,36 @@ def test_tune(install_test_package): with pytest.raises(PyAMLConfigException) as exc: ml: Accelerator = Accelerator.load("tests/config/bad_conf_duplicate_1.yaml", include_locations=True, validate=True) - assert "MagnetArray HCORR : duplicate name SH1A-C02-H @index 2" in str(exc) + assert "MagnetArray HCORR : duplicate name SH1A-C02-H @index 2" in str(exc.value) with pytest.raises(PyAMLConfigException) as exc: ml: Accelerator = Accelerator.load("tests/config/bad_conf_duplicate_2.yaml", include_locations=True, validate=True) - assert "BPMArray BPM : duplicate name BPM_C04-06 @index 3" in str(exc) + assert "BPMArray BPM : duplicate name BPM_C04-06 @index 3" in str(exc.value) with pytest.raises(PyAMLConfigException) as exc: ml: Accelerator = Accelerator.load("tests/config/bad_conf_duplicate_3.yaml", include_locations=True, validate=True) - assert "Configuration entry 'BPM_C04-06' is duplicated inside category 'devices'" in str(exc) - assert "bad_conf_duplicate_3.yaml" in str(exc) - assert "line 43, column 3" in str(exc) + assert "Configuration entry 'BPM_C04-06' is duplicated inside category 'devices'" in str(exc.value) + assert "bad_conf_duplicate_3.yaml" in str(exc.value) + assert "line 43, column 3" in str(exc.value) with pytest.raises(PyAMLConfigException) as exc: ml: Accelerator = Accelerator.load("tests/config/bad_conf_duplicate_4.yaml", include_locations=True, validate=True) - assert "MagnetArray HCORR : duplicate name SH1A-C02-H @index 2" in str(exc) + assert "MagnetArray HCORR : duplicate name SH1A-C02-H @index 2" in str(exc.value) sr: Accelerator = Accelerator.load("tests/config/EBSTune.yaml", include_locations=True, validate=True) m1 = sr.live.get_magnet("QF1E-C04") m2 = sr.design.get_magnet("QF1A-C05") with pytest.raises(PyAMLException) as exc: ma = MagnetArray("Test", [m1, m2]) - assert "MagnetArray Test: All elements must be attached to the same instance" in str(exc) + assert "MagnetArray Test: All elements must be attached to the same instance" in str(exc.value) with pytest.raises(PyAMLException) as exc: m2 = sr.design.get_magnet("QF1A-C05XX") - assert "Magnet QF1A-C05XX not defined" in str(exc) + assert "Magnet QF1A-C05XX not defined" in str(exc.value) with pytest.raises(PyAMLException) as exc: m2 = sr.design.get_bpm("QF1A-C05XX") - assert "BPM QF1A-C05XX not defined" in str(exc) + assert "BPM QF1A-C05XX not defined" in str(exc.value) def test_duplicate_error_reports_source_line_and_column_across_files(tmp_path): From 2fb6a886a63df5b167ce773537f534f4af4370aa Mon Sep 17 00:00:00 2001 From: Teresia Olsson Date: Fri, 10 Jul 2026 17:41:41 +0200 Subject: [PATCH 2/6] Improve error message in factory. --- pyaml/configuration/factory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyaml/configuration/factory.py b/pyaml/configuration/factory.py index f607e361..0035cb12 100644 --- a/pyaml/configuration/factory.py +++ b/pyaml/configuration/factory.py @@ -284,7 +284,9 @@ def _build_object(self, data: dict, ignore_external: bool = False): try: cfg = build_info.config_cls.model_validate(config) except ValidationError as exc: - raise PyAMLConfigException(str(exc)) from exc + raise PyAMLConfigException( + f"Validation failed for {build_info.config_cls.__module__}.{build_info.config_cls.__name__}:\n{exc}" + ) from exc else: cfg = config From 1174dfa3109e282d1f855321e6230bee5b311028 Mon Sep 17 00:00:00 2001 From: Teresia Olsson Date: Fri, 10 Jul 2026 17:43:28 +0200 Subject: [PATCH 3/6] Recursively dump in validate_to_dict. --- pyaml/validation/validator.py | 42 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/pyaml/validation/validator.py b/pyaml/validation/validator.py index 03cd4cec..b86a69f2 100644 --- a/pyaml/validation/validator.py +++ b/pyaml/validation/validator.py @@ -1,18 +1,51 @@ """Module for schema validation.""" +from collections.abc import Mapping import logging import warnings from typing import Any -from pydantic import ValidationError +from pydantic import ValidationError, BaseModel from .configuration_models import ConfigurationSchema, ModuleConfigurationSchema from .errors import extract_location_metadata, raise_validation_error from .registry import SchemaRegistry + logger = logging.getLogger(__name__) +def dump_nested(value: Any) -> Any: + """ + Recursively convert nested Pydantic models to plain Python objects. + + Traverses mappings, lists, and tuples, replacing any + :class:`pydantic.BaseModel` instances with the result of + :meth:`BaseModel.model_dump`. All other values are returned unchanged. + + Parameters + ---------- + value : Any + The object to convert. + + Returns + ------- + Any + The converted object with all nested Pydantic models represented as + dictionaries. + """ + + if isinstance(value, BaseModel): + return value.model_dump() + if isinstance(value, Mapping): + return {k: dump_nested(v) for k, v in value.items()} + if isinstance(value, list): + return [dump_nested(v) for v in value] + if isinstance(value, tuple): + return tuple(dump_nested(v) for v in value) + return value + + class SchemaValidator: """Recursive validator for configuration dictionaries. @@ -62,6 +95,7 @@ def validate( # raise TypeError("Top-level configuration did not validate to a ConfigurationSchema.") return validated + @classmethod def validate_to_dict( @@ -73,9 +107,9 @@ def validate_to_dict( """ validated = cls.validate(data) - if isinstance(validated, dict): - return validated - return validated.model_dump() + return dump_nested(validated) + + @classmethod def _recursive_validate(cls, obj: Any) -> Any: From 92b592f8f99c3abc129000ee52950ebff3f2cc90 Mon Sep 17 00:00:00 2001 From: Teresia Olsson Date: Fri, 10 Jul 2026 17:47:09 +0200 Subject: [PATCH 4/6] Changes from missing precommit hook run. --- pyaml/validation/validator.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pyaml/validation/validator.py b/pyaml/validation/validator.py index b86a69f2..cee96a45 100644 --- a/pyaml/validation/validator.py +++ b/pyaml/validation/validator.py @@ -1,17 +1,16 @@ """Module for schema validation.""" -from collections.abc import Mapping import logging import warnings +from collections.abc import Mapping from typing import Any -from pydantic import ValidationError, BaseModel +from pydantic import BaseModel, ValidationError from .configuration_models import ConfigurationSchema, ModuleConfigurationSchema from .errors import extract_location_metadata, raise_validation_error from .registry import SchemaRegistry - logger = logging.getLogger(__name__) @@ -34,7 +33,7 @@ def dump_nested(value: Any) -> Any: The converted object with all nested Pydantic models represented as dictionaries. """ - + if isinstance(value, BaseModel): return value.model_dump() if isinstance(value, Mapping): @@ -42,8 +41,8 @@ def dump_nested(value: Any) -> Any: if isinstance(value, list): return [dump_nested(v) for v in value] if isinstance(value, tuple): - return tuple(dump_nested(v) for v in value) - return value + return tuple(dump_nested(v) for v in value) + return value class SchemaValidator: @@ -95,7 +94,6 @@ def validate( # raise TypeError("Top-level configuration did not validate to a ConfigurationSchema.") return validated - @classmethod def validate_to_dict( @@ -108,8 +106,6 @@ def validate_to_dict( validated = cls.validate(data) return dump_nested(validated) - - @classmethod def _recursive_validate(cls, obj: Any) -> Any: From f44fda815a043081ed7aed58337e1454a58b193c Mon Sep 17 00:00:00 2001 From: Teresia Olsson Date: Fri, 10 Jul 2026 17:49:50 +0200 Subject: [PATCH 5/6] Remove config model from BPM. --- pyaml/bpm/bpm.py | 100 +++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index 22f3514a..3004dee9 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -1,60 +1,60 @@ +import copy +from typing import Self + from ..common.abstract import ReadFloatArray, ReadWriteFloatArray, ReadWriteFloatScalar -from ..common.element import Element, ElementConfigModel +from ..common.element import Element from ..common.exception import PyAMLException - -try: - from typing import Self # Python 3.11+ -except ImportError: - from typing_extensions import Self # Python 3.10 and earlier +from ..validation import DynamicValidation, register_schema PYAMLCLASS = "BPM" -class ConfigModel(ElementConfigModel): +@register_schema +class BPM(Element, DynamicValidation): """ - Configuration model for BPM element. + Beam position monitor (BPM) element. + + Represents a BPM in the accelerator lattice and provides access to its + associated readback signals, including horizontal and vertical beam + positions, calibration offsets, and mechanical tilt. Parameters ---------- - x_pos : str - Horizontal position device catalog key - y_pos : str - Vertical position device catalog key - x_offset : str - Horizontal BPM offset device catalog key - y_offset : str - Vertical BPM offset device catalog key - tilt : str - BPM tilt device catalog key - """ - - x_pos: str | None = None - y_pos: str | None = None - x_offset: str | None = None - y_offset: str | None = None - tilt: str | None = None - - -class BPM(Element): - """ - Class providing access to one BPM of a physical or simulated lattice + name : str + Name of the BPM. + lattice_names : str | None, optional + Lattice-specific name or names identifying the BPM. + description : str | None, optional + Description of the BPM. + x_pos : str | None, optional + Device catalog key for the horizontal beam position. + y_pos : str | None, optional + Device catalog key for the vertical beam position. + x_offset : str | None, optional + Device catalog key for the horizontal BPM offset. + y_offset : str | None, optional + Device catalog key for the vertical BPM offset. + tilt : str | None, optional + Device catalog key for the BPM tilt. """ - def __init__(self, cfg: ConfigModel): - """ - Construct a BPM - - Parameters - ---------- - name : str - Element name - model : BPMModel - BPM model in charge of computing beam position - """ - - super().__init__(cfg.name) - - self._cfg = cfg + def __init__( + self, + name: str, + lattice_names: str | None = None, + description: str | None = None, + x_pos: str | None = None, + y_pos: str | None = None, + x_offset: str | None = None, + y_offset: str | None = None, + tilt: str | None = None, + ): + super().__init__(name, lattice_names, description) + self._x_pos = x_pos + self._y_pos = y_pos + self._x_offset = x_offset + self._y_offset = y_offset + self._tilt_name = tilt self._positions = None self._offset = None self._tilt = None @@ -144,14 +144,14 @@ def attach( """ # Attach positions, offset and tilt attributes and returns a new # reference - obj = self.__class__(self._cfg) + obj = copy.copy(self) obj._positions = positions obj._offset = offset obj._tilt = tilt obj._peer = peer return obj - def get_pos_devices(self) -> list[str]: + def get_pos_devices(self) -> list[str | None]: """ Get device handles used for position reading @@ -160,7 +160,7 @@ def get_pos_devices(self) -> list[str]: list[DeviceAccess] Array of DeviceAcess """ - return [self._cfg.x_pos, self._cfg.y_pos] + return [self._x_pos, self._y_pos] def get_tilt_device(self) -> str | None: """ @@ -171,7 +171,7 @@ def get_tilt_device(self) -> str | None: DeviceAccess DeviceAcess """ - return self._cfg.tilt + return self._tilt_name def get_offset_devices(self) -> list[str | None]: """ @@ -182,4 +182,4 @@ def get_offset_devices(self) -> list[str | None]: list[DeviceAccess] Array of DeviceAcess """ - return [self._cfg.x_offset, self._cfg.y_offset] + return [self._x_offset, self._y_offset] From 3ee18b124dc10ecc62ab4c35edde9aea31321e46 Mon Sep 17 00:00:00 2001 From: Teresia Olsson Date: Fri, 10 Jul 2026 19:08:18 +0200 Subject: [PATCH 6/6] Add repr to BPM. --- pyaml/bpm/bpm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index 3004dee9..ccdbc0c8 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -2,7 +2,7 @@ from typing import Self from ..common.abstract import ReadFloatArray, ReadWriteFloatArray, ReadWriteFloatScalar -from ..common.element import Element +from ..common.element import Element, __pyaml_repr__ from ..common.exception import PyAMLException from ..validation import DynamicValidation, register_schema @@ -183,3 +183,6 @@ def get_offset_devices(self) -> list[str | None]: Array of DeviceAcess """ return [self._x_offset, self._y_offset] + + def __repr__(self): + return __pyaml_repr__(self, exclude=["positions", "offset", "tilt"])