TLDR:
What is the best way to build a class with generic property attributes and one can specify the getter and setter upon building those attributes?
Background:
I'm trying to make an object oriented representation of a SCPI command structure with Python. After parsing, the command layers are (sub-)objects and setting/getting the lowest command-object will actually communicate with a device to set/get its parameter.
The problem I encounter is the assignment of generic getter/setter functions to the command-object, acting like a property. I had a rough time with __setattr__
/ __getattr__
as I always ran into the recursive problem.
It would be cool if there was a distinction wether an attribute is set/get during class building and in its instantiated/runtime version. Or even better: is it possible to make an attribute a property "object"? Something like:
foo = property()
foo.__getter__ = lambda paramName : devcontroller.request(paramName)
foo.__setter__ = lambda paramName, paramValue : devcontroller.demand(paramName, paramValue)
setattr(self, paramName, foo)
but apparently this is not possible (read-only).
The best solution for now is the one below, but it feels very clunky. One cannot tab-complete the command-object for instance.
class SCPITree(object):
def __init__(self, cmd, branch):
self.__dict__['building'] = True
self.__dict__['params'] = {}
# parse tree
for k, v in branch.items():
catCmd = '{}:{}'.format(cmd, k[1])
param = k[0]
if isinstance(v, dict):
# still a dictionary, so unpack
#setattr(self, param, SCPITree(catCmd, v))
self.__dict__[param] = SCPITree(catCmd, v)
else:
# have the value type, so this is a leaf
print('{} of {}'.format(catCmd, v))
self.__dict__['params'][param] = {'cmd': catCmd, 'type': v}
#setattr(self, param, v())
del self.__dict__['building']
def __getattr__(self, name):
'''Requests the device for parameter'''
param = self.__dict__['params'].get(name)
if param:
request = param['cmd']+'?'
print('Requesting device with: {}'.format(request))
# do actual device communication here
return 'Response from device, converted to {}'.format(param['type'])
else:
if 'building' not in self.__dict__:
raise KeyError('Device has no SCPI command for {}'.format(name))
def __setattr__(self, name, value):
'''Sets a device parameter'''
param = self.__dict__['params'].get(name)
if param:
demand = param['cmd']+' {}'.format(value)
print('Demanding device with: {}'.format(demand))
# do actual device communication here
else:
if 'building' not in self.__dict__:
raise KeyError('Device has no SCPI command for {}'.format(name))
if __name__ == '__main__':
# test SCPI tree parsing
trunk = {
('input', 'INP'):{
('agc', 'AGC'):{
('mode', 'MOD'):str,
('refLevelDB', 'REF'):float},
('delayMS', 'DEL'):float,
('freqMHz', 'FREQ'):float}}
scpi = SCPITree('DCS', trunk)
The interaction is then something like this, after running the code:
DCS:INP:AGC:MOD of <type 'str'>
DCS:INP:AGC:REF of <type 'float'>
DCS:INP:DEL of <type 'float'>
DCS:INP:FREQ of <type 'float'>
In [1]: scpi.input.delayMS
Requesting device with: DCS:INP:DEL?
Out[1]: "Response from device, converted to <type 'float'>"
In [2]: scpi.input.delayMS = 3
Demanding device with: DCS:INP:DEL 3
In [3]: scpi.input.params
Out[3]:
{'delayMS': {'cmd': 'DCS:INP:DEL', 'type': float},
'freqMHz': {'cmd': 'DCS:INP:FREQ', 'type': float}}