core: better command handling
* check argument of do * automatically set optional struct members from function signature Change-Id: I95684f1826c1318ea92fad2bd4c9681d85ea72f5 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/32501 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
This commit is contained in:
parent
ae7bf3ce96
commit
eeea754181
@ -25,12 +25,12 @@
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from frappy.datatypes import BoolType, CommandType, DataType, \
|
from frappy.datatypes import ArrayOf, BoolType, CommandType, DataType, \
|
||||||
DataTypeType, EnumType, NoneOr, OrType, FloatRange, \
|
DataTypeType, EnumType, FloatRange, NoneOr, OrType, StringType, StructOf, \
|
||||||
StringType, StructOf, TextType, TupleOf, ValueType, ArrayOf
|
TextType, TupleOf, ValueType
|
||||||
from frappy.errors import BadValueError, WrongTypeError, ProgrammingError
|
from frappy.errors import BadValueError, ProgrammingError, WrongTypeError
|
||||||
from frappy.properties import HasProperties, Property
|
|
||||||
from frappy.lib import generalConfig
|
from frappy.lib import generalConfig
|
||||||
|
from frappy.properties import HasProperties, Property
|
||||||
|
|
||||||
generalConfig.set_default('tolerate_poll_property', False)
|
generalConfig.set_default('tolerate_poll_property', False)
|
||||||
generalConfig.set_default('omit_unchanged_within', 0.1)
|
generalConfig.set_default('omit_unchanged_within', 0.1)
|
||||||
@ -428,6 +428,18 @@ class Command(Accessible):
|
|||||||
|
|
||||||
def __call__(self, func):
|
def __call__(self, func):
|
||||||
"""called when used as decorator"""
|
"""called when used as decorator"""
|
||||||
|
if isinstance(self.argument, StructOf):
|
||||||
|
# automatically set optional struct members
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
params = set(sig.parameters.keys())
|
||||||
|
params.discard('self')
|
||||||
|
members = set(self.argument.members)
|
||||||
|
if params != members:
|
||||||
|
raise ProgrammingError(f'Command {func.__name__}: Function'
|
||||||
|
f' argument names do not match struct'
|
||||||
|
f' members!: {params} != {members}')
|
||||||
|
self.argument.optional = [p for p,v in sig.parameters.items()
|
||||||
|
if v.default is not inspect.Parameter.empty]
|
||||||
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.ownProperties['description'] = self.description
|
||||||
@ -498,6 +510,19 @@ 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:
|
||||||
|
if argument is None:
|
||||||
|
raise WrongTypeError(
|
||||||
|
f'{module_obj.__class__.__name__}.{self.name} needs an'
|
||||||
|
f' argument of type {self.argument}!'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
argument = self.argument.import_value(argument)
|
||||||
|
except TypeError:
|
||||||
|
pass # validate will raise appropriate message
|
||||||
|
except ValueError:
|
||||||
|
pass # validate will raise appropriate message
|
||||||
|
|
||||||
|
self.argument.validate(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):
|
||||||
|
@ -132,6 +132,8 @@ class Dispatcher:
|
|||||||
self.reset_connection(conn)
|
self.reset_connection(conn)
|
||||||
|
|
||||||
def _execute_command(self, modulename, exportedname, argument=None):
|
def _execute_command(self, modulename, exportedname, argument=None):
|
||||||
|
""" Execute a command. Importing the value is done in 'do' for nicer
|
||||||
|
error messages."""
|
||||||
moduleobj = self.secnode.get_module(modulename)
|
moduleobj = self.secnode.get_module(modulename)
|
||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError(f'Module {modulename!r} does not exist')
|
raise NoSuchModuleError(f'Module {modulename!r} does not exist')
|
||||||
@ -140,9 +142,6 @@ class Dispatcher:
|
|||||||
cobj = moduleobj.commands.get(cname)
|
cobj = moduleobj.commands.get(cname)
|
||||||
if cobj is None:
|
if cobj is None:
|
||||||
raise NoSuchCommandError(f'Module {modulename!r} has no command {cname or exportedname!r}')
|
raise NoSuchCommandError(f'Module {modulename!r} has no command {cname or exportedname!r}')
|
||||||
|
|
||||||
if cobj.argument:
|
|
||||||
argument = cobj.argument.import_value(argument)
|
|
||||||
# now call func
|
# now call func
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
result = cobj.do(moduleobj, argument)
|
result = cobj.do(moduleobj, argument)
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
# no fixtures needed
|
# no fixtures needed
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from frappy.datatypes import BoolType, FloatRange, IntRange
|
from frappy.datatypes import BoolType, FloatRange, IntRange, StructOf
|
||||||
from frappy.errors import ProgrammingError
|
from frappy.errors import ProgrammingError
|
||||||
from frappy.modules import HasAccessibles
|
from frappy.modules import HasAccessibles
|
||||||
from frappy.params import Command, Parameter
|
from frappy.params import Command, Parameter
|
||||||
@ -57,6 +57,29 @@ def test_Command():
|
|||||||
'description': 'do some other thing'}
|
'description': 'do some other thing'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_struct_opt():
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
class WrongName(HasAccessibles): # pylint: disable=unused-variable
|
||||||
|
@Command(StructOf(a=IntRange(), b=IntRange()))
|
||||||
|
def cmd(self, a, c):
|
||||||
|
pass
|
||||||
|
class Mod(HasAccessibles):
|
||||||
|
@Command(StructOf(a=IntRange(), b=IntRange()))
|
||||||
|
def cmd(self, a=5, b=5):
|
||||||
|
pass
|
||||||
|
assert Mod.cmd.datatype.argument.optional == ['a', 'b']
|
||||||
|
class Mod2(HasAccessibles):
|
||||||
|
@Command(StructOf(a=IntRange(), b=IntRange()))
|
||||||
|
def cmd(self, a, b=5):
|
||||||
|
pass
|
||||||
|
assert Mod2.cmd.datatype.argument.optional == ['b']
|
||||||
|
class Mod3(HasAccessibles):
|
||||||
|
@Command(StructOf(a=IntRange(), b=IntRange()))
|
||||||
|
def cmd(self, a, b):
|
||||||
|
pass
|
||||||
|
assert Mod3.cmd.datatype.argument.optional == []
|
||||||
|
|
||||||
|
|
||||||
def test_Parameter():
|
def test_Parameter():
|
||||||
class Mod(HasAccessibles):
|
class Mod(HasAccessibles):
|
||||||
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
|
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user