core: alternative approach for optional accessibles
This is meant to replace change 33375. Optional commands and parameters may be declared with the argument optional=True. In principle, optional commands are not really needed to be declared, but doing so is nice for documentation reasons and for inherited accessible properties. Optional parameters and commands can not be used and are not exported als long as they are not overridden in subclasses. - add a test for this + fix an issue with checking for methods like read_<param> without <param> being a parameter Change-Id: Ide5021127a02778e7f2f7162555ec8826f1471cb Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35495 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
parent
8c2588a5ed
commit
3663c62b46
@ -60,13 +60,14 @@ class HasAccessibles(HasProperties):
|
||||
(so the dispatcher will get notified of changed values)
|
||||
"""
|
||||
isWrapped = False
|
||||
checkedMethods = set()
|
||||
checkedMethods = ()
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls): # pylint: disable=too-many-branches
|
||||
super().__init_subclass__()
|
||||
if cls.isWrapped:
|
||||
return
|
||||
cls.checkedMethods = set(cls.checkedMethods) # might be initial empty tuple
|
||||
# merge accessibles from all sub-classes, treat overrides
|
||||
# for now, allow to use also the old syntax (parameters/commands dict)
|
||||
accessibles = OrderedDict() # dict of accessibles
|
||||
@ -114,8 +115,8 @@ class HasAccessibles(HasProperties):
|
||||
wrapped_name = '_' + cls.__name__
|
||||
for pname, pobj in accessibles.items():
|
||||
# wrap of reading/writing funcs
|
||||
if not isinstance(pobj, Parameter):
|
||||
# nothing to do for Commands
|
||||
if not isinstance(pobj, Parameter) or pobj.optional:
|
||||
# nothing to do for Commands and optional parameters
|
||||
continue
|
||||
|
||||
rname = 'read_' + pname
|
||||
@ -202,7 +203,7 @@ class HasAccessibles(HasProperties):
|
||||
cls.checkedMethods.update(cls.wrappedAttributes)
|
||||
|
||||
# check for programming errors
|
||||
for attrname in dir(cls):
|
||||
for attrname in cls.__dict__:
|
||||
prefix, _, pname = attrname.partition('_')
|
||||
if not pname:
|
||||
continue
|
||||
@ -390,6 +391,8 @@ class Module(HasAccessibles):
|
||||
accessibles = self.accessibles
|
||||
self.accessibles = {}
|
||||
for aname, aobj in accessibles.items():
|
||||
if aobj.optional:
|
||||
continue
|
||||
# make a copy of the Parameter/Command object
|
||||
aobj = aobj.copy()
|
||||
acfg = cfgdict.pop(aname, None)
|
||||
|
@ -47,6 +47,7 @@ class Accessible(HasProperties):
|
||||
"""
|
||||
|
||||
ownProperties = None
|
||||
optional = False
|
||||
|
||||
def init(self, kwds):
|
||||
# do not use self.propertyValues.update here, as no invalid values should be
|
||||
@ -96,6 +97,8 @@ class Accessible(HasProperties):
|
||||
props = []
|
||||
for k, v in sorted(self.propertyValues.items()):
|
||||
props.append(f'{k}={v!r}')
|
||||
if self.optional:
|
||||
props.append('optional=True')
|
||||
return f"{self.__class__.__name__}({', '.join(props)})"
|
||||
|
||||
def fixExport(self):
|
||||
@ -191,8 +194,9 @@ class Parameter(Accessible):
|
||||
readerror = None
|
||||
omit_unchanged_within = 0
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
||||
def __init__(self, description=None, datatype=None, inherit=True, optional=False, **kwds):
|
||||
super().__init__()
|
||||
self.optional = optional
|
||||
if 'poll' in kwds and generalConfig.tolerate_poll_property:
|
||||
kwds.pop('poll')
|
||||
if datatype is None:
|
||||
@ -226,10 +230,16 @@ class Parameter(Accessible):
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.parameters[self.name].value
|
||||
try:
|
||||
return instance.parameters[self.name].value
|
||||
except KeyError:
|
||||
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj.announceUpdate(self.name, value)
|
||||
try:
|
||||
obj.announceUpdate(self.name, value)
|
||||
except KeyError:
|
||||
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
@ -366,9 +376,6 @@ class Command(Accessible):
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
# optional = Property(
|
||||
# '[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||
# export=False, default=False, settable=False)
|
||||
datatype = Property(
|
||||
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||
DataTypeType(), extname='datainfo', export='always')
|
||||
@ -384,8 +391,9 @@ class Command(Accessible):
|
||||
|
||||
func = None
|
||||
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, optional=False, **kwds):
|
||||
super().__init__()
|
||||
self.optional = optional
|
||||
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'")
|
||||
@ -411,8 +419,8 @@ class Command(Accessible):
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
if self.func is None:
|
||||
raise ProgrammingError(f'Command {owner.__name__}.{name} must be used as a method decorator')
|
||||
if self.func is None and not self.optional:
|
||||
raise ProgrammingError(f'Command {owner.__name__}.{name} must be optional or used as a method decorator')
|
||||
|
||||
self.fixExport()
|
||||
self.datatype = CommandType(self.argument, self.result)
|
||||
|
@ -102,7 +102,9 @@ class Handler:
|
||||
"""create the wrapped read_* or write_* methods"""
|
||||
# at this point, this 'method_names' entry is no longer used -> delete
|
||||
self.method_names.discard((self.func.__module__, self.func.__qualname__))
|
||||
owner.checkedMethods.add(name)
|
||||
# avoid complain about read_<name> or write_<name> method without parameter <name>
|
||||
# can not use .add() here, as owner.checkedMethods might be an empty tuple
|
||||
owner.checkedMethods = set(owner.checkedMethods) | {name}
|
||||
for key in self.keys:
|
||||
wrapped = self.wrap(key)
|
||||
method_name = self.prefix + key
|
||||
|
@ -26,7 +26,7 @@ import pytest
|
||||
|
||||
from frappy.datatypes import BoolType, FloatRange, IntRange, StructOf
|
||||
from frappy.errors import ProgrammingError
|
||||
from frappy.modulebase import HasAccessibles
|
||||
from frappy.modulebase import HasAccessibles, Module
|
||||
from frappy.params import Command, Parameter
|
||||
|
||||
|
||||
@ -149,3 +149,105 @@ def test_update_unchanged_ok(arg, value):
|
||||
def test_update_unchanged_fail(arg):
|
||||
with pytest.raises(ProgrammingError):
|
||||
Parameter('', datatype=FloatRange(), default=0, update_unchanged=arg)
|
||||
|
||||
|
||||
def make_module(cls):
|
||||
class DispatcherStub:
|
||||
def announce_update(self, moduleobj, pobj):
|
||||
pass
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
class ServerStub:
|
||||
dispatcher = DispatcherStub()
|
||||
secnode = None
|
||||
|
||||
return cls('test', LoggerStub(), {'description': 'test'}, ServerStub())
|
||||
|
||||
|
||||
def test_optional_parameters():
|
||||
class Base(Module):
|
||||
p1 = Parameter('overridden', datatype=FloatRange(),
|
||||
default=1, readonly=False, optional=True)
|
||||
p2 = Parameter('not overridden', datatype=FloatRange(),
|
||||
default=2, readonly=False, optional=True)
|
||||
|
||||
class Mod(Base):
|
||||
p1 = Parameter()
|
||||
|
||||
def read_p1(self):
|
||||
return self.p1
|
||||
|
||||
def write_p1(self, value):
|
||||
return value
|
||||
|
||||
assert Base.accessibles['p2'].optional
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod2(Base): # pylint: disable=unused-variable
|
||||
def read_p2(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Base): # pylint: disable=unused-variable
|
||||
def write_p2(self):
|
||||
pass
|
||||
|
||||
base = make_module(Base)
|
||||
mod = make_module(Mod)
|
||||
|
||||
assert 'p1' not in base.accessibles
|
||||
assert 'p1' not in base.parameters
|
||||
assert 'p2' not in base.accessibles
|
||||
assert 'p2' not in base.parameters
|
||||
|
||||
assert 'p1' in mod.accessibles
|
||||
assert 'p1' in mod.parameters
|
||||
assert 'p2' not in mod.accessibles
|
||||
assert 'p2' not in mod.parameters
|
||||
|
||||
assert mod.p1 == 1
|
||||
assert mod.read_p1() == 1
|
||||
mod.p1 = 11
|
||||
assert mod.read_p1() == 11
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
assert mod.p2
|
||||
with pytest.raises(AttributeError):
|
||||
mod.read_p2()
|
||||
with pytest.raises(ProgrammingError):
|
||||
mod.p2 = 2
|
||||
with pytest.raises(AttributeError):
|
||||
mod.write_p2(2)
|
||||
|
||||
|
||||
def test_optional_commands():
|
||||
class Base(Module):
|
||||
c1 = Command(FloatRange(1), result=FloatRange(2), description='overridden', optional=True)
|
||||
c2 = Command(description='not overridden', optional=True)
|
||||
|
||||
class Mod(Base):
|
||||
def c1(self, value):
|
||||
return value + 1
|
||||
|
||||
base = make_module(Base)
|
||||
mod = make_module(Mod)
|
||||
|
||||
assert 'c1' not in base.accessibles
|
||||
assert 'c1' not in base.commands
|
||||
assert 'c2' not in base.accessibles
|
||||
assert 'c2' not in base.commands
|
||||
|
||||
assert 'c1' in mod.accessibles
|
||||
assert 'c1' in mod.commands
|
||||
assert 'c2' not in mod.accessibles
|
||||
assert 'c2' not in mod.commands
|
||||
|
||||
assert mod.c1(7) == 8
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
mod.c2()
|
||||
|
Loading…
x
Reference in New Issue
Block a user