Skip to content

Commit

Permalink
Merge #4292
Browse files Browse the repository at this point in the history
4292: ChannelTuple: add ability to call regular callable r=jenshnielsen a=jenshnielsen

This IMHO eliminates the only important feature Functions has over a regular method on a InstrumentModule/Channel

TODO

- [x] Add a changelog entry

Co-authored-by: Jens H. Nielsen <Jens.Nielsen@microsoft.com>
Co-authored-by: Jens Hedegaard Nielsen <jenshnielsen@gmail.com>
  • Loading branch information
3 people committed Jul 26, 2022
2 parents 1845f3e + e9028bc commit 9d27254
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 7 deletions.
2 changes: 2 additions & 0 deletions docs/changes/newsfragments/4292.improved
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``ChannelTuple`` and ``ChannelList`` has gained the ability to call methods defined on the channels
in the sequence in a way similar to how QCoDeS Functions can be called.
13 changes: 12 additions & 1 deletion qcodes/instrument/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ def __getattr__(
name: The name of the parameter, function or channel that we want to
operate on.
"""
# Check if this is a valid parameter
if len(self) > 0:
# Check if this is a valid parameter
if name in self._channels[0].parameters:
param = self._construct_multiparam(name)
return param
Expand All @@ -402,6 +402,17 @@ def multi_func(*args: Any) -> None:

return multi_func

# check if this is a method on the channels in the
# sequence
maybe_callable = getattr(self._channels[0], name, None)
if callable(maybe_callable):

def multi_callable(*args: Any) -> None:
for chan in self._channels:
getattr(chan, name)(*args)

return multi_callable

try:
return self._channel_mapping[name]
except KeyError:
Expand Down
8 changes: 6 additions & 2 deletions qcodes/instrument/instrument_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,12 @@ def add_function(self, name: str, **kwargs: Any) -> None:
This functionality is meant for simple cases, principally things that
map to simple commands like ``*RST`` (reset) or those with just a few
arguments. It requires a fixed argument count, and positional args
only. If your case is more complicated, you're probably better off
simply making a new method in your ``Instrument`` subclass definition.
only.
Note:
We do not recommend the usage of Function for any new driver.
Function does not add any significant features over a method
defined on the class.
Args:
name: How the Function will be stored within
Expand Down
11 changes: 7 additions & 4 deletions qcodes/parameters/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ class Function(Metadatable):
map to simple commands like ``*RST`` (reset) or those with just a few
arguments.
It requires a fixed argument count, and positional args
only. If your case is more complicated, you're probably better off
simply making a new method in your Instrument subclass definition.
The function validators.validate_all can help reduce boilerplate code
in this case.
only.
You execute this function object like a normal function, or use its
.call method.
Expand All @@ -29,6 +26,12 @@ class Function(Metadatable):
Parsers only apply if call_cmd is a string. The function form of
call_cmd should do its own parsing.
Note:
We do not recommend the usage of Function for any new driver.
Function does not add any significant features over a method
defined on the class.
Args:
name: the local name of this function
Expand Down
3 changes: 3 additions & 0 deletions qcodes/tests/instrument_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ def __init__(self, parent, name, channel):
self.add_function(name='log_my_name',
call_cmd=partial(log.debug, f'{name}'))

def turn_on(self) -> None:
pass


class DummyChannelInstrument(Instrument):
"""
Expand Down
25 changes: 25 additions & 0 deletions qcodes/tests/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def _make_empty_instrument():
class EmptyChannel(InstrumentChannel):
pass


def test_channels_call_function(dci, caplog):
"""
Test that dci.channels.some_function() calls
Expand Down Expand Up @@ -301,11 +302,13 @@ def test_channel_list_get_validator_not_locked_raised(dci_with_list):
with pytest.raises(AttributeError, match="Cannot create a validator"):
dci_with_list.channels.get_validator()


def test_channel_tuple_index(dci):

for i, chan in enumerate(dci.channels):
assert dci.channels.index(chan) == i


def test_channel_tuple_snapshot(dci):
snapshot = dci.channels.snapshot()
assert snapshot["snapshotable"] is False
Expand All @@ -327,6 +330,7 @@ def test_channel_tuple_snapshot_enabled(empty_instrument):
assert len(snapshot.keys()) == 3
assert "channels" in snapshot.keys()


def test_channel_tuple_dir(dci):

dir_list = dir(dci.channels)
Expand All @@ -337,6 +341,7 @@ def test_channel_tuple_dir(dci):
for param in dci.channels[0].parameters.values():
assert param.short_name in dir_list


def test_clear_channels(dci_with_list):
channels = dci_with_list.channels
channels.clear()
Expand Down Expand Up @@ -377,6 +382,7 @@ def test_channel_list_lock_twice(dci_with_list):
# locking twice should be a no op
channels.lock()


def test_remove_tupled_channel(dci_with_list):
channel_tuple = tuple(
DummyChannel(dci_with_list, f"Chan{C}", C)
Expand Down Expand Up @@ -511,6 +517,7 @@ def test_set_element_locked_raises(dci_with_list):
dci_with_list.channels[0] = dci_with_list.channels[1]
assert dci_with_list.channels[0] is not dci_with_list.channels[1]


@settings(suppress_health_check=(HealthCheck.function_scoped_fixture,))
@given(myindexs=hst.lists(elements=hst.integers(0, 7), min_size=2))
def test_access_channels_by_name(dci, myindexs):
Expand All @@ -523,6 +530,7 @@ def test_access_channels_by_name(dci, myindexs):
for chan, chanindex in zip(mychans, myindexs):
assert chan.name == f'dci_Chan{names[chanindex]}'


def test_channels_contain(dci):
names = ("A", "B", "C", "D", "E", "F", "G", "H")
channels = tuple(DummyChannel(dci, "Chan" + name, name) for name in names)
Expand Down Expand Up @@ -809,6 +817,23 @@ def test_get_attr_on_empty_channellist_works_as_expected(empty_instrument):
_ = empty_instrument.channels.temperature


def test_channel_tuple_call_method_basic_test(dci):
result = dci.channels.turn_on()
assert result is None


def test_channel_tuple_call_method_called_as_expected(dci, mocker):

for channel in dci.channels:
channel.turn_on = mocker.MagicMock(return_value=1)

result = dci.channels.turn_on("bar")
# We never return the result (same for Function)
assert result is None
for channel in dci.channels:
channel.turn_on.assert_called_with("bar")


def _verify_multiparam_data(data):
assert 'multi_setpoint_param_this_setpoint_set' in data.arrays.keys()
assert_array_equal(
Expand Down

0 comments on commit 9d27254

Please sign in to comment.