diff --git a/murano/dsl/dsl_types.py b/murano/dsl/dsl_types.py index 29f0868b..63b5b828 100644 --- a/murano/dsl/dsl_types.py +++ b/murano/dsl/dsl_types.py @@ -57,6 +57,13 @@ class MethodUsages(object): StaticMethods = {Static, Extension} +class MethodArgumentUsages(object): + Standard = 'Standard' + VarArgs = 'VarArgs' + KwArgs = 'KwArgs' + All = {Standard, VarArgs, KwArgs} + + class MuranoType(object): pass diff --git a/murano/dsl/executor.py b/murano/dsl/executor.py index fba19d5f..b6372bb2 100644 --- a/murano/dsl/executor.py +++ b/murano/dsl/executor.py @@ -207,13 +207,39 @@ class MuranoDslExecutor(object): def _canonize_parameters(arguments_scheme, args, kwargs, method_name, receiver): arg_names = list(arguments_scheme.keys()) - parameter_values = utils.filter_parameters_dict(kwargs) - if len(args) > len(arg_names): - raise yaql_exceptions.NoMatchingMethodException( - method_name, receiver) + parameter_values = {} + varargs_arg = None + vararg_values = [] + kwargs_arg = None + kwarg_values = {} + for name, definition in six.iteritems(arguments_scheme): + if definition.usage == dsl_types.MethodArgumentUsages.VarArgs: + varargs_arg = name + parameter_values[name] = vararg_values + elif definition.usage == dsl_types.MethodArgumentUsages.KwArgs: + kwargs_arg = name + parameter_values[name] = kwarg_values + for i, arg in enumerate(args): - name = arg_names[i] - parameter_values[name] = arg + name = None if i >= len(arg_names) else arg_names[i] + if name is None or name in (varargs_arg, kwargs_arg): + if varargs_arg: + vararg_values.append(arg) + else: + raise yaql_exceptions.NoMatchingMethodException( + method_name, receiver) + else: + parameter_values[name] = arg + + for name, value in six.iteritems(utils.filter_parameters_dict(kwargs)): + if name in arguments_scheme and name not in ( + varargs_arg, kwargs_arg): + parameter_values[name] = value + elif kwargs_arg: + kwarg_values[name] = value + else: + raise yaql_exceptions.NoMatchingMethodException( + method_name, receiver) return tuple(), parameter_values def load(self, data): diff --git a/murano/dsl/murano_method.py b/murano/dsl/murano_method.py index 2c6a413e..4f60152b 100644 --- a/murano/dsl/murano_method.py +++ b/murano/dsl/murano_method.py @@ -85,13 +85,36 @@ class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): arguments_scheme = [{key: value} for key, value in six.iteritems(arguments_scheme)] self._arguments_scheme = collections.OrderedDict() + seen_varargs = False + seen_kwargs = False + args_order_error = False for record in arguments_scheme: - if (not isinstance(record, dict) or - len(record) > 1): - raise ValueError() + if not isinstance(record, dict) or len(record) > 1: + raise exceptions.DslSyntaxError( + 'Invalid arguments declaration') name = list(record.keys())[0] - self._arguments_scheme[name] = MuranoMethodArgument( + argument = MuranoMethodArgument( self, self.name, name, record[name]) + usage = argument.usage + if (usage == dsl_types.MethodArgumentUsages.Standard and + (seen_kwargs or seen_varargs)): + args_order_error = True + elif usage == dsl_types.MethodArgumentUsages.VarArgs: + if seen_kwargs or seen_varargs: + args_order_error = True + seen_varargs = True + elif usage == dsl_types.MethodArgumentUsages.KwArgs: + if seen_kwargs: + args_order_error = True + seen_kwargs = True + + if args_order_error: + raise exceptions.DslSyntaxError( + 'Invalid argument order in method {0}'.format( + self.name)) + else: + self._arguments_scheme[name] = argument + self._meta = meta.MetaData( payload.get('Meta'), dsl_types.MetaTargets.Method, @@ -183,6 +206,14 @@ class MuranoMethodArgument(dsl_types.MuranoMethodArgument, typespec.Spec, self._meta = meta.MetaData( declaration.get('Meta'), dsl_types.MetaTargets.Argument, self.murano_method.declaring_type) + self._usage = declaration.get('Usage') or \ + dsl_types.MethodArgumentUsages.Standard + + if self._usage not in dsl_types.MethodArgumentUsages.All: + raise exceptions.DslSyntaxError( + 'Unknown usage {0}. Must be one of ({1})'.format( + self._usage, ', '.join(dsl_types.MethodArgumentUsages.All) + )) def transform(self, value, this, *args, **kwargs): try: @@ -207,6 +238,10 @@ class MuranoMethodArgument(dsl_types.MuranoMethodArgument, typespec.Spec, def name(self): return self._arg_name + @property + def usage(self): + return self._usage + def get_meta(self, context): executor = helpers.get_executor(context) context = executor.create_type_context( diff --git a/murano/dsl/murano_property.py b/murano/dsl/murano_property.py index 7d5eacd3..5bcf49f0 100644 --- a/murano/dsl/murano_property.py +++ b/murano/dsl/murano_property.py @@ -30,6 +30,11 @@ class MuranoProperty(dsl_types.MuranoProperty, typespec.Spec, super(MuranoProperty, self).__init__(declaration, declaring_type) self._property_name = property_name self._declaring_type = weakref.ref(declaring_type) + self._usage = declaration.get('Usage') or dsl_types.PropertyUsages.In + if self._usage not in dsl_types.PropertyUsages.All: + raise exceptions.DslSyntaxError( + 'Unknown usage {0}. Must be one of ({1})'.format( + self._usage, ', '.join(dsl_types.PropertyUsages.All))) self._meta = meta.MetaData( declaration.get('Meta'), dsl_types.MetaTargets.Property, declaring_type) @@ -53,6 +58,10 @@ class MuranoProperty(dsl_types.MuranoProperty, typespec.Spec, def name(self): return self._property_name + @property + def usage(self): + return self._usage + def get_meta(self, context): def meta_producer(cls): prop = cls.properties.get(self.name) diff --git a/murano/dsl/typespec.py b/murano/dsl/typespec.py index 5e1a4360..464aa0c3 100644 --- a/murano/dsl/typespec.py +++ b/murano/dsl/typespec.py @@ -15,7 +15,6 @@ import weakref from murano.dsl import dsl_types -from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import type_scheme @@ -24,13 +23,8 @@ class Spec(object): def __init__(self, declaration, container_type): self._container_type = weakref.ref(container_type) self._contract = type_scheme.TypeScheme(declaration['Contract']) - self._usage = declaration.get('Usage') or dsl_types.PropertyUsages.In - self._default = declaration.get('Default') self._has_default = 'Default' in declaration - if self._usage not in dsl_types.PropertyUsages.All: - raise exceptions.DslSyntaxError( - 'Unknown type {0}. Must be one of ({1})'.format( - self._usage, ', '.join(dsl_types.PropertyUsages.All))) + self._default = declaration.get('Default') def transform(self, value, this, owner, context, default=None): if default is None: @@ -64,7 +58,3 @@ class Spec(object): @property def has_default(self): return self._has_default - - @property - def usage(self): - return self._usage diff --git a/murano/dsl/yaql_integration.py b/murano/dsl/yaql_integration.py index 1007396f..c7315cd2 100644 --- a/murano/dsl/yaql_integration.py +++ b/murano/dsl/yaql_integration.py @@ -335,21 +335,33 @@ def _create_basic_mpl_stub(murano_method, reserve_params, payload, fd = specs.FunctionDefinition( murano_method.name, payload, is_function=False, is_method=True) - i = 0 - for i, (name, arg_spec) in enumerate( - six.iteritems(murano_method.arguments_scheme), reserve_params + 1): + i = reserve_params + 1 + varargs = False + kwargs = False + for name, arg_spec in six.iteritems(murano_method.arguments_scheme): + position = i + if arg_spec.usage == dsl_types.MethodArgumentUsages.VarArgs: + name = '*' + varargs = True + elif arg_spec.usage == dsl_types.MethodArgumentUsages.KwArgs: + name = '**' + position = None + kwargs = True p = specs.ParameterDefinition( name, ContractedValue(arg_spec, with_check=check_first_arg), - position=i, default=dsl.NO_VALUE) + position=position, default=dsl.NO_VALUE) check_first_arg = False fd.parameters[name] = p + i += 1 - fd.parameters['*'] = specs.ParameterDefinition( - '*', - value_type=yaqltypes.PythonType(object, nullable=True), - position=i) - fd.parameters['**'] = specs.ParameterDefinition( - '**', value_type=yaqltypes.PythonType(object, nullable=True)) + if not varargs: + fd.parameters['*'] = specs.ParameterDefinition( + '*', + value_type=yaqltypes.PythonType(object, nullable=True), + position=i) + if not kwargs: + fd.parameters['**'] = specs.ParameterDefinition( + '**', value_type=yaqltypes.PythonType(object, nullable=True)) fd.set_parameter(specs.ParameterDefinition( '__context', yaqltypes.Context(), 0)) diff --git a/murano/tests/unit/dsl/meta/TestVarKwArgs.yaml b/murano/tests/unit/dsl/meta/TestVarKwArgs.yaml new file mode 100644 index 00000000..213dff2b --- /dev/null +++ b/murano/tests/unit/dsl/meta/TestVarKwArgs.yaml @@ -0,0 +1,65 @@ +Name: TestVarKwArgs + +Methods: + testVarArgs: + Body: + Return: $.varArgsMethod(1, 2, 3, 4) + + testVarArgsContract: + Body: + Return: $.varArgsMethod(1, string) + + testDuplicateVarArgs: + Body: + Return: $.varArgsMethod(1, arg1 => 2) + + testExplicitVarArgs: + Body: + Return: $.varArgsMethod(1, rest => 2) + + varArgsMethod: + Arguments: + - arg1: + Contract: $.int() + - rest: + Contract: $.int() + Usage: VarArgs + Body: + Return: $rest + + testKwArgs: + Body: + Return: $.kwArgsMethod(arg1 => 1, arg2 => 2, arg3 => 3) + + testKwArgsContract: + Body: + Return: $.kwArgsMethod(arg1 => 1, arg2 => string) + + testDuplicateKwArgs: + Body: + Return: $.kwArgsMethod(1, arg1 => 2) + + kwArgsMethod: + Arguments: + - arg1: + Contract: $.int() + - rest: + Contract: $.int() + Usage: KwArgs + Body: + Return: $rest + + testArgs: + Body: + Return: $.argsMethod(1, 2, 3, arg1 => 4, arg2 => 5, arg3 => 6) + + argsMethod: + Arguments: + - args: + Contract: $.int() + Usage: VarArgs + - kwargs: + Contract: $.int() + Usage: KwArgs + Body: + Return: [$args, $kwargs] diff --git a/murano/tests/unit/dsl/test_varkwargs.py b/murano/tests/unit/dsl/test_varkwargs.py new file mode 100644 index 00000000..60ce3d6e --- /dev/null +++ b/murano/tests/unit/dsl/test_varkwargs.py @@ -0,0 +1,62 @@ +# coding: utf-8 +# Copyright (c) 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from yaql.language import exceptions as yaql_exceptions + +from murano.dsl import exceptions as dsl_exceptions +from murano.tests.unit.dsl.foundation import object_model as om +from murano.tests.unit.dsl.foundation import test_case + + +class TestVarKwArgs(test_case.DslTestCase): + def setUp(self): + super(TestVarKwArgs, self).setUp() + self._runner = self.new_runner(om.Object('TestVarKwArgs')) + + def test_varargs(self): + self.assertEqual([2, 3, 4], self._runner.testVarArgs()) + + def test_kwargs(self): + self.assertEqual({'arg2': 2, 'arg3': 3}, self._runner.testKwArgs()) + + def test_duplicate_kwargs(self): + self.assertRaises( + yaql_exceptions.NoMatchingMethodException, + self._runner.testDuplicateKwArgs) + + def test_duplicate_varargs(self): + self.assertRaises( + yaql_exceptions.NoMatchingMethodException, + self._runner.testDuplicateVarArgs) + + def test_explicit_varargs(self): + self.assertRaises( + yaql_exceptions.NoMatchingMethodException, + self._runner.testExplicitVarArgs) + + def test_args(self): + self.assertEqual( + [[1, 2, 3], {'arg1': 4, 'arg2': 5, 'arg3': 6}], + self._runner.testArgs()) + + def test_varargs_contract(self): + self.assertRaises( + dsl_exceptions.ContractViolationException, + self._runner.testVarArgsContract) + + def test_kwargs_contract(self): + self.assertRaises( + dsl_exceptions.ContractViolationException, + self._runner.testKwArgsContract) diff --git a/releasenotes/notes/var-kw-args-c42c31678d8bc747.yaml b/releasenotes/notes/var-kw-args-c42c31678d8bc747.yaml new file mode 100644 index 00000000..d881f60e --- /dev/null +++ b/releasenotes/notes/var-kw-args-c42c31678d8bc747.yaml @@ -0,0 +1,11 @@ +--- +features: + - > + It is possible now to declare MuranoPL YAML methods with variable length + positional and keyword arguments. This is done using argument Usage + attribute. Regular arguments have Standard usage which is the default. + Variable length args (args in Python) should have "Usage: VarArgs" and + keyword args (kwargs) are declared with "Usage: KwArgs". Inside the + method they seen as a list and a dictionary correspondingly. For such + arguments contracts are written for individual argument values thus + no need to write them as lists/dicts.