From b084fe495eae0caa28ec84574cbc35a900b39606 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Tue, 11 Jul 2017 17:05:01 +0200 Subject: [PATCH] Feat/channelization further work (#641) * fix: refactor Instrument to avoid removing methods from channels * Include channels in print_readable_snaphoot * Fix: make locked channels a named tuple That way you can access the names on the channellist via names as soon as they are locked * Fix: channels add support for indexing from 1 many instruments have hardware channels that are indexed and start at 1 * Fix linting errors in channel * fix typo * Move print readable snapshot to instrument class where it belongs * Fix: more channel annotation * Improve error message * pprint limit line lenght of header to max char * pep8 line lenght * Fix: channel remove support for oneindexed channels * improve type annotation * Optional -> Union * add channels to api docs * fix: make submodules nested * add submodule attributes to base * Replace namedtuple by dict Make it possible to get channels by name even when not locked * Add submodule to docstring --- docs/api/public.rst | 2 + qcodes/instrument/base.py | 524 +++++++++++++++++++---------------- qcodes/instrument/channel.py | 251 +++++++++-------- 3 files changed, 422 insertions(+), 355 deletions(-) diff --git a/docs/api/public.rst b/docs/api/public.rst index 5794f4e8df1d..03112e261b00 100644 --- a/docs/api/public.rst +++ b/docs/api/public.rst @@ -101,6 +101,8 @@ Instrument IPInstrument VisaInstrument + InstrumentChannel + ChannelList Plot ~~~~ diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 24f1efcaa523..681f835f7e90 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -12,7 +12,285 @@ from .function import Function -class Instrument(Metadatable, DelegateAttributes): +class InstrumentBase(Metadatable, DelegateAttributes): + """ + Base class for all QCodes instruments and instrument channels + + Args: + name (str): an identifier for this instrument, particularly for + attaching it to a Station. + + metadata (Optional[Dict]): additional static metadata to add to this + instrument's JSON snapshot. + + + Attributes: + name (str): an identifier for this instrument, particularly for + attaching it to a Station. + + parameters (Dict[Parameter]): All the parameters supported by this + instrument. Usually populated via ``add_parameter`` + + functions (Dict[Function]): All the functions supported by this + instrument. Usually populated via ``add_function`` + + submodules (Dict[Metadatable]): All the submodules of this instrument + such as channel lists or logical groupings of parameters. + Usually populated via ``add_submodule`` + """ + def __init__(self, name, **kwargs): + self.name = str(name) + self.parameters = {} + self.functions = {} + self.submodules = {} + super().__init__(**kwargs) + + def add_parameter(self, name, parameter_class=StandardParameter, + **kwargs): + """ + Bind one Parameter to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + for every real parameter of the instrument. + + In this sense, parameters are the state variables of the instrument, + anything the user can set and/or get + + Args: + name (str): How the parameter will be stored within + ``instrument.parameters`` and also how you address it using the + shortcut methods: ``instrument.set(param_name, value)`` etc. + + parameter_class (Optional[type]): You can construct the parameter + out of any class. Default ``StandardParameter``. + + **kwargs: constructor arguments for ``parameter_class``. + + Raises: + KeyError: if this instrument already has a parameter with this + name. + """ + if name in self.parameters: + raise KeyError('Duplicate parameter name {}'.format(name)) + param = parameter_class(name=name, instrument=self, **kwargs) + self.parameters[name] = param + + def add_function(self, name, **kwargs): + """ + Bind one Function to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + for every real function of the instrument. + + 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. + + Args: + name (str): how the Function will be stored within + ``instrument.Functions`` and also how you address it using the + shortcut methods: ``instrument.call(func_name, *args)`` etc. + + **kwargs: constructor kwargs for ``Function`` + + Raises: + KeyError: if this instrument already has a function with this + name. + """ + if name in self.functions: + raise KeyError('Duplicate function name {}'.format(name)) + func = Function(name=name, instrument=self, **kwargs) + self.functions[name] = func + + def add_submodule(self, name, submodule): + """ + Bind one submodule to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + method for every submodule of the instrument. + + Submodules can effectively be considered as instruments within the main + instrument, and should at minimum be snapshottable. For example, they can + be used to either store logical groupings of parameters, which may or may + not be repeated, or channel lists. + + Args: + name (str): how the submodule will be stored within ``instrument.submodules`` + and also how it can be addressed. + + submodule (Metadatable): The submodule to be stored. + + Raises: + KeyError: if this instrument already contains a submodule with this + name. + TypeError: if the submodule that we are trying to add is not an instance + of an Metadatable object. + """ + if name in self.submodules: + raise KeyError('Duplicate submodule name {}'.format(name)) + if not isinstance(submodule, Metadatable): + raise TypeError('Submodules must be metadatable.') + self.submodules[name] = submodule + + def snapshot_base(self, update=False): + """ + State of the instrument as a JSON-compatible dict. + + Args: + update (bool): If True, update the state by querying the + instrument. If False, just use the latest values in memory. + + Returns: + dict: base snapshot + """ + snap = {'parameters': dict((name, param.snapshot(update=update)) + for name, param in self.parameters.items()), + 'functions': dict((name, func.snapshot(update=update)) + for name, func in self.functions.items()), + 'submodules': dict((name, subm.snapshot(update=update)) + for name, subm in self.submodules.items()), + '__class__': full_class(self), + } + for attr in set(self._meta_attrs): + if hasattr(self, attr): + snap[attr] = getattr(self, attr) + return snap + + def print_readable_snapshot(self, update=False, max_chars=80): + """ + Prints a readable version of the snapshot. + The readable snapshot includes the name, value and unit of each + parameter. + A convenience function to quickly get an overview of the status of an instrument. + + Args: + update (bool) : If True, update the state by querying the + instrument. If False, just use the latest values in memory. + This argument gets passed to the snapshot function. + max_chars (int) : the maximum number of characters per line. The + readable snapshot will be cropped if this value is exceeded. + Defaults to 80 to be consistent with default terminal width. + """ + floating_types = (float, np.integer, np.floating) + snapshot = self.snapshot(update=update) + + par_lengths = [len(p) for p in snapshot['parameters']] + + # Min of 50 is to prevent a super long parameter name to break this + # function + par_field_len = min(max(par_lengths)+1, 50) + + print(self.name + ':') + print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') + print('-'*max_chars) + for par in sorted(snapshot['parameters']): + name = snapshot['parameters'][par]['name'] + msg = '{0:<{1}}:'.format(name, par_field_len) + val = snapshot['parameters'][par]['value'] + unit = snapshot['parameters'][par].get('unit', None) + if unit is None: + # this may be a multi parameter + unit = snapshot['parameters'][par].get('units', None) + if isinstance(val, floating_types): + msg += '\t{:.5g} '.format(val) + else: + msg += '\t{} '.format(val) + if unit is not '': # corresponds to no unit + msg += '({})'.format(unit) + # Truncate the message if it is longer than max length + if len(msg) > max_chars and not max_chars == -1: + msg = msg[0:max_chars-3] + '...' + print(msg) + + for submodule in self.submodules.values(): + if hasattr(submodule, '_channels'): + if submodule._snapshotable: + for channel in submodule._channels: + channel.print_readable_snapshot() + else: + submodule.print_readable_snapshot(update, max_chars) + + # + # shortcuts to parameters & setters & getters # + # + # instrument['someparam'] === instrument.parameters['someparam'] # + # instrument.someparam === instrument.parameters['someparam'] # + # instrument.get('someparam') === instrument['someparam'].get() # + # etc... # + # + delegate_attr_dicts = ['parameters', 'functions', 'submodules'] + + def __getitem__(self, key): + """Delegate instrument['name'] to parameter or function 'name'.""" + try: + return self.parameters[key] + except KeyError: + return self.functions[key] + + def set(self, param_name, value): + """ + Shortcut for setting a parameter from its name and new value. + + Args: + param_name (str): The name of a parameter of this instrument. + value (any): The new value to set. + """ + self.parameters[param_name].set(value) + + def get(self, param_name): + """ + Shortcut for getting a parameter from its name. + + Args: + param_name (str): The name of a parameter of this instrument. + + Returns: + any: The current value of the parameter. + """ + return self.parameters[param_name].get() + + def call(self, func_name, *args): + """ + Shortcut for calling a function from its name. + + Args: + func_name (str): The name of a function of this instrument. + *args: any arguments to the function. + + Returns: + any: The return value of the function. + """ + return self.functions[func_name].call(*args) + + def __getstate__(self): + """Prevent pickling instruments, and give a nice error message.""" + raise RuntimeError( + 'Pickling %s. qcodes Instruments should not be pickled. Likely this means you ' + 'were trying to use a local instrument (defined with ' + 'server_name=None) in a background Loop. Local instruments can ' + 'only be used in Loops with background=False.' % self.name) + + def validate_status(self, verbose=False): + """ Validate the values of all gettable parameters + + The validation is done for all parameters that have both a get and + set method. + + Arguments: + verbose (bool): If True, then information about the parameters that are being check is printed. + + """ + for k, p in self.parameters.items(): + if p.has_get and p.has_set: + value = p.get() + if verbose: + print('validate_status: param %s: %s' % (k, value)) + p.validate(value) + + +class Instrument(InstrumentBase): """ Base class for all QCodes instruments. @@ -49,12 +327,7 @@ def __init__(self, name, **kwargs): if kwargs.pop('server_name', False): warnings.warn("server_name argument not supported any more", stacklevel=0) - super().__init__(**kwargs) - self.parameters = {} - self.functions = {} - self.submodules = {} - - self.name = str(name) + super().__init__(name, **kwargs) self.add_parameter('IDN', get_cmd=self.get_idn, vals=Anything()) @@ -252,165 +525,6 @@ def find_instrument(cls, name, instrument_class=None): return ins - def add_parameter(self, name, parameter_class=StandardParameter, - **kwargs): - """ - Bind one Parameter to this instrument. - - Instrument subclasses can call this repeatedly in their ``__init__`` - for every real parameter of the instrument. - - In this sense, parameters are the state variables of the instrument, - anything the user can set and/or get - - Args: - name (str): How the parameter will be stored within - ``instrument.parameters`` and also how you address it using the - shortcut methods: ``instrument.set(param_name, value)`` etc. - - parameter_class (Optional[type]): You can construct the parameter - out of any class. Default ``StandardParameter``. - - **kwargs: constructor arguments for ``parameter_class``. - - Raises: - KeyError: if this instrument already has a parameter with this - name. - """ - if name in self.parameters: - raise KeyError('Duplicate parameter name {}'.format(name)) - param = parameter_class(name=name, instrument=self, **kwargs) - self.parameters[name] = param - - def add_function(self, name, **kwargs): - """ - Bind one Function to this instrument. - - Instrument subclasses can call this repeatedly in their ``__init__`` - for every real function of the instrument. - - 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. - - Args: - name (str): how the Function will be stored within - ``instrument.Functions`` and also how you address it using the - shortcut methods: ``instrument.call(func_name, *args)`` etc. - - **kwargs: constructor kwargs for ``Function`` - - Raises: - KeyError: if this instrument already has a function with this - name. - """ - if name in self.functions: - raise KeyError('Duplicate function name {}'.format(name)) - func = Function(name=name, instrument=self, **kwargs) - self.functions[name] = func - - def add_submodule(self, name, submodule): - """ - Bind one submodule to this instrument. - - Instrument subclasses can call this repeatedly in their ``__init__`` - method for every submodule of the instrument. - - Submodules can effectively be considered as instruments within the main - instrument, and should at minimum be snapshottable. For example, they can - be used to either store logical groupings of parameters, which may or may - not be repeated, or channel lists. - - Args: - name (str): how the submodule will be stored within ``instrument.submodules`` - and also how it can be addressed. - - submodule (Metadatable): The submodule to be stored. - - Raises: - KeyError: if this instrument already contains a submodule with this - name. - TypeError: if the submodule that we are trying to add is not an instance - of an Metadatable object. - """ - if name in self.submodules: - raise KeyError('Duplicate submodule name {}'.format(name)) - if not isinstance(submodule, Metadatable): - raise TypeError('Submodules must be metadatable.') - self.submodules[name] = submodule - - def snapshot_base(self, update=False): - """ - State of the instrument as a JSON-compatible dict. - - Args: - update (bool): If True, update the state by querying the - instrument. If False, just use the latest values in memory. - - Returns: - dict: base snapshot - """ - snap = {'parameters': dict((name, param.snapshot(update=update)) - for name, param in self.parameters.items()), - 'functions': dict((name, func.snapshot(update=update)) - for name, func in self.functions.items()), - 'submodules': dict((name, subm.snapshot(update=update)) - for name, subm in self.submodules.items()), - '__class__': full_class(self), - } - for attr in set(self._meta_attrs): - if hasattr(self, attr): - snap[attr] = getattr(self, attr) - return snap - - def print_readable_snapshot(self, update=False, max_chars=80): - """ - Prints a readable version of the snapshot. - The readable snapshot includes the name, value and unit of each - parameter. - A convenience function to quickly get an overview of the status of an instrument. - - Args: - update (bool) : If True, update the state by querying the - instrument. If False, just use the latest values in memory. - This argument gets passed to the snapshot function. - max_chars (int) : the maximum number of characters per line. The - readable snapshot will be cropped if this value is exceeded. - Defaults to 80 to be consistent with default terminal width. - """ - floating_types = (float, np.integer, np.floating) - snapshot = self.snapshot(update=update) - - par_lengths = [len(p) for p in snapshot['parameters']] - - # Min of 50 is to prevent a super long parameter name to break this - # function - par_field_len = min(max(par_lengths)+1, 50) - - print(self.name + ':') - print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') - print('-'*80) - for par in sorted(snapshot['parameters']): - name = snapshot['parameters'][par]['name'] - msg = '{0:<{1}}:'.format(name, par_field_len) - val = snapshot['parameters'][par]['value'] - unit = snapshot['parameters'][par].get('unit', None) - if unit is None: - # this may be a multi parameter - unit = snapshot['parameters'][par].get('units', None) - if isinstance(val, floating_types): - msg += '\t{:.5g} '.format(val) - else: - msg += '\t{} '.format(val) - if unit is not '': # corresponds to no unit - msg += '({})'.format(unit) - # Truncate the message if it is longer than max length - if len(msg) > max_chars and not max_chars == -1: - msg = msg[0:max_chars-3] + '...' - print(msg) - # `write_raw` and `ask_raw` are the interface to hardware # # `write` and `ask` are standard wrappers to help with error reporting # # @@ -489,81 +603,3 @@ def ask_raw(self, cmd): raise NotImplementedError( 'Instrument {} has not defined an ask method'.format( type(self).__name__)) - - # - # shortcuts to parameters & setters & getters # - # - # instrument['someparam'] === instrument.parameters['someparam'] # - # instrument.someparam === instrument.parameters['someparam'] # - # instrument.get('someparam') === instrument['someparam'].get() # - # etc... # - # - - delegate_attr_dicts = ['parameters', 'functions', 'submodules'] - - def __getitem__(self, key): - """Delegate instrument['name'] to parameter or function 'name'.""" - try: - return self.parameters[key] - except KeyError: - return self.functions[key] - - def set(self, param_name, value): - """ - Shortcut for setting a parameter from its name and new value. - - Args: - param_name (str): The name of a parameter of this instrument. - value (any): The new value to set. - """ - self.parameters[param_name].set(value) - - def get(self, param_name): - """ - Shortcut for getting a parameter from its name. - - Args: - param_name (str): The name of a parameter of this instrument. - - Returns: - any: The current value of the parameter. - """ - return self.parameters[param_name].get() - - def call(self, func_name, *args): - """ - Shortcut for calling a function from its name. - - Args: - func_name (str): The name of a function of this instrument. - *args: any arguments to the function. - - Returns: - any: The return value of the function. - """ - return self.functions[func_name].call(*args) - - def __getstate__(self): - """Prevent pickling instruments, and give a nice error message.""" - raise RuntimeError( - 'Pickling %s. qcodes Instruments should not be pickled. Likely this means you ' - 'were trying to use a local instrument (defined with ' - 'server_name=None) in a background Loop. Local instruments can ' - 'only be used in Loops with background=False.' % self.name) - - def validate_status(self, verbose=False): - """ Validate the values of all gettable parameters - - The validation is done for all parameters that have both a get and - set method. - - Arguments: - verbose (bool): If True, then information about the parameters that are being check is printed. - - """ - for k, p in self.parameters.items(): - if p.has_get and p.has_set: - value = p.get() - if verbose: - print('validate_status: param %s: %s' % (k, value)) - p.validate(value) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index f5c441101247..9868effa5af0 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -1,40 +1,39 @@ """ Base class for the channel of an instrument """ from typing import List, Tuple, Union -from .base import Instrument +from .base import InstrumentBase, Instrument from .parameter import MultiParameter, ArrayParameter from ..utils.metadata import Metadatable from ..utils.helpers import full_class -class InstrumentChannel(Instrument): +class InstrumentChannel(InstrumentBase): """ Base class for a channel in an instrument Args: - parent (Instrument): the instrument to which this channel should be attached + parent (Instrument): the instrument to which this channel should be + attached name (str): the name of this channel Attributes: name (str): the name of this channel - parameters (Dict[Parameter]): All the parameters supported by this channel. - Usually populated via ``add_parameter`` + parameters (Dict[Parameter]): All the parameters supported by this + channel. Usually populated via ``add_parameter`` - functions (Dict[Function]): All the functions supported by this channel. - Usually populated via ``add_function`` + functions (Dict[Function]): All the functions supported by this + channel. Usually populated via ``add_function`` """ - def __init__(self, parent, name, **kwargs): - # Initialize base classes of Instrument. We will overwrite what we want to do - # in the Instrument initializer + def __init__(self, parent: Instrument, name: str, **kwargs): + # Initialize base classes of Instrument. We will overwrite what we + # want to do in the Instrument initializer super().__init__(name=name, **kwargs) - self.parameters = {} - self.functions = {} - self.name = "{}_{}".format(parent.name, str(name)) + self.short_name = str(name) self._meta_attrs = ['name'] self._parent = parent @@ -46,35 +45,6 @@ def __repr__(self): type(self._parent).__name__, self._parent.name) - # We aren't a member of the global list of instruments, don't try and remove ourself - def __del__(self): - """ Does nothing for an instrument channel """ - pass - - def close(self): - """ Doesn't make sense to just close a channel by default, raise NotImplemented """ - raise NotImplementedError("Can't close a channel. Close my parent instead.") - - @classmethod - def record_instance(cls, instance): - """ Instances should not be recorded for channels. This should happen for the parent instrument. """ - pass - - @classmethod - def instances(cls): - """ Instances should not be recorded for channels. This should happen for the parent instrument. """ - pass - - @classmethod - def remove_instances(cls, instance): - """ It doesn't make sense to remove a channel from an instrument, raise NotImplemented""" - raise NotImplementedError("Can't remove a channel.") - - # This method doesn't make sense for a channel, raise NotImplemented - @classmethod - def find_instruments(cls, name, instrument_class=None): - raise NotImplementedError("Can't find instruments in a channel") - # Pass any commands to read or write from the instrument up to the parent def write(self, cmd): return self._parent.write(cmd) @@ -93,23 +63,30 @@ class MultiChannelInstrumentParameter(MultiParameter): """ Parameter to get or set multiple channels simultaneously. - Will normally be created by a ChannelList and not directly by anything else. + Will normally be created by a ChannelList and not directly by anything + else. Args: - channels(list[chan_type]): A list of channels which we can operate on simultaneously. + channels(list[chan_type]): A list of channels which we can operate on + simultaneously. param_name(str): Name of the multichannel parameter """ - def __init__(self, channels: Union[List, Tuple], param_name, *args, **kwargs): + def __init__(self, + channels: Union[List, Tuple], + param_name: str, + *args, **kwargs): super().__init__(*args, **kwargs) self._channels = channels self._param_name = param_name - def get(self): + def get(self) -> tuple: """ - Return a tuple containing the data from each of the channels in the list + Return a tuple containing the data from each of the channels in the + list """ - return tuple(chan.parameters[self._param_name].get() for chan in self._channels) + return tuple(chan.parameters[self._param_name].get() for chan + in self._channels) def set(self, value): """ @@ -124,9 +101,9 @@ def set(self, value): @property def full_names(self): - """Overwrite full_names because the instument name is already included in the name. - This happens because the instument name is included in the channel name merged into the - parameter name above. + """Overwrite full_names because the instrument name is already included + in the name. This happens because the instrument name is included in + the channel name merged into the parameter name above. """ return self.names @@ -155,9 +132,9 @@ class ChannelList(Metadatable): stored inside a channel list are accessible in multiple ways and should not be repeated in an instrument snapshot. - multichan_paramclass (MultiChannelInstrumentParameter): The class of the object - to be returned by the ChanneList's __getattr__ method. Should be a - subclass of MultiChannelInstrumentParameter. + multichan_paramclass (MultiChannelInstrumentParameter): The class of + the object to be returned by the ChanneList's __getattr__ method. + Should be a subclass of MultiChannelInstrumentParameter. Raises: ValueError: If chan_type is not a subclass of InstrumentChannel @@ -167,9 +144,12 @@ class ChannelList(Metadatable): """ - def __init__(self, parent, name, chan_type: type, chan_list=None, - snapshotable=True, - multichan_paramclass: type=MultiChannelInstrumentParameter): + def __init__(self, parent: Instrument, + name: str, + chan_type: type, + chan_list: Union[List, Tuple, None]=None, + snapshotable: bool=True, + multichan_paramclass: type = MultiChannelInstrumentParameter): super().__init__() self._parent = parent @@ -179,7 +159,8 @@ def __init__(self, parent, name, chan_type: type, chan_list=None, raise ValueError("Channel Lists can only hold instances of type" " InstrumentChannel") if (not isinstance(multichan_paramclass, type) or - not issubclass(multichan_paramclass, MultiChannelInstrumentParameter)): + not issubclass(multichan_paramclass, + MultiChannelInstrumentParameter)): raise ValueError("multichan_paramclass must be a (subclass of) " "MultiChannelInstrumentParameter") @@ -187,23 +168,29 @@ def __init__(self, parent, name, chan_type: type, chan_list=None, self._snapshotable = snapshotable self._paramclass = multichan_paramclass - # If a list of channels is not provided, define a list to store channels. - # This will eventually become a locked tuple. + self._channel_mapping = {} # provide lookup of channels by name + # If a list of channels is not provided, define a list to store + # channels. This will eventually become a locked tuple. if chan_list is None: self._locked = False self._channels = [] else: self._locked = True self._channels = tuple(chan_list) + self._channel_mapping = {channel.short_name: channel + for channel in self._channels} if not all(isinstance(chan, chan_type) for chan in self._channels): - raise TypeError("All items in this channel list must be of type {}.".format(chan_type.__name__)) + raise TypeError("All items in this channel list must be of " + "type {}.".format(chan_type.__name__)) - def __getitem__(self, i): + def __getitem__(self, i: Union[int, slice]): """ - Return either a single channel, or a new ChannelList containing only the specified channels + Return either a single channel, or a new ChannelList containing only + the specified channels Args: - i (int/slice): Either a single channel index or a slice of channels to get + i (int/slice): Either a single channel index or a slice of channels + to get """ if isinstance(i, slice): return ChannelList(self._parent, self._name, self._chan_type, @@ -218,32 +205,42 @@ def __len__(self): return len(self._channels) def __repr__(self): - return "ChannelList({!r}, {}, {!r})".format(self._parent, self._chan_type.__name__, self._channels) + return "ChannelList({!r}, {}, {!r})".format(self._parent, + self._chan_type.__name__, + self._channels) - def __add__(self, other): + def __add__(self, other: 'ChannelList'): """ - Return a new channel list containing the channels from both ChannelList self and r. + Return a new channel list containing the channels from both + ChannelList self and r. Both channel lists must hold the same type and have the same parent. Args: other(ChannelList): Right argument to add. """ - if not isinstance(self, ChannelList) or not isinstance(other, ChannelList): - raise TypeError("Can't add objects of type {} and {} together".format( - type(self).__name__, type(other).__name__)) + if not isinstance(self, ChannelList) or not isinstance(other, + ChannelList): + raise TypeError("Can't add objects of type" + " {} and {} together".format(type(self).__name__, + type(other).__name__)) if self._chan_type != other._chan_type: - raise TypeError("Both l and r arguments to add must contain channels of the same type." - " Adding channels of type {} and {}.".format(self._chan_type.__name__, - other._chan_type.__name__)) + raise TypeError("Both l and r arguments to add must contain " + "channels of the same type." + " Adding channels of type " + "{} and {}.".format(self._chan_type.__name__, + other._chan_type.__name__)) if self._parent != other._parent: - raise ValueError("Can only add channels from the same parent together.") + raise ValueError("Can only add channels from the same parent " + "together.") - return ChannelList(self._parent, self._name, self._chan_type, self._channels + other._channels) + return ChannelList(self._parent, self._name, self._chan_type, + self._channels + other._channels) - def append(self, obj): + def append(self, obj: InstrumentChannel): """ - When initially constructing the channel list, a new channel to add to the end of the list + When initially constructing the channel list, a new channel to add to + the end of the list Args: obj(chan_type): New channel to add to the list. @@ -251,9 +248,11 @@ def append(self, obj): if self._locked: raise AttributeError("Cannot append to a locked channel list") if not isinstance(obj, self._chan_type): - raise TypeError("All items in a channel list must be of the same type." - " Adding {} to a list of {}.".format(type(obj).__name__, - self._chan_type.__name__)) + raise TypeError("All items in a channel list must be of the same " + "type. Adding {} to a list of {}" + ".".format(type(obj).__name__, + self._chan_type.__name__)) + self._channel_mapping[obj.short_name] = obj return self._channels.append(obj) def extend(self, objects): @@ -261,18 +260,20 @@ def extend(self, objects): Insert an iterable of objects into the list of channels. Args: - objects(Iterable[chan_type]): A list of objects to add into the ChannelList. + objects(Iterable[chan_type]): A list of objects to add into the + ChannelList. """ - # objects may be a generator but we need to iterate over it twice below so - # copy it into a tuple just in case. + # objects may be a generator but we need to iterate over it twice + # below so copy it into a tuple just in case. objects = tuple(objects) if self._locked: raise AttributeError("Cannot extend a locked channel list") if not all(isinstance(obj, self._chan_type) for obj in objects): - raise TypeError("All items in a channel list must be of the same type.") + raise TypeError("All items in a channel list must be of the same " + "type.") return self._channels.extend(objects) - def index(self, obj): + def index(self, obj: InstrumentChannel): """ Return the index of the given object @@ -281,7 +282,7 @@ def index(self, obj): """ return self._channels.index(obj) - def insert(self, index, obj): + def insert(self, index: int, obj: InstrumentChannel): """ Insert an object into the channel list at a specific index. @@ -293,22 +294,25 @@ def insert(self, index, obj): if self._locked: raise AttributeError("Cannot insert into a locked channel list") if not isinstance(obj, self._chan_type): - raise TypeError("All items in a channel list must be of the same type." - " Adding {} to a list of {}.".format(type(obj).__name__, - self._chan_type.__name__)) + raise TypeError("All items in a channel list must be of the same " + "type. Adding {} to a list of {}" + ".".format(type(obj).__name__, + self._chan_type.__name__)) + return self._channels.insert(index, obj) def lock(self): """ - Lock the channel list. Once this is done, the channel list is converted to a tuple - and any future changes to the list are prevented. + Lock the channel list. Once this is done, the channel list is + converted to a tuple and any future changes to the list are prevented. """ if self._locked: return + self._channels = tuple(self._channels) self._locked = True - def snapshot_base(self, update=False): + def snapshot_base(self, update: bool=False): """ State of the instrument as a JSON-compatible dict. @@ -331,13 +335,14 @@ def snapshot_base(self, update=False): } return snap - def __getattr__(self, name): + def __getattr__(self, name: str): """ - Return a multi-channel function or parameter that we can use to get or set all items - in a channel list simultaneously. + Return a multi-channel function or parameter that we can use to get or + set all items in a channel list simultaneously. Params: - name(str): The name of the parameter or function that we want to operate on. + name(str): The name of the parameter or function that we want to + operate on. """ # Check if this is a valid parameter if name in self._channels[0].parameters: @@ -346,27 +351,37 @@ def __getattr__(self, name): setpoint_labels = None setpoint_units = None # We need to construct a MultiParameter object to get each of the - # values our of each parameter in our list, we don't currently try to - # construct a multiparameter from a list of multi parameters + # values our of each parameter in our list, we don't currently try + # to construct a multiparameter from a list of multi parameters if isinstance(self._channels[0].parameters[name], MultiParameter): - raise NotImplementedError("Slicing is currently not supported for MultiParameters") - names = tuple("{}_{}".format(chan.name, name) for chan in self._channels) - labels = tuple(chan.parameters[name].label for chan in self._channels) - units = tuple(chan.parameters[name].unit for chan in self._channels) + raise NotImplementedError("Slicing is currently not " + "supported for MultiParameters") + names = tuple("{}_{}".format(chan.name, name) + for chan in self._channels) + labels = tuple(chan.parameters[name].label + for chan in self._channels) + units = tuple(chan.parameters[name].unit + for chan in self._channels) if isinstance(self._channels[0].parameters[name], ArrayParameter): - shapes = tuple(chan.parameters[name].shape for chan in self._channels) + shapes = tuple(chan.parameters[name].shape for + chan in self._channels) if self._channels[0].parameters[name].setpoints: - setpoints = tuple(chan.parameters[name].setpoints for chan in self._channels) + setpoints = tuple(chan.parameters[name].setpoints for + chan in self._channels) if self._channels[0].parameters[name].setpoint_names: - setpoint_names = tuple(chan.parameters[name].setpoint_names for chan in self._channels) + setpoint_names = tuple(chan.parameters[name].setpoint_names + for chan in self._channels) if self._channels[0].parameters[name].setpoint_labels: - setpoint_labels = tuple(chan.parameters[name].setpoint_labels for chan in self._channels) + setpoint_labels = tuple( + chan.parameters[name].setpoint_labels + for chan in self._channels) if self._channels[0].parameters[name].setpoint_units: - setpoint_units = tuple(chan.parameters[name].setpoint_units for chan in self._channels) + setpoint_units = tuple(chan.parameters[name].setpoint_units + for chan in self._channels) else: - shapes = tuple(() for chan in self._channels) + shapes = tuple(() for _ in self._channels) param = self._paramclass(self._channels, param_name=name, @@ -384,11 +399,25 @@ def __getattr__(self, name): # Check if this is a valid function if name in self._channels[0].functions: - # We want to return a reference to a function that would call the function - # for each of the channels in turn. + # We want to return a reference to a function that would call the + # function for each of the channels in turn. def multi_func(*args, **kwargs): for chan in self._channels: chan.functions[name](*args, **kwargs) return multi_func - raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, name)) + try: + return self._channel_mapping[name] + except KeyError: + pass + + raise AttributeError('\'{}\' object has no attribute \'{}\'' + ''.format(self.__class__.__name__, name)) + + def __dir__(self) -> list: + names = super().__dir__() + if self._channels: + names += list(self._channels[0].parameters.keys()) + names += list(self._channels[0].functions.keys()) + names += [channel.short_name for channel in self._channels] + return sorted(set(names))