issues with StructOf
- depending whether client or server side, handling of optional is different - fix issues when struct is used as keyworded command arguments Change-Id: I72b347b1a96ee3bce1f67dace4862c313c12c7a9 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30972 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
d6d564f4aa
commit
6c02f37bbb
@ -1,7 +1,7 @@
|
|||||||
Node('test.config.frappy.demo',
|
Node('test.config.frappy.demo',
|
||||||
'''short description of the testing sec-node
|
'''short description of the testing sec-node
|
||||||
|
|
||||||
This description for the Nodecan be as long as you need if you use a multiline string.
|
This description for the node can be as long as you need if you use a multiline string.
|
||||||
|
|
||||||
Very long!
|
Very long!
|
||||||
The needed fields are Equipment id (1st argument), description (this)
|
The needed fields are Equipment id (1st argument), description (this)
|
||||||
@ -51,3 +51,8 @@ Mod('Decision',
|
|||||||
'that can be converted to a list',
|
'that can be converted to a list',
|
||||||
choices = ['Yes', 'Maybe', 'No'],
|
choices = ['Yes', 'Maybe', 'No'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Mod('c',
|
||||||
|
'frappy_demo.test.Commands',
|
||||||
|
'a command test',
|
||||||
|
)
|
||||||
|
@ -62,6 +62,7 @@ class DataType(HasProperties):
|
|||||||
IS_COMMAND = False
|
IS_COMMAND = False
|
||||||
unit = ''
|
unit = ''
|
||||||
default = None
|
default = None
|
||||||
|
client = False # used on the client side
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""convert given value to our datatype and validate
|
"""convert given value to our datatype and validate
|
||||||
@ -826,6 +827,7 @@ class ArrayOf(DataType):
|
|||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
|
self.check_type(value)
|
||||||
return [self.members.export_value(elem) for elem in value]
|
return [self.members.export_value(elem) for elem in value]
|
||||||
|
|
||||||
def import_value(self, value):
|
def import_value(self, value):
|
||||||
@ -893,11 +895,11 @@ class TupleOf(DataType):
|
|||||||
|
|
||||||
def check_type(self, value):
|
def check_type(self, value):
|
||||||
try:
|
try:
|
||||||
if len(value) != len(self.members):
|
if len(value) == len(self.members):
|
||||||
raise WrongTypeError(
|
return
|
||||||
f'tuple needs {len(self.members)} elements')
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise WrongTypeError(f'{type(value).__name__} can not be converted to TupleOf DataType!') from None
|
raise WrongTypeError(f'{type(value).__name__} can not be converted to TupleOf DataType!') from None
|
||||||
|
raise WrongTypeError(f'tuple needs {len(self.members)} elements')
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""accepts any sequence, converts to tuple"""
|
"""accepts any sequence, converts to tuple"""
|
||||||
@ -920,6 +922,7 @@ class TupleOf(DataType):
|
|||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
|
self.check_type(value)
|
||||||
return [sub.export_value(elem) for sub, elem in zip(self.members, value)]
|
return [sub.export_value(elem) for sub, elem in zip(self.members, value)]
|
||||||
|
|
||||||
def import_value(self, value):
|
def import_value(self, value):
|
||||||
@ -990,54 +993,59 @@ class StructOf(DataType):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
opt = f', optional={self.optional!r}' if self.optional else ''
|
opt = f', optional={self.optional!r}' if set(self.optional) == set(self.members) else ''
|
||||||
return 'StructOf(%s%s)' % (', '.join(
|
return 'StructOf(%s%s)' % (', '.join(
|
||||||
['%s=%s' % (n, repr(st)) for n, st in list(self.members.items())]), opt)
|
['%s=%s' % (n, repr(st)) for n, st in list(self.members.items())]), opt)
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""accepts any mapping, returns an immutable dict"""
|
"""accepts any mapping, returns an immutable dict"""
|
||||||
|
self.check_type(value)
|
||||||
try:
|
try:
|
||||||
if set(dict(value)) != set(self.members):
|
result = {}
|
||||||
raise WrongTypeError('member names do not match') from None
|
for key, val in value.items():
|
||||||
except TypeError:
|
if val is not None: # goodie: allow None instead of missing key
|
||||||
raise WrongTypeError(f'{type(value).__name__} can not be converted a StructOf') from None
|
result[key] = self.members[key](val)
|
||||||
try:
|
return ImmutableDict(result)
|
||||||
return ImmutableDict((str(k), self.members[k](v))
|
|
||||||
for k, v in list(value.items()))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
|
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
|
||||||
raise errcls('can not convert some struct element') from e
|
raise errcls('can not convert struct element %s' % key) from e
|
||||||
|
|
||||||
def validate(self, value, previous=None):
|
def validate(self, value, previous=None):
|
||||||
|
self.check_type(value, True)
|
||||||
|
try:
|
||||||
|
result = dict(previous or {})
|
||||||
|
for key, val in value.items():
|
||||||
|
if val is not None: # goodie: allow None instead of missing key
|
||||||
|
result[key] = self.members[key].validate(val)
|
||||||
|
return ImmutableDict(result)
|
||||||
|
except Exception as e:
|
||||||
|
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
|
||||||
|
raise errcls('struct element %s is invalid' % key) from e
|
||||||
|
|
||||||
|
def check_type(self, value, allow_optional=False):
|
||||||
try:
|
try:
|
||||||
superfluous = set(dict(value)) - set(self.members)
|
superfluous = set(dict(value)) - set(self.members)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise WrongTypeError(f'{type(value).__name__} can not be converted a StructOf') from None
|
raise WrongTypeError(f'{type(value).__name__} can not be converted a StructOf') from None
|
||||||
if superfluous - set(self.optional):
|
if superfluous - set(self.optional):
|
||||||
raise WrongTypeError(f"struct contains superfluous members: {', '.join(superfluous)}")
|
raise WrongTypeError(f"struct contains superfluous members: {', '.join(superfluous)}")
|
||||||
missing = set(self.members) - set(value) - set(self.optional)
|
missing = set(self.members) - set(value)
|
||||||
|
if self.client or allow_optional: # on the client side, allow optional elements always
|
||||||
|
missing -= set(self.optional)
|
||||||
if missing:
|
if missing:
|
||||||
raise WrongTypeError(f"missing struct elements: {', '.join(missing)}")
|
raise WrongTypeError(f"missing struct elements: {', '.join(missing)}")
|
||||||
try:
|
|
||||||
if previous is None:
|
|
||||||
return ImmutableDict((str(k), self.members[k].validate(v))
|
|
||||||
for k, v in list(value.items()))
|
|
||||||
result = dict(previous)
|
|
||||||
result.update(((k, self.members[k].validate(v, previous[k])) for k, v in value.items()))
|
|
||||||
return ImmutableDict(result)
|
|
||||||
except Exception as e:
|
|
||||||
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
|
|
||||||
raise errcls('some struct elements are invalid') from e
|
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
self(value) # check validity
|
self.check_type(value)
|
||||||
return dict((str(k), self.members[k].export_value(v))
|
return dict((str(k), self.members[k].export_value(v))
|
||||||
for k, v in list(value.items()))
|
for k, v in list(value.items()))
|
||||||
|
|
||||||
def import_value(self, value):
|
def import_value(self, value):
|
||||||
"""returns a python object from serialisation"""
|
"""returns a python object from serialisation"""
|
||||||
return self({str(k): self.members[k].import_value(v) for k, v in value.items()})
|
self.check_type(value, True)
|
||||||
|
return {str(k): self.members[k].import_value(v)
|
||||||
|
for k, v in value.items()}
|
||||||
|
|
||||||
def from_string(self, text):
|
def from_string(self, text):
|
||||||
value, rem = Parser.parse(text)
|
value, rem = Parser.parse(text)
|
||||||
@ -1344,7 +1352,7 @@ DATATYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# important for getting the right datatype from formerly jsonified descr.
|
# used on the client side for getting the right datatype from formerly jsonified descr.
|
||||||
def get_datatype(json, pname=''):
|
def get_datatype(json, pname=''):
|
||||||
"""returns a DataType object from description
|
"""returns a DataType object from description
|
||||||
|
|
||||||
@ -1366,6 +1374,8 @@ def get_datatype(json, pname=''):
|
|||||||
raise WrongTypeError(f'a data descriptor must be a dict containing a "type" key, not {json!r}') from None
|
raise WrongTypeError(f'a data descriptor must be a dict containing a "type" key, not {json!r}') from None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return DATATYPES[base](pname=pname, **kwargs)
|
datatype = DATATYPES[base](pname=pname, **kwargs)
|
||||||
|
datatype.client = True
|
||||||
|
return datatype
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise WrongTypeError(f'invalid data descriptor: {json!r} ({str(e)})') from None
|
raise WrongTypeError(f'invalid data descriptor: {json!r} ({str(e)})') from None
|
||||||
|
@ -492,8 +492,6 @@ class Command(Accessible):
|
|||||||
# pylint: disable=unnecessary-dunder-call
|
# pylint: disable=unnecessary-dunder-call
|
||||||
func = self.__get__(module_obj)
|
func = self.__get__(module_obj)
|
||||||
if self.argument:
|
if self.argument:
|
||||||
# validate
|
|
||||||
argument = self.argument(argument)
|
|
||||||
if isinstance(self.argument, TupleOf):
|
if isinstance(self.argument, TupleOf):
|
||||||
res = func(*argument)
|
res = func(*argument)
|
||||||
elif isinstance(self.argument, StructOf):
|
elif isinstance(self.argument, StructOf):
|
||||||
|
@ -23,10 +23,10 @@
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from frappy.datatypes import FloatRange, StringType, ValueType
|
from frappy.datatypes import FloatRange, StringType, ValueType, TupleOf, StructOf, ArrayOf
|
||||||
from frappy.modules import Communicator, Drivable, Parameter, Property, \
|
from frappy.modules import Communicator, Drivable, Parameter, Property, Readable, Module
|
||||||
Readable
|
|
||||||
from frappy.params import Command
|
from frappy.params import Command
|
||||||
|
from frappy.errors import RangeError
|
||||||
|
|
||||||
|
|
||||||
class LN2(Readable):
|
class LN2(Readable):
|
||||||
@ -95,9 +95,36 @@ class Lower(Communicator):
|
|||||||
"""lowercase a string"""
|
"""lowercase a string"""
|
||||||
return str(command).lower()
|
return str(command).lower()
|
||||||
|
|
||||||
|
|
||||||
class Mapped(Readable):
|
class Mapped(Readable):
|
||||||
value = Parameter(datatype=StringType())
|
value = Parameter(datatype=StringType())
|
||||||
choices = Property('List of choices',
|
choices = Property('List of choices',
|
||||||
datatype=ValueType(list))
|
datatype=ValueType(list))
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.choices[random.randrange(len(self.choices))]
|
return self.choices[random.randrange(len(self.choices))]
|
||||||
|
|
||||||
|
|
||||||
|
class Commands(Module):
|
||||||
|
"""Command argument tests"""
|
||||||
|
|
||||||
|
@Command(argument=TupleOf(FloatRange(0, 1), StringType()), result=StringType())
|
||||||
|
def t(self, f, s):
|
||||||
|
"""a command with positional arguments (tuple)"""
|
||||||
|
return '%g %r' % (f, s)
|
||||||
|
|
||||||
|
@Command(argument=StructOf(a=FloatRange(0, 1), b=StringType()), result=StringType())
|
||||||
|
def s(self, a=0, b=''):
|
||||||
|
"""a command with keyword arguments (struct)"""
|
||||||
|
return 'a=%r b=%r' % (a, b)
|
||||||
|
|
||||||
|
@Command(result=FloatRange(0, 1))
|
||||||
|
def n(self):
|
||||||
|
"""no args, but returning a value"""
|
||||||
|
return 2 # returning a value outside range should be allowed
|
||||||
|
|
||||||
|
@Command(argument=ArrayOf(FloatRange()))
|
||||||
|
def a(self, a):
|
||||||
|
"""array argument. raises an error when sum is negativ"""
|
||||||
|
if sum(a) < 0:
|
||||||
|
raise RangeError('sum must be >= 0')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user