diff --git a/docs/changes/newsfragments/6648.improved b/docs/changes/newsfragments/6648.improved new file mode 100644 index 000000000000..5ea7d7c97ffb --- /dev/null +++ b/docs/changes/newsfragments/6648.improved @@ -0,0 +1,2 @@ +DelegateParameter now reads delegated properties from source rather than setting them at source change. +This resolves an issue when delegating from another delegate parameter may result in incorrect attributes. diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index 07e26699da96..dd0e450daea1 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -156,19 +156,6 @@ def __init__( if "bind_to_instrument" not in kwargs.keys(): kwargs["bind_to_instrument"] = False - self._attr_inherit = { - "label": {"fixed": False, "value_when_without_source": name}, - "unit": {"fixed": False, "value_when_without_source": ""}, - } - - for attr, attr_props in self._attr_inherit.items(): - if attr in kwargs: - attr_props["fixed"] = True - else: - attr_props["fixed"] = False - source_attr = getattr(source, attr, attr_props["value_when_without_source"]) - kwargs[attr] = kwargs.get(attr, source_attr) - for cmd in ("set_cmd", "get_cmd"): if cmd in kwargs: raise KeyError( @@ -188,9 +175,14 @@ def __init__( initial_cache_value = kwargs.pop("initial_cache_value", None) self.source = source super().__init__(name, *args, **kwargs) - # explicitly set the source properties as - # init will overwrite the ones set when assigning source - self._set_properties_from_source(source) + self.label = kwargs.get("label", None) + self.unit = kwargs.get("unit", None) + + # Hack While we inherit the settable status from the parent parameter + # we do allow param.set_to to temporary override _settable in a + # context. Here _settable should always be true except when set_to + # i.e. _SetParamContext overrides it + self._settable = True self.cache = self._DelegateCache(self) if initial_cache_value is not None: @@ -209,25 +201,62 @@ def source(self) -> Parameter | None: @source.setter def source(self, source: Parameter | None) -> None: - self._set_properties_from_source(source) self._source: Parameter | None = source - def _set_properties_from_source(self, source: Parameter | None) -> None: - if source is None: - self._gettable = False - self._settable = False - self._snapshot_value = False + @property + def snapshot_value(self) -> bool: + if self.source is None: + return False + return self.source.snapshot_value + + @property + def unit(self) -> str: + """ + The unit of measure. Read from source if not explicitly overwritten. + Set to None to disable overwrite. + """ + if self._unit_override is not None: + return self._unit_override + elif self.source is not None: + return self.source.unit else: - self._gettable = source.gettable - self._settable = source.settable - self._snapshot_value = source._snapshot_value - - for attr, attr_props in self._attr_inherit.items(): - if not attr_props["fixed"]: - attr_val = getattr( - source, attr, attr_props["value_when_without_source"] - ) - setattr(self, attr, attr_val) + return "" + + @unit.setter + def unit(self, unit: str | None) -> None: + self._unit_override = unit + + @property + def label(self) -> str: + """ + Label of the data used for plots etc. + Read from source if not explicitly overwritten. + Set to None to disable overwrite. + """ + if self._label_override is not None: + return self._label_override + elif self.source is not None: + return self.source.label + else: + return self.name + + @label.setter + def label(self, label: str | None) -> None: + self._label_override = label + + @property + def gettable(self) -> bool: + if self.source is None: + return False + return self.source.gettable + + @property + def settable(self) -> bool: + if self._settable is False: + return False + if self.source is None: + return False + return self.source.settable def get_raw(self) -> Any: if self.source is None: diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index f4776be83df9..b4358caa3cf8 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -537,7 +537,7 @@ def snapshot_base( state: dict[str, Any] = {"__class__": full_class(self), "full_name": str(self)} - if self._snapshot_value: + if self.snapshot_value: has_get = self.gettable allowed_to_call_get_when_snapshotting = ( self._snapshot_get and update is not False diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 7b39d6b1f9ad..8cf89bd26e8b 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -426,7 +426,7 @@ def test_gettable_settable_snapshotget_delegate_parameter( delegate_param = DelegateParameter("delegate", source=source_param) assert delegate_param.gettable is gettable assert delegate_param.settable is settable - assert delegate_param._snapshot_value is snapshot_value + assert delegate_param.snapshot_value is snapshot_value @pytest.mark.parametrize("snapshot_value", [True, False]) @@ -450,7 +450,7 @@ def test_gettable_settable_snapshotget_delegate_parameter_2( delegate_param.source = source_param assert delegate_param.gettable is gettable assert delegate_param.settable is settable - assert delegate_param._snapshot_value is snapshot_value + assert delegate_param.snapshot_value is snapshot_value def test_initial_value_and_none_source_raises() -> None: @@ -624,3 +624,64 @@ def test_value_validation_with_offset_and_scale() -> None: delegate_param.validate(1) # raw_value = 100 with pytest.raises(ValueError): delegate_param.set(1) + + +def test_delegate_of_delegate_updates_settable_gettable(): + gettable_settable_source_param = Parameter( + "source", set_cmd=None, get_cmd=None, vals=vals.Numbers(-5, 5) + ) + not_gettable_source_param = Parameter( + "source", set_cmd=None, get_cmd=False, vals=vals.Numbers(-5, 5) + ) + not_settable_source_param = Parameter( + "source", set_cmd=False, get_cmd=None, vals=vals.Numbers(-5, 5) + ) + delegate_param_inner = DelegateParameter( + "delegate_inner", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_outer = DelegateParameter( + "delegate_outer", source=None, vals=vals.Numbers(-10, 10) + ) + delegate_param_outer.source = delegate_param_inner + delegate_param_inner.source = gettable_settable_source_param + + assert delegate_param_outer.gettable + assert delegate_param_outer.settable + + delegate_param_inner.source = not_gettable_source_param + + assert not delegate_param_outer.gettable + assert delegate_param_outer.settable + + delegate_param_inner.source = not_settable_source_param + + assert delegate_param_outer.gettable + assert not delegate_param_outer.settable + + +def test_delegate_parameter_context() -> None: + gettable_settable_source_param = Parameter( + "source", set_cmd=None, get_cmd=None, vals=vals.Numbers(-5, 5) + ) + + delegate_param = DelegateParameter( + "delegate_outer", source=None, vals=vals.Numbers(-10, 10) + ) + + delegate_param.source = gettable_settable_source_param + + delegate_param(2) + assert delegate_param() == 2 + + with delegate_param.set_to(3): + assert delegate_param() == 3 + with pytest.raises(NotImplementedError): + delegate_param(4) + assert delegate_param() == 3 + assert delegate_param() == 2 + + with delegate_param.set_to(3, allow_changes=True): + assert delegate_param() == 3 + delegate_param(4) + assert delegate_param() == 4 + assert delegate_param() == 2