From eeea754181db72194b36b5d2b30b370bbc48d0be Mon Sep 17 00:00:00 2001 From: Alexander Zaft Date: Wed, 8 Nov 2023 15:08:30 +0100 Subject: [PATCH] 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 Reviewed-by: Alexander Zaft --- frappy/params.py | 35 ++++++++++++++++++++++++++++++----- frappy/protocol/dispatcher.py | 5 ++--- test/test_params.py | 25 ++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/frappy/params.py b/frappy/params.py index 08b4485..6eab9a7 100644 --- a/frappy/params.py +++ b/frappy/params.py @@ -25,12 +25,12 @@ import inspect -from frappy.datatypes import BoolType, CommandType, DataType, \ - DataTypeType, EnumType, NoneOr, OrType, FloatRange, \ - StringType, StructOf, TextType, TupleOf, ValueType, ArrayOf -from frappy.errors import BadValueError, WrongTypeError, ProgrammingError -from frappy.properties import HasProperties, Property +from frappy.datatypes import ArrayOf, BoolType, CommandType, DataType, \ + DataTypeType, EnumType, FloatRange, NoneOr, OrType, StringType, StructOf, \ + TextType, TupleOf, ValueType +from frappy.errors import BadValueError, ProgrammingError, WrongTypeError from frappy.lib import generalConfig +from frappy.properties import HasProperties, Property generalConfig.set_default('tolerate_poll_property', False) generalConfig.set_default('omit_unchanged_within', 0.1) @@ -428,6 +428,18 @@ class Command(Accessible): def __call__(self, func): """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__: self.description = inspect.cleandoc(func.__doc__) self.ownProperties['description'] = self.description @@ -498,6 +510,19 @@ class Command(Accessible): # pylint: disable=unnecessary-dunder-call func = self.__get__(module_obj) 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): res = func(*argument) elif isinstance(self.argument, StructOf): diff --git a/frappy/protocol/dispatcher.py b/frappy/protocol/dispatcher.py index 85f7449..31a07fa 100644 --- a/frappy/protocol/dispatcher.py +++ b/frappy/protocol/dispatcher.py @@ -132,6 +132,8 @@ class Dispatcher: self.reset_connection(conn) 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) if moduleobj is None: raise NoSuchModuleError(f'Module {modulename!r} does not exist') @@ -140,9 +142,6 @@ class Dispatcher: cobj = moduleobj.commands.get(cname) if cobj is None: 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 # note: exceptions are handled in handle_request, not here! result = cobj.do(moduleobj, argument) diff --git a/test/test_params.py b/test/test_params.py index 7d15033..a3e3bf9 100644 --- a/test/test_params.py +++ b/test/test_params.py @@ -25,7 +25,7 @@ # no fixtures needed import pytest -from frappy.datatypes import BoolType, FloatRange, IntRange +from frappy.datatypes import BoolType, FloatRange, IntRange, StructOf from frappy.errors import ProgrammingError from frappy.modules import HasAccessibles from frappy.params import Command, Parameter @@ -57,6 +57,29 @@ def test_Command(): '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(): class Mod(HasAccessibles): p1 = Parameter('desc1', datatype=FloatRange(), default=0)