add more tests and fixes for command inheritance
- fix CommandType.__repr__ - secop/modules.py: command properties are allowed to be configured: - section 2: remove comment and rename - section 3: all accessible properties should be checked - command description should be inherited also when taken from docstring - move test for command inheritance to test_modules.py - added tests to check for valid properties of commands Change-Id: Ic7795e305048625558e415ece099e6824df6e2c4 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27135 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:
parent
1d75d192e5
commit
c91d726f9d
@ -955,10 +955,9 @@ class CommandType(DataType):
|
|||||||
return props
|
return props
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
argstr = repr(self.argument) if self.argument else ''
|
|
||||||
if self.result is None:
|
if self.result is None:
|
||||||
return 'CommandType(%s)' % argstr
|
return 'CommandType(%s)' % (repr(self.argument) if self.argument else '')
|
||||||
return 'CommandType(%s, %s)' % (argstr, repr(self.result))
|
return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""return the validated argument value or raise"""
|
"""return the validated argument value or raise"""
|
||||||
|
@ -322,26 +322,23 @@ class Module(HasAccessibles):
|
|||||||
|
|
||||||
# 2) check and apply parameter_properties
|
# 2) check and apply parameter_properties
|
||||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||||
|
# this may also be done on commands: e.g. 'stop.visibility = advanced'
|
||||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||||
if '.' in k[1:]:
|
if '.' in k[1:]:
|
||||||
paramname, propname = k.split('.', 1)
|
aname, propname = k.split('.', 1)
|
||||||
propvalue = cfgdict.pop(k)
|
propvalue = cfgdict.pop(k)
|
||||||
paramobj = self.accessibles.get(paramname, None)
|
aobj = self.accessibles.get(aname, None)
|
||||||
# paramobj might also be a command (not sure if this is needed)
|
if aobj:
|
||||||
if paramobj:
|
|
||||||
# no longer needed, this conversion is done by DataTypeType.__call__:
|
|
||||||
# if propname == 'datatype':
|
|
||||||
# propvalue = get_datatype(propvalue, k)
|
|
||||||
try:
|
try:
|
||||||
paramobj.setProperty(propname, propvalue)
|
aobj.setProperty(propname, propvalue)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
errors.append("'%s.%s' does not exist" %
|
errors.append("'%s.%s' does not exist" %
|
||||||
(paramname, propname))
|
(aname, propname))
|
||||||
except BadValueError as e:
|
except BadValueError as e:
|
||||||
errors.append('%s.%s: %s' %
|
errors.append('%s.%s: %s' %
|
||||||
(paramname, propname, str(e)))
|
(aname, propname, str(e)))
|
||||||
else:
|
else:
|
||||||
errors.append('%r not found' % paramname)
|
errors.append('%r not found' % aname)
|
||||||
|
|
||||||
# 3) check config for problems:
|
# 3) check config for problems:
|
||||||
# only accept remaining config items specified in parameters
|
# only accept remaining config items specified in parameters
|
||||||
@ -427,11 +424,11 @@ class Module(HasAccessibles):
|
|||||||
self.checkProperties()
|
self.checkProperties()
|
||||||
except ConfigError as e:
|
except ConfigError as e:
|
||||||
errors.append(str(e))
|
errors.append(str(e))
|
||||||
for pname, p in self.parameters.items():
|
for aname, aobj in self.accessibles.items():
|
||||||
try:
|
try:
|
||||||
p.checkProperties()
|
aobj.checkProperties()
|
||||||
except ConfigError as e:
|
except (ConfigError, ProgrammingError) as e:
|
||||||
errors.append('%s: %s' % (pname, e))
|
errors.append('%s: %s' % (aname, e))
|
||||||
if errors:
|
if errors:
|
||||||
raise ConfigError(errors)
|
raise ConfigError(errors)
|
||||||
|
|
||||||
|
@ -346,6 +346,9 @@ class Command(Accessible):
|
|||||||
|
|
||||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
if 'datatype' in kwds:
|
||||||
|
# self.init will complain about invalid keywords except 'datatype', as this is a property
|
||||||
|
raise ProgrammingError("Command() got an invalid keyword 'datatype'")
|
||||||
self.init(kwds)
|
self.init(kwds)
|
||||||
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||||
# normal case
|
# normal case
|
||||||
@ -362,7 +365,7 @@ class Command(Accessible):
|
|||||||
self.func = argument # this is the wrapped method!
|
self.func = argument # this is the wrapped method!
|
||||||
if argument.__doc__:
|
if argument.__doc__:
|
||||||
self.description = inspect.cleandoc(argument.__doc__)
|
self.description = inspect.cleandoc(argument.__doc__)
|
||||||
self.name = self.func.__name__
|
self.name = self.func.__name__ # this is probably not needed
|
||||||
self._inherit = inherit # save for __set_name__
|
self._inherit = inherit # save for __set_name__
|
||||||
self.ownProperties = self.propertyValues.copy()
|
self.ownProperties = self.propertyValues.copy()
|
||||||
|
|
||||||
@ -397,6 +400,7 @@ class Command(Accessible):
|
|||||||
"""called when used as decorator"""
|
"""called when used as decorator"""
|
||||||
if 'description' not in self.propertyValues and func.__doc__:
|
if 'description' not in self.propertyValues and func.__doc__:
|
||||||
self.description = inspect.cleandoc(func.__doc__)
|
self.description = inspect.cleandoc(func.__doc__)
|
||||||
|
self.ownProperties['description'] = self.description
|
||||||
self.func = func
|
self.func = func
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -264,9 +264,101 @@ def test_param_inheritance():
|
|||||||
Base('o', logger, {'description': ''}, srv)
|
Base('o', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
|
||||||
def test_mixin():
|
def test_command_inheritance():
|
||||||
# srv = ServerStub({})
|
class Base(Module):
|
||||||
|
@Command(BoolType(), visibility=2)
|
||||||
|
def cmd(self, arg):
|
||||||
|
"""base"""
|
||||||
|
|
||||||
|
class Sub1(Base):
|
||||||
|
@Command(group='grp')
|
||||||
|
def cmd(self, arg):
|
||||||
|
"""first"""
|
||||||
|
|
||||||
|
class Sub2(Sub1):
|
||||||
|
@Command(None, result=BoolType())
|
||||||
|
def cmd(self): # pylint: disable=arguments-differ
|
||||||
|
"""second"""
|
||||||
|
|
||||||
|
class Sub3(Base):
|
||||||
|
# when either argument or result is given, the other one is assumed to be None
|
||||||
|
# i.e. here we override the argument with None
|
||||||
|
@Command(result=FloatRange())
|
||||||
|
def cmd(self, arg):
|
||||||
|
"""third"""
|
||||||
|
|
||||||
|
assert Sub1.accessibles['cmd'].for_export() == {
|
||||||
|
'description': 'first', 'group': 'grp', 'visibility': 2,
|
||||||
|
'datainfo': {'type': 'command', 'argument': {'type': 'bool'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Sub2.accessibles['cmd'].for_export() == {
|
||||||
|
'description': 'second', 'group': 'grp', 'visibility': 2,
|
||||||
|
'datainfo': {'type': 'command', 'result': {'type': 'bool'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Sub3.accessibles['cmd'].for_export() == {
|
||||||
|
'description': 'third', 'visibility': 2,
|
||||||
|
'datainfo': {'type': 'command', 'result': {'type': 'double'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
for cls in locals().values():
|
||||||
|
if hasattr(cls, 'accessibles'):
|
||||||
|
for p in cls.accessibles.values():
|
||||||
|
assert isinstance(p.ownProperties, dict)
|
||||||
|
assert p.copy().ownProperties == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_check():
|
||||||
|
srv = ServerStub({})
|
||||||
|
|
||||||
|
class Good(Module):
|
||||||
|
@Command(description='available')
|
||||||
|
def with_description(self):
|
||||||
|
pass
|
||||||
|
@Command()
|
||||||
|
def with_docstring(self):
|
||||||
|
"""docstring"""
|
||||||
|
|
||||||
|
Good('o', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
class Bad1(Module):
|
||||||
|
@Command
|
||||||
|
def without_description(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Bad2(Module):
|
||||||
|
@Command()
|
||||||
|
def without_description(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for cls in Bad1, Bad2:
|
||||||
|
with pytest.raises(ConfigError) as e_info:
|
||||||
|
cls('o', logger, {'description': ''}, srv)
|
||||||
|
assert 'description' in repr(e_info.value)
|
||||||
|
|
||||||
|
class BadDatatype(Module):
|
||||||
|
@Command(FloatRange(0.1, 0.9), result=FloatRange())
|
||||||
|
def cmd(self):
|
||||||
|
"""valid command"""
|
||||||
|
|
||||||
|
BadDatatype('o', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
# test for command property checking
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
BadDatatype('o', logger, {
|
||||||
|
'description': '',
|
||||||
|
'cmd.argument': {'type': 'double', 'min': 1, 'max': 0},
|
||||||
|
}, srv)
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
BadDatatype('o', logger, {
|
||||||
|
'description': '',
|
||||||
|
'cmd.visibility': 'invalid',
|
||||||
|
}, srv)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixin():
|
||||||
class Mixin: # no need to inherit from Module or HasAccessible
|
class Mixin: # no need to inherit from Module or HasAccessible
|
||||||
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
|
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
|
||||||
param1 = Parameter('no datatype yet', fmtstr='%.5f')
|
param1 = Parameter('no datatype yet', fmtstr='%.5f')
|
||||||
@ -319,11 +411,19 @@ def test_override():
|
|||||||
assert Mod.value.default == 5
|
assert Mod.value.default == 5
|
||||||
assert Mod.stop.description == "no decorator needed"
|
assert Mod.stop.description == "no decorator needed"
|
||||||
|
|
||||||
|
class Mod2(Drivable):
|
||||||
|
@Command()
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert Mod2.stop.description == Drivable.stop.description
|
||||||
|
|
||||||
|
|
||||||
def test_command_config():
|
def test_command_config():
|
||||||
class Mod(Module):
|
class Mod(Module):
|
||||||
@Command(IntRange(0, 1), result=IntRange(0, 1))
|
@Command(IntRange(0, 1), result=IntRange(0, 1))
|
||||||
def convert(self, value):
|
def convert(self, value):
|
||||||
|
"""dummy conversion"""
|
||||||
return value
|
return value
|
||||||
|
|
||||||
srv = ServerStub({})
|
srv = ServerStub({})
|
||||||
|
@ -98,35 +98,14 @@ def test_Override():
|
|||||||
Mod.p1.default = False
|
Mod.p1.default = False
|
||||||
assert repr(Mod.p1) == repr(Base.p1)
|
assert repr(Mod.p1) == repr(Base.p1)
|
||||||
|
|
||||||
|
for cls in locals().values():
|
||||||
|
if hasattr(cls, 'accessibles'):
|
||||||
|
for p in cls.accessibles.values():
|
||||||
|
assert isinstance(p.ownProperties, dict)
|
||||||
|
assert p.copy().ownProperties == {}
|
||||||
|
|
||||||
|
|
||||||
def test_Export():
|
def test_Export():
|
||||||
class Mod(HasAccessibles):
|
class Mod(HasAccessibles):
|
||||||
param = Parameter('description1', datatype=BoolType, default=False)
|
param = Parameter('description1', datatype=BoolType, default=False)
|
||||||
assert Mod.param.export == '_param'
|
assert Mod.param.export == '_param'
|
||||||
|
|
||||||
|
|
||||||
def test_Command_Inheritance():
|
|
||||||
class Base(HasAccessibles):
|
|
||||||
@Command(BoolType(), visibility=2)
|
|
||||||
def cmd(self, arg):
|
|
||||||
"""first"""
|
|
||||||
|
|
||||||
class Sub(Base):
|
|
||||||
@Command(group='grp')
|
|
||||||
def cmd(self, arg):
|
|
||||||
"""second"""
|
|
||||||
|
|
||||||
class Sub2(Base):
|
|
||||||
@Command(FloatRange())
|
|
||||||
def cmd(self, arg):
|
|
||||||
"""third"""
|
|
||||||
|
|
||||||
assert Sub.accessibles['cmd'].for_export() == {
|
|
||||||
'description': 'second', 'group': 'grp', 'visibility': 2,
|
|
||||||
'datainfo': {'type': 'command', 'argument': {'type': 'bool'}}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert Sub2.accessibles['cmd'].for_export() == {
|
|
||||||
'description': 'third', 'visibility': 2,
|
|
||||||
'datainfo': {'type': 'command', 'argument': {'type': 'double'}}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user