diff --git a/secop/datatypes.py b/secop/datatypes.py index 571c93c..029ba8f 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -955,10 +955,9 @@ class CommandType(DataType): return props def __repr__(self): - argstr = repr(self.argument) if self.argument else '' if self.result is None: - return 'CommandType(%s)' % argstr - return 'CommandType(%s, %s)' % (argstr, repr(self.result)) + return 'CommandType(%s)' % (repr(self.argument) if self.argument else '') + return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result)) def __call__(self, value): """return the validated argument value or raise""" diff --git a/secop/modules.py b/secop/modules.py index d29fd77..5746378 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -322,26 +322,23 @@ class Module(HasAccessibles): # 2) check and apply parameter_properties # specified as '. = ' + # this may also be done on commands: e.g. 'stop.visibility = advanced' for k, v in list(cfgdict.items()): # keep list() as dict may change during iter if '.' in k[1:]: - paramname, propname = k.split('.', 1) + aname, propname = k.split('.', 1) propvalue = cfgdict.pop(k) - paramobj = self.accessibles.get(paramname, None) - # paramobj might also be a command (not sure if this is needed) - if paramobj: - # no longer needed, this conversion is done by DataTypeType.__call__: - # if propname == 'datatype': - # propvalue = get_datatype(propvalue, k) + aobj = self.accessibles.get(aname, None) + if aobj: try: - paramobj.setProperty(propname, propvalue) + aobj.setProperty(propname, propvalue) except KeyError: errors.append("'%s.%s' does not exist" % - (paramname, propname)) + (aname, propname)) except BadValueError as e: errors.append('%s.%s: %s' % - (paramname, propname, str(e))) + (aname, propname, str(e))) else: - errors.append('%r not found' % paramname) + errors.append('%r not found' % aname) # 3) check config for problems: # only accept remaining config items specified in parameters @@ -427,11 +424,11 @@ class Module(HasAccessibles): self.checkProperties() except ConfigError as e: errors.append(str(e)) - for pname, p in self.parameters.items(): + for aname, aobj in self.accessibles.items(): try: - p.checkProperties() - except ConfigError as e: - errors.append('%s: %s' % (pname, e)) + aobj.checkProperties() + except (ConfigError, ProgrammingError) as e: + errors.append('%s: %s' % (aname, e)) if errors: raise ConfigError(errors) diff --git a/secop/params.py b/secop/params.py index 6efebaa..9add5a8 100644 --- a/secop/params.py +++ b/secop/params.py @@ -346,6 +346,9 @@ class Command(Accessible): def __init__(self, argument=False, *, result=None, inherit=True, **kwds): super().__init__() + 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'") self.init(kwds) if result or kwds or isinstance(argument, DataType) or not callable(argument): # normal case @@ -362,7 +365,7 @@ class Command(Accessible): self.func = argument # this is the wrapped method! if argument.__doc__: self.description = inspect.cleandoc(argument.__doc__) - self.name = self.func.__name__ + self.name = self.func.__name__ # this is probably not needed self._inherit = inherit # save for __set_name__ self.ownProperties = self.propertyValues.copy() @@ -397,6 +400,7 @@ class Command(Accessible): """called when used as decorator""" if 'description' not in self.propertyValues and func.__doc__: self.description = inspect.cleandoc(func.__doc__) + self.ownProperties['description'] = self.description self.func = func return self diff --git a/test/test_modules.py b/test/test_modules.py index 66b1b3f..361488a 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -264,9 +264,101 @@ def test_param_inheritance(): Base('o', logger, {'description': ''}, srv) -def test_mixin(): - # srv = ServerStub({}) +def test_command_inheritance(): + class Base(Module): + @Command(BoolType(), visibility=2) + def cmd(self, arg): + """base""" + class Sub1(Base): + @Command(group='grp') + def cmd(self, arg): + """first""" + + class Sub2(Sub1): + @Command(None, result=BoolType()) + def cmd(self): # pylint: disable=arguments-differ + """second""" + + class Sub3(Base): + # when either argument or result is given, the other one is assumed to be None + # i.e. here we override the argument with None + @Command(result=FloatRange()) + def cmd(self, arg): + """third""" + + assert Sub1.accessibles['cmd'].for_export() == { + 'description': 'first', 'group': 'grp', 'visibility': 2, + 'datainfo': {'type': 'command', 'argument': {'type': 'bool'}} + } + + assert Sub2.accessibles['cmd'].for_export() == { + 'description': 'second', 'group': 'grp', 'visibility': 2, + 'datainfo': {'type': 'command', 'result': {'type': 'bool'}} + } + + assert Sub3.accessibles['cmd'].for_export() == { + 'description': 'third', 'visibility': 2, + 'datainfo': {'type': 'command', 'result': {'type': 'double'}} + } + + for cls in locals().values(): + if hasattr(cls, 'accessibles'): + for p in cls.accessibles.values(): + assert isinstance(p.ownProperties, dict) + assert p.copy().ownProperties == {} + + +def test_command_check(): + srv = ServerStub({}) + + class Good(Module): + @Command(description='available') + def with_description(self): + pass + @Command() + def with_docstring(self): + """docstring""" + + Good('o', logger, {'description': ''}, srv) + + class Bad1(Module): + @Command + def without_description(self): + pass + + class Bad2(Module): + @Command() + def without_description(self): + pass + + for cls in Bad1, Bad2: + with pytest.raises(ConfigError) as e_info: + cls('o', logger, {'description': ''}, srv) + assert 'description' in repr(e_info.value) + + class BadDatatype(Module): + @Command(FloatRange(0.1, 0.9), result=FloatRange()) + def cmd(self): + """valid command""" + + BadDatatype('o', logger, {'description': ''}, srv) + + # test for command property checking + with pytest.raises(ProgrammingError): + BadDatatype('o', logger, { + 'description': '', + 'cmd.argument': {'type': 'double', 'min': 1, 'max': 0}, + }, srv) + + with pytest.raises(ProgrammingError): + BadDatatype('o', logger, { + 'description': '', + 'cmd.visibility': 'invalid', + }, srv) + + +def test_mixin(): class Mixin: # no need to inherit from Module or HasAccessible value = Parameter(unit='K') # missing datatype and description acceptable in mixins param1 = Parameter('no datatype yet', fmtstr='%.5f') @@ -319,11 +411,19 @@ def test_override(): assert Mod.value.default == 5 assert Mod.stop.description == "no decorator needed" + class Mod2(Drivable): + @Command() + def stop(self): + pass + + assert Mod2.stop.description == Drivable.stop.description + def test_command_config(): class Mod(Module): @Command(IntRange(0, 1), result=IntRange(0, 1)) def convert(self, value): + """dummy conversion""" return value srv = ServerStub({}) diff --git a/test/test_params.py b/test/test_params.py index 091eb24..bf39577 100644 --- a/test/test_params.py +++ b/test/test_params.py @@ -98,35 +98,14 @@ def test_Override(): Mod.p1.default = False assert repr(Mod.p1) == repr(Base.p1) + for cls in locals().values(): + if hasattr(cls, 'accessibles'): + for p in cls.accessibles.values(): + assert isinstance(p.ownProperties, dict) + assert p.copy().ownProperties == {} + def test_Export(): class Mod(HasAccessibles): param = Parameter('description1', datatype=BoolType, default=False) assert Mod.param.export == '_param' - - -def test_Command_Inheritance(): - class Base(HasAccessibles): - @Command(BoolType(), visibility=2) - def cmd(self, arg): - """first""" - - class Sub(Base): - @Command(group='grp') - def cmd(self, arg): - """second""" - - class Sub2(Base): - @Command(FloatRange()) - def cmd(self, arg): - """third""" - - assert Sub.accessibles['cmd'].for_export() == { - 'description': 'second', 'group': 'grp', 'visibility': 2, - 'datainfo': {'type': 'command', 'argument': {'type': 'bool'}} - } - - assert Sub2.accessibles['cmd'].for_export() == { - 'description': 'third', 'visibility': 2, - 'datainfo': {'type': 'command', 'argument': {'type': 'double'}} - }