removed old style syntax

- removed secop/metaclass.py
- moved code from ModuleMeta to modules.HasAccessibles.__init_subclass__
- reworked properties:
  assignment obj.property = value now always allowed
- reworked Parameters and Command to be true descriptors
- Command must now be solely used as decorator
- renamed 'usercommand' to 'Command'
- command methods no longer start with 'do_'
- reworked mechanism to determine accessible order:
  the attribute paramOrder, if given, determines order of accessibles
+ fixed some issues makeing the IDE more happy
+ simplified code for StatusType and added a test for it

Change-Id: I8045cf38ee6f4d4862428272df0b12a7c8abaca7
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25049
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2021-02-12 18:37:04 +01:00
parent ed02131a37
commit 1a8ddbc696
34 changed files with 1678 additions and 1978 deletions

View File

@ -25,7 +25,7 @@
# no fixtures needed
import pytest
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
from secop.datatypes import ArrayOf, BLOBType, BoolType, Enum, StatusType, \
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
@ -359,6 +359,7 @@ def test_BoolType():
# pylint: disable=unexpected-keyword-arg
BoolType(unit='K')
def test_ArrayOf():
# test constructor catching illegal arguments
with pytest.raises(ValueError):
@ -478,6 +479,14 @@ def test_Command():
'result':{'type': 'int', 'min':-3, 'max':3}}
def test_StatusType():
status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400)
dt = StatusType(status_codes)
assert dt.IDLE == status_codes.IDLE
assert dt.ERROR == status_codes.ERROR
assert dt._enum == status_codes
def test_get_datatype():
with pytest.raises(ValueError):
get_datatype(1)

View File

@ -107,15 +107,11 @@ def test_IOHandler():
class Module1(Module):
properties = {
'channel': Property('the channel', IntRange(), default=3),
'loop': Property('the loop', IntRange(), default=2),
}
parameters = {
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
'real': Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False),
'text': Parameter('a string value', StringType(), default='x', handler=group2, readonly=False),
}
channel = Property('the channel', IntRange(), default=3)
loop = Property('the loop', IntRange(), default=2)
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
def sendRecv(self, command):
assert data.pop('command') == command
@ -196,6 +192,4 @@ def test_IOHandler():
with pytest.raises(ProgrammingError): # can not use a handler for different modules
# pylint: disable=unused-variable
class Module2(Module):
parameters = {
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
}
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)

View File

@ -22,14 +22,14 @@
# *****************************************************************************
"""test data types."""
# no fixtures needed
#import pytest
import threading
import pytest
from secop.datatypes import BoolType, FloatRange, StringType
from secop.modules import Communicator, Drivable, Module
from secop.params import Command, Override, Parameter, usercommand
from secop.params import Command, Parameter
from secop.poller import BasicPoller
from secop.errors import ProgrammingError
class DispatcherStub:
@ -64,30 +64,27 @@ def test_Communicator():
assert event.is_set() # event should be set immediately
def test_ModuleMeta():
def test_ModuleMagic():
class Newclass1(Drivable):
parameters = {
'pollinterval': Override(reorder=True),
'param1' : Parameter('param1', datatype=BoolType(), default=False),
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
}
commands = {
# intermixing parameters with commands is not recommended,
# but acceptable for influencing the order
'a1': Parameter('a1', datatype=BoolType(), default=False),
'a2': Parameter('a2', datatype=BoolType(), default=True),
'value': Override(datatype=StringType(), default='first'),
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
}
param1 = Parameter('param1', datatype=BoolType(), default=False)
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
@Command(argument=BoolType(), result=BoolType())
def cmd(self, arg):
"""stuff"""
return not arg
a1 = Parameter('a1', datatype=BoolType(), default=False)
a2 = Parameter('a2', datatype=BoolType(), default=True)
value = Parameter(datatype=StringType(), default='first')
@Command(argument=BoolType(), result=BoolType())
def cmd2(self, arg):
"""another stuff"""
return not arg
pollerClass = BasicPoller
def do_cmd(self, arg):
return not arg
def do_cmd2(self, arg):
return not arg
def read_param1(self):
return True
@ -103,19 +100,31 @@ def test_ModuleMeta():
def read_value(self):
return 'second'
with pytest.raises(ProgrammingError):
class Mod1(Module): # pylint: disable=unused-variable
def do_this(self): # old style command
pass
# first inherited accessibles, then Overrides with reorder=True and new accessibles
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
with pytest.raises(ProgrammingError):
class Mod2(Module): # pylint: disable=unused-variable
param = Parameter(), # pylint: disable=trailing-comma-tuple
# first inherited accessibles
sortcheck1 = ['value', 'status', 'pollinterval', 'target', 'stop',
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
class Newclass2(Newclass1):
parameters = {
'cmd2': Override('another stuff'),
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
'b2': Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True),
}
paramOrder = 'param1', 'param2', 'cmd', 'value'
@Command(description='another stuff')
def cmd2(self, arg):
return arg
value = Parameter(datatype=FloatRange(unit='deg'))
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
def write_a1(self, value):
self._a1_written = value
@ -128,47 +137,15 @@ def test_ModuleMeta():
def read_value(self):
return 0
sortcheck2 = ['status', 'target', 'pollinterval',
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'value', 'a1', 'b2']
# check consistency of new syntax:
class Testclass1(Drivable):
pollinterval = Parameter(reorder=True)
param1 = Parameter('param1', datatype=BoolType(), default=False)
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
@usercommand(BoolType(), BoolType())
def cmd(self, arg):
"""stuff"""
return not arg
a1 = Parameter('a1', datatype=BoolType(), default=False)
a2 = Parameter('a2', datatype=BoolType(), default=True)
value = Parameter(datatype=StringType(), default='first')
@usercommand(BoolType(), BoolType())
def cmd2(self, arg):
"""another stuff"""
return not arg
class Testclass2(Testclass1):
cmd2 = Command('another stuff')
value = Parameter(datatype=FloatRange(unit='deg'), reorder=True)
a1 = Parameter(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
for old, new in (Newclass1, Testclass1), (Newclass2, Testclass2):
assert len(old.accessibles) == len(new.accessibles)
for (oname, oobj), (nname, nobj) in zip(old.accessibles.items(), new.accessibles.items()):
assert oname == nname
assert oobj.for_export() == nobj.for_export()
# first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
logger = LoggerStub()
updates = {}
srv = ServerStub(updates)
params_found = set() # set of instance accessibles
params_found = set() # set of instance accessibles
objects = []
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
@ -176,15 +153,11 @@ def test_ModuleMeta():
o2 = newclass('o2', logger, {'.description':''}, srv)
for obj in [o1, o2]:
objects.append(obj)
ctr_found = set()
for n, o in obj.accessibles.items():
for o in obj.accessibles.values():
# check that instance accessibles are unique objects
assert o not in params_found
params_found.add(o)
assert o.ctr not in ctr_found
ctr_found.add(o.ctr)
check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck]
assert check_order == sorted(check_order)
assert list(obj.accessibles) == sortcheck
# check for inital updates working properly
o1 = Newclass1('o1', logger, {'.description':''}, srv)
@ -246,7 +219,7 @@ def test_ModuleMeta():
assert acs is not None
else: # do not check object or mixin
acs = {}
for n, o in acs.items():
for o in acs.values():
# check that class accessibles are not reused as instance accessibles
assert o not in params_found

View File

@ -25,68 +25,78 @@
# no fixtures needed
import pytest
from secop.datatypes import BoolType, IntRange
from secop.params import Command, Override, Parameter, Parameters
from secop.datatypes import BoolType, IntRange, FloatRange
from secop.params import Command, Parameter
from secop.modules import HasAccessibles
from secop.errors import ProgrammingError
def test_Command():
cmd = Command('do_something')
assert cmd.description == 'do_something'
assert cmd.ctr
assert cmd.argument is None
assert cmd.result is None
assert cmd.for_export() == {'datainfo': {'type': 'command'},
'description': 'do_something'}
class Mod(HasAccessibles):
@Command()
def cmd(self):
"""do something"""
@Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
def cmd2(self):
pass
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1))
assert cmd.description
assert isinstance(cmd.argument, IntRange)
assert isinstance(cmd.result, IntRange)
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9},
'result': {'type': 'int', 'min':-1, 'max':1}},
'description': 'do_something'}
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
'result': {'type': 'int', 'max': 1, 'min': -1}},
'description': 'do_something'}
assert Mod.cmd.description == 'do something'
assert Mod.cmd.argument is None
assert Mod.cmd.result is None
assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
'description': 'do something'}
assert Mod.cmd2.description == 'do some other thing'
assert isinstance(Mod.cmd2.argument, IntRange)
assert isinstance(Mod.cmd2.result, IntRange)
assert Mod.cmd2.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min': -9, 'max': 9},
'result': {'type': 'int', 'min': -1, 'max': 1}},
'description': 'do some other thing'}
assert Mod.cmd2.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
'result': {'type': 'int', 'max': 1, 'min': -1}},
'description': 'do some other thing'}
def test_Parameter():
p1 = Parameter('description1', datatype=IntRange(), default=0)
p2 = Parameter('description2', datatype=IntRange(), constant=1)
assert p1 != p2
assert p1.ctr != p2.ctr
class Mod(HasAccessibles):
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
p3 = Parameter('desc3', datatype=FloatRange(), default=0, readonly=False)
p4 = Parameter('desc4', datatype=FloatRange(), constant=1)
assert repr(Mod.p1) != repr(Mod.p3)
assert id(Mod.p1.datatype) != id(Mod.p2.datatype)
assert Mod.p1.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc1', 'readonly': True}
assert Mod.p2.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc2', 'readonly': True}
assert Mod.p3.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc3', 'readonly': False}
assert Mod.p4.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc4', 'readonly': True,
'constant': 1.0}
p3 = Mod.p1.copy()
assert id(p3) != id(Mod.p1)
assert repr(Mod.p1) == repr(p3)
with pytest.raises(ProgrammingError):
Parameter(None, datatype=float, inherit=False)
p3 = p1.copy()
assert p1.ctr == p3.ctr
p3.ctr = p1.ctr # manipulate ctr for next line
assert repr(p1) == repr(p3)
assert p1.datatype != p2.datatype
def test_Override():
p = Parameter('description1', datatype=BoolType, default=False)
class Base(HasAccessibles):
p1 = Parameter('description1', datatype=BoolType, default=False)
p2 = Parameter('description1', datatype=BoolType, default=False)
p3 = Parameter('description1', datatype=BoolType, default=False)
o = Override(default=True, reorder=True)
q = o.apply(p)
qctr = q.ctr
assert q.ctr > p.ctr # reorder=True: take ctr from override object
assert q != p
assert qctr == o.apply(p).ctr # do not create a new ctr when applied again
class Mod(Base):
p1 = Parameter(default=True)
p2 = Parameter() # override without change
o2 = Override(default=True)
q2 = o2.apply(p)
assert q2.ctr == p.ctr # reorder=False: take ctr from inherited param
assert q2 != p
assert repr(q2) != repr(p)
assert Mod.p1 != Base.p1
assert Mod.p2 != Base.p2
assert Mod.p3 == Base.p3
q3 = Override().apply(p) # Override without change
assert id(q2) != id(p) # must be a new object
assert repr(q3) == repr(p) # but must be a clone
assert id(Mod.p2) != id(Base.p2) # must be a new object
assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
def test_Parameters():
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True)))
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True)
assert ps['_p2'].export == '_p2'
def test_Export():
class Mod:
param = Parameter('description1', datatype=BoolType, default=False)
assert Mod.param.export == '_param'

View File

@ -24,38 +24,58 @@
import pytest
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
from secop.errors import ProgrammingError, ConfigError
from secop.properties import Property, Properties, HasProperties
from secop.errors import ProgrammingError, ConfigError, BadValueError
from secop.properties import Property, HasProperties
# args are: datatype, default, extname, export, mandatory, settable
def Prop(*args, name=None, **kwds):
# collect the args for Property
return name, args, kwds
# Property(description, datatype, default, ...)
V_test_Property = [
[(StringType(), 'default', 'extname', False, False),
dict(default='default', extname='extname', export=True, mandatory=False)],
[(IntRange(), '42', '_extname', False, True),
dict(default=42, extname='_extname', export=True, mandatory=True)],
[(IntRange(), '42', '_extname', True, False),
dict(default=42, extname='_extname', export=True, mandatory=False)],
[(IntRange(), 42, '_extname', True, True),
dict(default=42, extname='_extname', export=True, mandatory=True)],
[(IntRange(), 0, '', True, True),
dict(default=0, extname='', export=True, mandatory=True)],
[(IntRange(), 0, '', True, False),
dict(default=0, extname='', export=True, mandatory=False)],
[(IntRange(), 0, '', False, True),
dict(default=0, extname='', export=False, mandatory=True)],
[(IntRange(), 0, '', False, False),
dict(default=0, extname='', export=False, mandatory=False)],
[(IntRange(), None, '', None),
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory
[(ValueType(), 1, '', False),
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory
[Prop(StringType(), 'default', extname='extname', mandatory=False),
dict(default='default', extname='extname', export=True, mandatory=False)
],
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
dict(default=42, extname='_custom', export=True, mandatory=True),
],
[Prop(IntRange(), '42', export=True, name='name'),
dict(default=42, extname='_name', export=True, mandatory=False)
],
[Prop(IntRange(), 42, '_extname', mandatory=True),
dict(default=42, extname='_extname', export=True, mandatory=True)
],
[Prop(IntRange(), 0, export=True, mandatory=True),
dict(default=0, extname='', export=True, mandatory=True)
],
[Prop(IntRange(), 0, export=True, mandatory=False),
dict(default=0, extname='', export=True, mandatory=False)
],
[Prop(IntRange(), 0, export=False, mandatory=True),
dict(default=0, extname='', export=False, mandatory=True)
],
[Prop(IntRange(), 0, export=False, mandatory=False),
dict(default=0, extname='', export=False, mandatory=False)
],
[Prop(IntRange()),
dict(default=0, extname='', export=False, mandatory=True) # mandatory not given, no default -> mandatory
],
[Prop(ValueType(), 1),
dict(default=1, extname='', export=False, mandatory=False) # mandatory not given, default given -> NOT mandatory
],
]
@pytest.mark.parametrize('args, check', V_test_Property)
def test_Property(args, check):
p = Property('', *args)
@pytest.mark.parametrize('propargs, check', V_test_Property)
def test_Property(propargs, check):
name, args, kwds = propargs
p = Property('', *args, **kwds)
if name:
p.__set_name__(None, name)
result = {k: getattr(p, k) for k in check}
assert result == check
def test_Property_basic():
with pytest.raises(TypeError):
# pylint: disable=no-value-for-parameter
@ -67,47 +87,47 @@ def test_Property_basic():
Property('', 1)
Property('', IntRange(), '42', 'extname', False, False)
def test_Properties():
p = Properties()
with pytest.raises(ProgrammingError):
p[1] = 2
p['a'] = Property('', IntRange(), '42', export=True)
assert p['a'].default == 42
assert p['a'].export is True
assert p['a'].extname == '_a'
with pytest.raises(ProgrammingError):
p['a'] = 137
with pytest.raises(ProgrammingError):
del p[1]
with pytest.raises(ProgrammingError):
del p['a']
p['a'] = Property('', IntRange(), 0, export=False)
assert p['a'].default == 0
assert p['a'].export is False
assert p['a'].extname == ''
class Cls(HasProperties):
aa = Property('', IntRange(0, 99), '42', export=True)
bb = Property('', IntRange(), 0, export=False)
assert Cls.aa.default == 42
assert Cls.aa.export is True
assert Cls.aa.extname == '_aa'
cc = Cls()
with pytest.raises(BadValueError):
cc.aa = 137
assert Cls.bb.default == 0
assert Cls.bb.export is False
assert Cls.bb.extname == ''
class c(HasProperties):
properties = {
'a' : Property('', IntRange(), 1),
}
# properties
a = Property('', IntRange(), 1)
class cl(c):
properties = {
'a' : Property('', IntRange(), 3),
'b' : Property('', FloatRange(), 3.14),
'minabc': Property('', IntRange(), 8),
'maxabc': Property('', IntRange(), 9),
'minx': Property('', IntRange(), 2),
'maxy': Property('', IntRange(), 1),
}
# properties
a = Property('', IntRange(), 3)
b = Property('', FloatRange(), 3.14)
minabc = Property('', IntRange(), 8)
maxabc = Property('', IntRange(), 9)
minx = Property('', IntRange(), 2)
maxy = Property('', IntRange(), 1)
def test_HasProperties():
o = c()
assert o.properties['a'] == 1
assert o.a == 1
o = cl()
assert o.properties['a'] == 3
assert o.properties['b'] == 3.14
assert o.a == 3
assert o.b == 3.14
def test_Property_checks():
o = c()
@ -119,6 +139,7 @@ def test_Property_checks():
with pytest.raises(ConfigError):
o.checkProperties()
def test_Property_override():
o1 = c()
class co(c):
@ -131,10 +152,10 @@ def test_Property_override():
class cx(c): # pylint: disable=unused-variable
def a(self):
pass
assert 'collides with method' in str(e.value)
assert 'collides with' in str(e.value)
with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable
a = 's'
assert 'can not be set to' in str(e.value)
assert 'can not set' in str(e.value)