diff --git a/pyaml/common/element.py b/pyaml/common/element.py index cdf83239..d5b6e6e4 100644 --- a/pyaml/common/element.py +++ b/pyaml/common/element.py @@ -62,11 +62,13 @@ def _class_exclusions(obj) -> set[str]: return exclusions -def _properties(obj) -> dict[str, Any]: +def _properties(obj, exclusions: set[str] = frozenset()) -> dict[str, Any]: + # Excluded properties are skipped before evaluation: a getter that raises + # with str(self) in its message would otherwise re-enter the repr. values: dict[str, Any] = {} for cls in reversed(type(obj).__mro__): for name, descriptor in vars(cls).items(): - if name.startswith("_") or not isinstance(descriptor, property): + if name.startswith("_") or name in exclusions or not isinstance(descriptor, property): continue try: values[name] = getattr(obj, name) @@ -75,7 +77,7 @@ def _properties(obj) -> dict[str, Any]: return values -def _fields(obj) -> dict[str, Any]: +def _fields(obj, exclusions: set[str] = frozenset()) -> dict[str, Any]: custom_fields = getattr(obj, "_pyaml_repr_fields", None) if custom_fields is not None: try: @@ -83,7 +85,7 @@ def _fields(obj) -> dict[str, Any]: except Exception as error: return {"value": _unavailable(error)} - values = _properties(obj) + values = _properties(obj, exclusions) for name, value in vars(obj).items(): if not name.startswith("_"): values.setdefault(name, value) @@ -158,10 +160,10 @@ def _format_object(obj, depth: int, active: set[int], extra_exclusions: set[str] active.add(id(obj)) try: - values = _fields(obj) exclusions = _class_exclusions(obj) if extra_exclusions: exclusions.update(extra_exclusions) + values = _fields(obj, exclusions) parts = [] for name, value in values.items(): if name.startswith("_") or name in exclusions or callable(value): diff --git a/pyaml/diagnostics/bpm/bpm.py b/pyaml/diagnostics/bpm/bpm.py index c63f662a..1f432ad5 100644 --- a/pyaml/diagnostics/bpm/bpm.py +++ b/pyaml/diagnostics/bpm/bpm.py @@ -116,7 +116,7 @@ def positions(self) -> ReadFloatArray: If positions have not been attached """ if self._positions is None: - raise PyAMLException(f"{str(self)} has no attached positions") + raise PyAMLException(f"{self.name} has no attached positions") return self._positions @property @@ -135,7 +135,7 @@ def offset(self) -> ReadWriteFloatArray: If offset has not been attached """ if self._offset is None: - raise PyAMLException(f"{str(self)} has no attached offset") + raise PyAMLException(f"{self.name} has no attached offset") return self._offset @property @@ -154,7 +154,7 @@ def tilt(self) -> ReadWriteFloatScalar: If tilt has not been attached """ if self._tilt is None: - raise PyAMLException(f"{str(self)} has no attached tilt") + raise PyAMLException(f"{self.name} has no attached tilt") return self._tilt def attach( diff --git a/pyaml/magnet/cfm_magnet.py b/pyaml/magnet/cfm_magnet.py index 83c5578f..47140100 100644 --- a/pyaml/magnet/cfm_magnet.py +++ b/pyaml/magnet/cfm_magnet.py @@ -239,7 +239,7 @@ def strengths(self) -> abstract.ReadWriteFloatScalar: """ self.check_peer() if self.__strengths is None: - raise PyAMLException(f"{str(self)} has no model that supports physics units") + raise PyAMLException(f"{self.name} has no model that supports physics units") return self.__strengths @property @@ -250,7 +250,7 @@ def hardwares(self) -> abstract.ReadWriteFloatScalar: """ self.check_peer() if self.__hardwares is None: - raise PyAMLException(f"{str(self)} has no model that supports hardware units") + raise PyAMLException(f"{self.name} has no model that supports hardware units") return self.__hardwares def set_energy(self, E: float): diff --git a/pyaml/magnet/magnet.py b/pyaml/magnet/magnet.py index b01491d7..fd5a4a09 100644 --- a/pyaml/magnet/magnet.py +++ b/pyaml/magnet/magnet.py @@ -83,7 +83,7 @@ def strength(self) -> abstract.ReadWriteFloatScalar: """ self.check_peer() if self.__strength is None: - raise PyAMLException(f"{str(self)} has no model that supports physics units") + raise PyAMLException(f"{self.name} has no model that supports physics units") return self.__strength @property @@ -94,7 +94,7 @@ def hardware(self) -> abstract.ReadWriteFloatScalar: """ self.check_peer() if self.__hardware is None: - raise PyAMLException(f"{str(self)} has no model that supports hardware units") + raise PyAMLException(f"{self.name} has no model that supports hardware units") return self.__hardware @property diff --git a/pyaml/magnet/serialized_magnet.py b/pyaml/magnet/serialized_magnet.py index ddfe1733..c5aab6fa 100644 --- a/pyaml/magnet/serialized_magnet.py +++ b/pyaml/magnet/serialized_magnet.py @@ -355,7 +355,7 @@ def strength(self) -> abstract.ReadWriteFloatScalar: """ self.check_peer() if self.__strengths is None: - raise PyAMLException(f"{str(self)} has no model that supports physics units") + raise PyAMLException(f"{self.name} has no model that supports physics units") return self.__strengths @property @@ -365,7 +365,7 @@ def hardware(self) -> abstract.ReadWriteFloatScalar: """ self.check_peer() if self.__hardwares is None: - raise PyAMLException(f"{str(self)} has no model that supports hardware units") + raise PyAMLException(f"{self.name} has no model that supports hardware units") return self.__hardwares def set_energy(self, energy: float): diff --git a/tests/diagnostics/bpm/test_bpm.py b/tests/diagnostics/bpm/test_bpm.py index 3bf8ee71..322ec6fb 100644 --- a/tests/diagnostics/bpm/test_bpm.py +++ b/tests/diagnostics/bpm/test_bpm.py @@ -2,6 +2,8 @@ import pytest from pyaml.accelerator import Accelerator +from pyaml.common.exception import PyAMLException +from pyaml.diagnostics.bpm.bpm import BPM def test_simulator_bpm_tilt(): @@ -67,3 +69,13 @@ def test_simulator_bpm_position_with_bad_corrector_strength(): for bpm in [bpm1, bpm_simple, bpm3]: assert bpm.positions.get()[0] != 0.0 assert bpm.positions.get()[1] != 0.0 + + +def test_unattached_bpm_errors_name_bpm_without_formatting_it(): + bpm = BPM(name="BPM1") + + for accessor in ("positions", "offset", "tilt"): + with pytest.raises(PyAMLException, match=f"BPM1 has no attached {accessor}"): + getattr(bpm, accessor) + + assert "BPM1" in repr(bpm) diff --git a/tests/magnet/test_missing_model_errors.py b/tests/magnet/test_missing_model_errors.py new file mode 100644 index 00000000..7ae1d368 --- /dev/null +++ b/tests/magnet/test_missing_model_errors.py @@ -0,0 +1,16 @@ +import pytest + +from pyaml import PyAMLException +from pyaml.magnet.hcorrector import HCorrector + + +def test_missing_model_errors_name_magnet_without_formatting_it(): + magnet = HCorrector("CH1").attach(object(), strength=None, hardware=None) + + with pytest.raises(PyAMLException, match="CH1 has no model that supports physics units"): + _ = magnet.strength + + with pytest.raises(PyAMLException, match="CH1 has no model that supports hardware units"): + _ = magnet.hardware + + assert "CH1" in repr(magnet)