diff --git a/docs/changes/newsfragments/6585.improved b/docs/changes/newsfragments/6585.improved new file mode 100644 index 00000000000..1b9c5ad5561 --- /dev/null +++ b/docs/changes/newsfragments/6585.improved @@ -0,0 +1,3 @@ +``DelegateParameter`` now includes validators of its source Parameter into its validators. This ensures that a ``DelegateParameter`` +with a non numeric source parameter is registered correctly in a measurement when the ``DelegateParameter`` it self does not +set a validator. diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index b4c6ecfedf6..f49d695f10e 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -8,6 +8,8 @@ from collections.abc import Sequence from datetime import datetime + from qcodes.validators.validators import Validator + from .parameter_base import ParamDataType, ParamRawDataType @@ -314,3 +316,18 @@ def validate(self, value: ParamDataType) -> None: super().validate(value) if self.source is not None: self.source.validate(self._from_value_to_raw_value(value)) + + @property + def validators(self) -> tuple[Validator, ...]: + """ + Tuple of all validators associated with the parameter. Note that this + includes validators of the source parameter if source parameter is set + and has any validators. + + :getter: All validators associated with the parameter. + """ + source_validators: tuple[Validator, ...] = ( + self.source.validators if self.source is not None else () + ) + + return tuple(self._vals) + source_validators diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 452896f6fb5..6ce74a53321 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -383,9 +383,10 @@ def vals(self) -> Validator | None: RuntimeError: If removing the first validator when more than one validator is set. """ + validators = self.validators - if len(self._vals): - return self._vals[0] + if len(validators): + return validators[0] else: return None diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 0a0e0dee211..edd2f80d997 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -24,8 +24,14 @@ from qcodes.dataset.export_config import DataExportType from qcodes.dataset.measurements import Measurement from qcodes.dataset.sqlite.connection import atomic_transaction -from qcodes.parameters import ManualParameter, Parameter, expand_setpoints_helper +from qcodes.parameters import ( + DelegateParameter, + ManualParameter, + Parameter, + expand_setpoints_helper, +) from qcodes.station import Station +from qcodes.validators import ComplexNumbers from tests.common import retry_until_does_not_throw @@ -201,6 +207,65 @@ def test_register_custom_parameter(DAC) -> None: ) +def test_register_delegate_parameters() -> None: + x_param = Parameter("x", set_cmd=None, get_cmd=None) + + complex_param = Parameter( + "complex_param", get_cmd=None, set_cmd=None, vals=ComplexNumbers() + ) + delegate_param = DelegateParameter("delegate", source=complex_param) + + meas = Measurement() + + meas.register_parameter(x_param) + meas.register_parameter(delegate_param, setpoints=(x_param,)) + assert len(meas.parameters) == 2 + assert meas.parameters["delegate"].type == "complex" + assert meas.parameters["x"].type == "numeric" + + +def test_register_delegate_parameters_with_late_source() -> None: + x_param = Parameter("x", set_cmd=None, get_cmd=None) + + complex_param = Parameter( + "complex_param", get_cmd=None, set_cmd=None, vals=ComplexNumbers() + ) + delegate_param = DelegateParameter("delegate", source=None) + + meas = Measurement() + + meas.register_parameter(x_param) + + delegate_param.source = complex_param + + meas.register_parameter(delegate_param, setpoints=(x_param,)) + assert len(meas.parameters) == 2 + assert meas.parameters["delegate"].type == "complex" + assert meas.parameters["x"].type == "numeric" + + +def test_register_delegate_parameters_with_late_source_chain(): + x_param = Parameter("x", set_cmd=None, get_cmd=None) + + complex_param = Parameter( + "complex_param", get_cmd=None, set_cmd=None, vals=ComplexNumbers() + ) + delegate_inner = DelegateParameter("delegate_inner", source=None) + delegate_outer = DelegateParameter("delegate_outer", source=None) + + meas = Measurement() + + meas.register_parameter(x_param) + + delegate_outer.source = delegate_inner + delegate_inner.source = complex_param + + meas.register_parameter(delegate_outer, setpoints=(x_param,)) + assert len(meas.parameters) == 2 + assert meas.parameters["delegate_outer"].type == "complex" + assert meas.parameters["x"].type == "numeric" + + def test_unregister_parameter(DAC, DMM) -> None: """ Test the unregistering of parameters. diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 07b7d093c0f..a232154fdeb 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -574,18 +574,23 @@ def test_value_validation() -> None: source_param = Parameter("source", set_cmd=None, get_cmd=None) delegate_param = DelegateParameter("delegate", source=source_param) + # Test case where source parameter validator is None and delegate parameter validator is + # specified. delegate_param.vals = vals.Numbers(-10, 10) source_param.vals = None delegate_param.validate(1) with pytest.raises(ValueError): delegate_param.validate(11) + # Test where delegate parameter validator is None and source parameter validator is + # specified. delegate_param.vals = None source_param.vals = vals.Numbers(-5, 5) delegate_param.validate(1) with pytest.raises(ValueError): delegate_param.validate(6) + # Test case where source parameter validator is more restricted than delegate parameter. delegate_param.vals = vals.Numbers(-10, 10) source_param.vals = vals.Numbers(-5, 5) delegate_param.validate(1) @@ -594,6 +599,115 @@ def test_value_validation() -> None: with pytest.raises(ValueError): delegate_param.validate(11) + # Test case that the order of setting validator on source and delegate parameters does not matter. + source_param.vals = vals.Numbers(-5, 5) + delegate_param.vals = vals.Numbers(-10, 10) + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + with pytest.raises(ValueError): + delegate_param.validate(11) + + # Test case where delegate parameter validator is more restricted than source parameter. + delegate_param.vals = vals.Numbers(-5, 5) + source_param.vals = vals.Numbers(-10, 10) + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + with pytest.raises(ValueError): + delegate_param.validate(11) + + # Test case that the order of setting validator on source and delegate parameters does not matter. + source_param.vals = vals.Numbers(-10, 10) + delegate_param.vals = vals.Numbers(-5, 5) + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + with pytest.raises(ValueError): + delegate_param.validate(11) + + +def test_validator_delegates_as_expected() -> None: + source_param = Parameter("source", set_cmd=None, get_cmd=None) + delegate_param = DelegateParameter("delegate", source=source_param) + some_validator = vals.Numbers(-10, 10) + source_param.vals = some_validator + delegate_param.vals = None + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(11) + assert delegate_param.validators == (some_validator,) + assert delegate_param.vals == some_validator + + +def test_validator_delegates_and_source() -> None: + source_param = Parameter("source", set_cmd=None, get_cmd=None) + delegate_param = DelegateParameter("delegate", source=source_param) + some_validator = vals.Numbers(-10, 10) + some_other_validator = vals.Numbers(-5, 5) + source_param.vals = some_validator + delegate_param.vals = some_other_validator + delegate_param.validate(1) + with pytest.raises(ValueError): + delegate_param.validate(6) + assert delegate_param.validators == (some_other_validator, some_validator) + assert delegate_param.vals == some_other_validator + + assert delegate_param.source is not None + delegate_param.source.vals = None + + assert delegate_param.validators == (some_other_validator,) + assert delegate_param.vals == some_other_validator + + +def test_validator_delegates_and_source_chain() -> None: + source_param = Parameter("source", set_cmd=None, get_cmd=None) + delegate_inner = DelegateParameter("delegate_inner", source=source_param) + delegate_outer = DelegateParameter("delegate_outer", source=delegate_inner) + source_validator = vals.Numbers(-10, 10) + delegate_inner_validator = vals.Numbers(-7, 7) + delegate_outer_validator = vals.Numbers(-5, 5) + + source_param.vals = source_validator + delegate_inner.vals = delegate_inner_validator + delegate_outer.vals = delegate_outer_validator + + delegate_outer.validate(1) + with pytest.raises(ValueError): + delegate_outer.validate(6) + + delegate_inner.validate(6) + source_param.validate(6) + + assert delegate_outer.validators == ( + delegate_outer_validator, + delegate_inner_validator, + source_validator, + ) + assert delegate_outer.vals == delegate_outer_validator + + assert delegate_inner.validators == ( + delegate_inner_validator, + source_validator, + ) + assert delegate_inner.vals == delegate_inner_validator + + assert delegate_outer.source is not None + delegate_outer.source.vals = None + + assert delegate_outer.validators == ( + delegate_outer_validator, + source_validator, + ) + assert delegate_outer.vals == delegate_outer_validator + + assert isinstance(delegate_outer.source, DelegateParameter) + assert delegate_outer.source.source is not None + delegate_outer.source.source.vals = None + + assert delegate_outer.validators == (delegate_outer_validator,) + assert delegate_outer.vals == delegate_outer_validator + def test_value_validation_with_offset_and_scale() -> None: source_param = Parameter(