# Creating hardware modules satisfying multiple interfaces This article will describe the qudi feature to overload interface attributes within hardware modules in case of namespace collisions between multiple inherited interface classes. ## Why do we even need this feature? Imagine you have an interface class `DataReaderInterface` which interfaces a simple data reader device. You also have another interface class `DataOutputInterface` which interfaces a simple data output device. Now let's say you have a piece of hardware that can handle both tasks simultaneously and you want to write a hardware module for that. First of all you would need to inherit both interfaces in the class definition of your hardware module: ```python from qudi.interface.data_reader import DataReaderInterface from qudi.interface.data_output import DataOutputInterface class MyHardwareModule(DataReaderInterface, DataOutputInterface): """ This will become my new fancy hardware module to combine DataReaderInterface and DataOutputInterface functionality """ pass ``` So far so good... Now let's have a closer look at the class members of `DataReaderInterface` and `DataOutputInterface`: ```python from abc import abstractmethod from qudi.core.module import Base class DataReaderInterface(Base): """ This is a fictional data reader interface. """ @property @abstractmethod def constraints(self): """ A read-only data structure containing all hardware parameter limitations. """ raise NotImplementedError @abstractmethod def get_data(self): """ Get the read data array. @return iterable: Data array """ raise NotImplementedError @abstractmethod def start(self): """ Start the data acquisition. """ raise NotImplementedError class DataOutputInterface(Base): """ This is a fictional data output interface. """ @property @abstractmethod def constraints(self): """ A read-only data structure containing all hardware parameter limitations. """ raise NotImplementedError @abstractmethod def set_data(self, data): """ Set the data array to output. @param iterable data: The data array to output """ raise NotImplementedError @abstractmethod def start(self): """ Start the data output. """ raise NotImplementedError ``` As you can see it will be no problem to just implement the `get_data` and `set_data` methods in our new hardware module since they have unique names that are just present in a single interface, respectively. The method `start` and the property `constraints` however are an entirely different story. In a traditional way you can only define one attribute with that name in your hardware module, thus preventing to start each task separately and getting the constraints selectively. And this is where the ominous meta object `qudi.util.overload.OverloadedAttribute` comes into play. ## How to overload an interface attribute The meta object `OverloadedAttribute` enables you to flag any attribute (descriptor object, variable, method/function etc.) as an overloaded attribute giving the possibility to register multiple implementations for the attribute under unique `str` keys. Let's look at an example on how this can be used in a hardware module based on the example classes presented in the previous section: ```python from qudi.interface.data_reader import DataReaderInterface from qudi.interface.data_output import DataOutputInterface from qudi.util.overload import OverloadedAttribute class MyHardwareModule(DataReaderInterface, DataOutputInterface): """ This will become my new fancy hardware module to combine DataReaderInterface and DataOutputInterface functionality. """ # Define qudi module activation/deactivation ... # Do other stuff ... def get_data(self): # Do something return tuple(range(42)) def set_data(self, data): # Do something pass # Flag "start" attribute as overloaded attribute start = OverloadedAttribute() # Register multiple implementations for "start" via convenient decorator # The key words under which the implementations are registered must be the corresponding # interface class names. # Make sure to use "start" as attribute name for all implementations. @start.overload('DataReaderInterface') def start(self): # Start the data reader print('Data reader started through "DataReaderInterface" interface method') @start.overload('DataOutputInterface') def start(self): # Start the data output print('Data output started through "DataOutputInterface" interface method') # You can do the same for properties. Just make sure to apply the @property decorator first. constraints = OverloadedAttribute() @constraints.overload('DataReaderInterface') @property def constraints(self): # Return data reader constraints print('Data reader constraints requested through "DataReaderInterface" interface.') return dict() @constraints.overload('DataOutputInterface') @property def constraints(self): # Return data output constraints print('Data output constraints requested through "DataOutputInterface" interface.') return dict() ``` As already mentioned, `set_data` and `get_data` do not need special treatment. Through the `OverloadedAttribute` object and decorators used here, the two implementations for `start` will both be registered to the attribute `start`; each one associated to a different interface class name ('DataReaderInterface' or 'DataOutputInterface'). Same goes for the property `constraints`. The string given in the `overload` decorator is used as a keyword to address which implementation to use: ```python # Call different implementations for "start" .start['DataReaderInterface']() .start['DataOutputInterface']() # Get different implementations for "constraints" property .constraints['DataReaderInterface'] .constraints['DataOutputInterface'] ``` So when accessing overloaded attributes of a hardware class directly, you can select which implementation to address by adding the respective overload keyword in square brackets after the method name just as you would for any mapping in Python. Using keywords that have not been registered will result in `KeyError`. You can however always overwrite the interface attribute in your hardware class as usual by just defining it regularly (without the decorator and the meta object) if you do not need to overload it. ## Interface methods and logic module Connector Now you might think this new way of addressing overloaded attributes will not work seamlessly with logic modules due to the changed attribute access syntax. In order to work around this issue the `qudi.core.connector.Connector` object is your best friend. During instantiation of a `Connector` object the logic module passes the interface type or class name as parameter. As such the `Connector` instance can provide a hardware module proxy object when called to hide the overload mechanics of interface methods from the calling logic module. This is enabled by `qudi.util.overload.OverloadProxy`. To illustrate this further, let's assume you have a logic module `MyLogicModule` which is interfacing `DataReaderInterface` and `DataOutputInterface` through our new hardware module `MyHardwareModule`. A call to each different `start` implementation would look like: ```python from qudi.core.connector import Connector from qudi.core.module import LogicBase class MyLogicModule(LogicBase): """ Fictional logic module illustrating the use of the Connector object with overloaded interface methods. """ # Instantiate connectors _data_reader = Connector(name='data_reader', interface='DataReaderInterface') _data_output = Connector(name='data_output', interface='DataOutputInterface') # Declare other class-level stuff ... # define qudi module on_activate/on_deactivate ... # example method with calls to hardware module(s) def do_stuff(self): self._data_reader().start() # Will call "start" implementation for "DataReaderInterface" self._data_output().start() # Will call "start" implementation for "DataOutputInterface" ``` As you can see through the use of the `Connector` object, the logic does not need to know if two separate devices are connected or a single device with overloaded interface methods. ## Generalization The qudi meta object `OverloadedAttribute` as well as `OverloadProxy` can in fact be used in a very general way and not only with qudi hardware interfaces. It can overload any attribute type (descriptor objects, callables, staticmethods, classmethods, etc.) in the class body with any non-empty `str` keywords. You do not need qudi module base/meta classes for the overload mechanism to work, which allows you to use it with any Python3 class. The same is true for hiding the overloading semantics using `OverloadProxy`. ---