fix command doc string handling and change default stop doc string
- fix inheritance of command description - when no stop method is given, then the description should indicate that stop is a no-op -> add missing doc strings to stop methods - add test to make sure stop command doc strings are given when implemented Change-Id: If891359350e8dcdec39a706841d61d4f8ec8926f Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33266 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
b454f47a12
commit
0f50de9a7f
@ -79,7 +79,9 @@ class StructParam(Parameter):
|
|||||||
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
|
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
|
||||||
kwds['influences'] = [p.name for p in paramdict.values()]
|
kwds['influences'] = [p.name for p in paramdict.values()]
|
||||||
self.updateEnable = {}
|
self.updateEnable = {}
|
||||||
super().__init__(description, datatype, paramdict=paramdict, readonly=readonly, **kwds)
|
if paramdict:
|
||||||
|
kwds['paramdict'] = paramdict
|
||||||
|
super().__init__(description, datatype, readonly=readonly, **kwds)
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
def __set_name__(self, owner, name):
|
||||||
# names of access methods of structed param (e.g. ctrlpars)
|
# names of access methods of structed param (e.g. ctrlpars)
|
||||||
|
@ -141,6 +141,7 @@ class SequencerMixin:
|
|||||||
return self.Status.IDLE, ''
|
return self.Status.IDLE, ''
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop sequence"""
|
||||||
if self.seq_is_alive():
|
if self.seq_is_alive():
|
||||||
self._seq_stopflag = True
|
self._seq_stopflag = True
|
||||||
|
|
||||||
|
@ -83,15 +83,14 @@ class HasAccessibles(HasProperties):
|
|||||||
override_values.pop(key, None)
|
override_values.pop(key, None)
|
||||||
elif key in accessibles:
|
elif key in accessibles:
|
||||||
override_values[key] = value
|
override_values[key] = value
|
||||||
|
# remark: merged_properties contain already the properties of accessibles of cls
|
||||||
for aname, aobj in list(accessibles.items()):
|
for aname, aobj in list(accessibles.items()):
|
||||||
if aname in override_values:
|
if aname in override_values:
|
||||||
aobj = aobj.copy()
|
|
||||||
value = override_values[aname]
|
value = override_values[aname]
|
||||||
if value is None:
|
if value is None:
|
||||||
accessibles.pop(aname)
|
accessibles.pop(aname)
|
||||||
continue
|
continue
|
||||||
aobj.merge(merged_properties[aname])
|
aobj = aobj.create_from_value(merged_properties[aname], value)
|
||||||
aobj.override(value)
|
|
||||||
# replace the bare value by the created accessible
|
# replace the bare value by the created accessible
|
||||||
setattr(cls, aname, aobj)
|
setattr(cls, aname, aobj)
|
||||||
else:
|
else:
|
||||||
|
@ -36,6 +36,7 @@ from .modulebase import Module
|
|||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
"""basic readable module"""
|
"""basic readable module"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
Status = Enum('Status',
|
Status = Enum('Status',
|
||||||
IDLE=StatusType.IDLE,
|
IDLE=StatusType.IDLE,
|
||||||
WARN=StatusType.WARN,
|
WARN=StatusType.WARN,
|
||||||
@ -92,7 +93,7 @@ class Drivable(Writable):
|
|||||||
|
|
||||||
@Command(None, result=None)
|
@Command(None, result=None)
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""cease driving, go to IDLE state"""
|
"""not implemented - this is a no-op"""
|
||||||
|
|
||||||
|
|
||||||
class Communicator(HasComlog, Module):
|
class Communicator(HasComlog, Module):
|
||||||
|
@ -57,13 +57,17 @@ class Accessible(HasProperties):
|
|||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return self.propertyValues
|
return self.propertyValues
|
||||||
|
|
||||||
def override(self, value):
|
def create_from_value(self, properties, value):
|
||||||
"""override with a bare value"""
|
"""return a clone with given value and inherited properties"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def clone(self, properties, **kwds):
|
||||||
|
"""return a clone of ourselfs with inherited properties"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a (deep) copy of ourselfs"""
|
||||||
raise NotImplementedError
|
return self.clone(self.propertyValues)
|
||||||
|
|
||||||
def updateProperties(self, merged_properties):
|
def updateProperties(self, merged_properties):
|
||||||
"""update merged_properties with our own properties"""
|
"""update merged_properties with our own properties"""
|
||||||
@ -234,13 +238,15 @@ class Parameter(Accessible):
|
|||||||
# avoid export=True overrides export=<name>
|
# avoid export=True overrides export=<name>
|
||||||
self.ownProperties['export'] = self.export
|
self.ownProperties['export'] = self.export
|
||||||
|
|
||||||
def copy(self):
|
def clone(self, properties, **kwds):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a clone of ourselfs with inherited properties"""
|
||||||
res = type(self)()
|
res = type(self)(**kwds)
|
||||||
res.name = self.name
|
res.name = self.name
|
||||||
res.init(self.propertyValues)
|
res.init(properties)
|
||||||
|
res.init(res.ownProperties)
|
||||||
if 'datatype' in self.propertyValues:
|
if 'datatype' in self.propertyValues:
|
||||||
res.datatype = res.datatype.copy()
|
res.datatype = res.datatype.copy()
|
||||||
|
res.finish()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def updateProperties(self, merged_properties):
|
def updateProperties(self, merged_properties):
|
||||||
@ -253,9 +259,9 @@ class Parameter(Accessible):
|
|||||||
merged_properties.pop(key)
|
merged_properties.pop(key)
|
||||||
merged_properties.update(self.ownProperties)
|
merged_properties.update(self.ownProperties)
|
||||||
|
|
||||||
def override(self, value):
|
def create_from_value(self, properties, value):
|
||||||
"""override default"""
|
"""return a clone with given value and inherited properties"""
|
||||||
self.value = self.datatype(value)
|
return self.clone(properties, value=self.datatype(value))
|
||||||
|
|
||||||
def merge(self, merged_properties):
|
def merge(self, merged_properties):
|
||||||
"""merge with inherited properties
|
"""merge with inherited properties
|
||||||
@ -390,7 +396,7 @@ class Command(Accessible):
|
|||||||
else:
|
else:
|
||||||
# goodie: allow @Command instead of @Command()
|
# goodie: allow @Command instead of @Command()
|
||||||
self.func = argument # this is the wrapped method!
|
self.func = argument # this is the wrapped method!
|
||||||
if argument.__doc__:
|
if argument.__doc__ is not None:
|
||||||
self.description = inspect.cleandoc(argument.__doc__)
|
self.description = inspect.cleandoc(argument.__doc__)
|
||||||
self.name = self.func.__name__ # this is probably not needed
|
self.name = self.func.__name__ # this is probably not needed
|
||||||
self._inherit = inherit # save for __set_name__
|
self._inherit = inherit # save for __set_name__
|
||||||
@ -439,38 +445,37 @@ class Command(Accessible):
|
|||||||
f' members!: {params} != {members}')
|
f' members!: {params} != {members}')
|
||||||
self.argument.optional = [p for p,v in sig.parameters.items()
|
self.argument.optional = [p for p,v in sig.parameters.items()
|
||||||
if v.default is not inspect.Parameter.empty]
|
if v.default is not inspect.Parameter.empty]
|
||||||
if 'description' not in self.propertyValues and func.__doc__:
|
if 'description' not in self.ownProperties and func.__doc__ is not None:
|
||||||
self.description = inspect.cleandoc(func.__doc__)
|
self.description = inspect.cleandoc(func.__doc__)
|
||||||
self.ownProperties['description'] = self.description
|
self.ownProperties['description'] = self.description
|
||||||
self.func = func
|
self.func = func
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def copy(self):
|
def clone(self, properties, **kwds):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a clone of ourselfs with inherited properties"""
|
||||||
res = type(self)()
|
res = type(self)(**kwds)
|
||||||
res.name = self.name
|
res.name = self.name
|
||||||
res.func = self.func
|
res.func = self.func
|
||||||
res.init(self.propertyValues)
|
res.init(properties)
|
||||||
|
res.init(res.ownProperties)
|
||||||
if res.argument:
|
if res.argument:
|
||||||
res.argument = res.argument.copy()
|
res.argument = res.argument.copy()
|
||||||
if res.result:
|
if res.result:
|
||||||
res.result = res.result.copy()
|
res.result = res.result.copy()
|
||||||
self.finish()
|
res.finish()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def updateProperties(self, merged_properties):
|
def updateProperties(self, merged_properties):
|
||||||
"""update merged_properties with our own properties"""
|
"""update merged_properties with our own properties"""
|
||||||
merged_properties.update(self.ownProperties)
|
merged_properties.update(self.ownProperties)
|
||||||
|
|
||||||
def override(self, value):
|
def create_from_value(self, properties, value):
|
||||||
"""override method
|
"""return a clone with given value and inherited properties
|
||||||
|
|
||||||
this is needed when the @Command is missing on a method overriding a command"""
|
this is needed when the @Command is missing on a method overriding a command"""
|
||||||
if not callable(value):
|
if not callable(value):
|
||||||
raise ProgrammingError(f'{self.name} = {value!r} is overriding a Command')
|
raise ProgrammingError(f'{self.name} = {value!r} is overriding a Command')
|
||||||
self.func = value
|
return self.clone(properties)(value)
|
||||||
if value.__doc__:
|
|
||||||
self.description = inspect.cleandoc(value.__doc__)
|
|
||||||
|
|
||||||
def merge(self, merged_properties):
|
def merge(self, merged_properties):
|
||||||
"""merge with inherited properties
|
"""merge with inherited properties
|
||||||
|
@ -239,6 +239,7 @@ class HasStates:
|
|||||||
|
|
||||||
@Command
|
@Command
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop state machine"""
|
||||||
self.stop_machine()
|
self.stop_machine()
|
||||||
|
|
||||||
def final_status(self, code=IDLE, text=''):
|
def final_status(self, code=IDLE, text=''):
|
||||||
|
@ -120,6 +120,7 @@ class MagneticField(Drivable):
|
|||||||
)
|
)
|
||||||
heatswitch = Attached(Switch, description='name of heat switch device')
|
heatswitch = Attached(Switch, description='name of heat switch device')
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
||||||
|
|
||||||
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||||
@ -193,6 +194,7 @@ class MagneticField(Drivable):
|
|||||||
self.log.error(self, 'main thread exited unexpectedly!')
|
self.log.error(self, 'main thread exited unexpectedly!')
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current value"""
|
||||||
self.write_target(self.read_value())
|
self.write_target(self.read_value())
|
||||||
|
|
||||||
|
|
||||||
|
@ -641,6 +641,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
sleep(0.3)
|
sleep(0.3)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""cease driving, go to IDLE state"""
|
||||||
self._dev.Stop()
|
self._dev.Stop()
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,6 +198,10 @@ class MotorValve(PersistentMixin, Drivable):
|
|||||||
|
|
||||||
@Command
|
@Command
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current position
|
||||||
|
|
||||||
|
state will probably be undefined
|
||||||
|
"""
|
||||||
self._state.stop()
|
self._state.stop()
|
||||||
self.motor.stop()
|
self.motor.stop()
|
||||||
|
|
||||||
|
@ -483,6 +483,10 @@ class Temp(PpmsDrivable):
|
|||||||
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""set setpoint to current value
|
||||||
|
|
||||||
|
but restrict to values between last target and current target
|
||||||
|
"""
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
if self.status[0] != StatusType.STABILIZING:
|
if self.status[0] != StatusType.STABILIZING:
|
||||||
@ -612,6 +616,7 @@ class Field(PpmsDrivable):
|
|||||||
# do not execute FIELD command, as this would trigger a ramp up of leads current
|
# do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current driven Field"""
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
@ -714,6 +719,7 @@ class Position(PpmsDrivable):
|
|||||||
return value # do not execute MOVE command, as this would trigger an unnecessary move
|
return value # do not execute MOVE command, as this would trigger an unnecessary move
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop motor"""
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import importlib
|
||||||
|
from glob import glob
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||||
@ -440,12 +442,12 @@ def test_override():
|
|||||||
assert Mod.value.value == 5
|
assert Mod.value.value == 5
|
||||||
assert Mod.stop.description == "no decorator needed"
|
assert Mod.stop.description == "no decorator needed"
|
||||||
|
|
||||||
class Mod2(Drivable):
|
class Mod2(Mod):
|
||||||
@Command()
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert Mod2.stop.description == Drivable.stop.description
|
# inherit doc string
|
||||||
|
assert Mod2.stop.description == Mod.stop.description
|
||||||
|
|
||||||
|
|
||||||
def test_command_config():
|
def test_command_config():
|
||||||
@ -920,3 +922,24 @@ def test_interface_classes(bases, iface_classes):
|
|||||||
pass
|
pass
|
||||||
m = Mod('mod', LoggerStub(), {'description': 'test'}, srv)
|
m = Mod('mod', LoggerStub(), {'description': 'test'}, srv)
|
||||||
assert m.interface_classes == iface_classes
|
assert m.interface_classes == iface_classes
|
||||||
|
|
||||||
|
|
||||||
|
all_drivables = set()
|
||||||
|
for pyfile in glob('frappy_*/*.py'):
|
||||||
|
module = pyfile[:-3].replace('/', '.')
|
||||||
|
try:
|
||||||
|
importlib.import_module(module)
|
||||||
|
except Exception as e:
|
||||||
|
print(module, e)
|
||||||
|
continue
|
||||||
|
for obj_ in sys.modules[module].__dict__.values():
|
||||||
|
if isinstance(obj_, type) and issubclass(obj_, Drivable):
|
||||||
|
all_drivables.add(obj_)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('modcls', all_drivables)
|
||||||
|
def test_stop_doc(modcls):
|
||||||
|
# make sure that implemented stop methods have a doc string
|
||||||
|
if (modcls.stop.description == Drivable.stop.description
|
||||||
|
and modcls.stop.func != Drivable.stop.func):
|
||||||
|
assert modcls.stop.func.__doc__ # stop method needs a doc string
|
||||||
|
Loading…
x
Reference in New Issue
Block a user