diff --git a/murano/dsl/constants.py b/murano/dsl/constants.py index 98e97b066..079adfa25 100644 --- a/murano/dsl/constants.py +++ b/murano/dsl/constants.py @@ -27,6 +27,7 @@ CTX_CURRENT_EXCEPTION = '$?currentException' CTX_CURRENT_METHOD = '$?currentMethod' CTX_EXECUTOR = '$?executor' CTX_EXECUTION_SESSION = '$?executionSession' +CTX_NAMES_SCOPE = '$?namesScope' CTX_ORIGINAL_CONTEXT = '$?originalContext' CTX_PACKAGE_LOADER = '$?packageLoader' CTX_SKIP_FRAME = '$?skipFrame' diff --git a/murano/dsl/dsl.py b/murano/dsl/dsl.py index c8d091253..c53fcd5b7 100644 --- a/murano/dsl/dsl.py +++ b/murano/dsl/dsl.py @@ -108,9 +108,11 @@ class InterfacesParameter(yaqltypes.HiddenParameterType, class MuranoTypeParameter(yaqltypes.PythonType): - def __init__(self, base_type=None, nullable=False, context=None): + def __init__(self, base_type=None, nullable=False, context=None, + resolve_strings=True): self._context = context self._base_type = base_type + self._resolve_strings = resolve_strings super(MuranoTypeParameter, self).__init__( (dsl_types.MuranoTypeReference, six.string_types), nullable) @@ -119,6 +121,10 @@ class MuranoTypeParameter(yaqltypes.PythonType): if not super(MuranoTypeParameter, self).check( value, context, *args, **kwargs): return False + if isinstance(value, six.string_types): + if not self._resolve_strings: + return False + value = helpers.get_class(value, context).get_reference() if isinstance(value, dsl_types.MuranoTypeReference): if not self._base_type: return True @@ -133,12 +139,7 @@ class MuranoTypeParameter(yaqltypes.PythonType): value = super(MuranoTypeParameter, self).convert( value, sender, context, function_spec, engine) if isinstance(value, six.string_types): - if function_spec.meta.get(constants.META_MURANO_METHOD): - context = helpers.get_caller_context(context) - murano_type = helpers.get_type(context) - value = helpers.get_class( - murano_type.namespace_resolver.resolve_name(value), - context).get_reference() + value = helpers.get_class(value, context).get_reference() if self._base_type and not self._base_type.is_compatible(value): raise ValueError('Value must be subtype of {0}'.format( self._base_type.name diff --git a/murano/dsl/dsl_types.py b/murano/dsl/dsl_types.py index 97d3b778d..b59259ce9 100644 --- a/murano/dsl/dsl_types.py +++ b/murano/dsl/dsl_types.py @@ -50,7 +50,11 @@ class MethodUsages(object): Action = 'Action' Runtime = 'Runtime' Static = 'Static' - All = {Action, Runtime, Static} + Extension = 'Extension' + + All = {Action, Runtime, Static, Extension} + InstanceMethods = {Runtime, Action} + StaticMethods = {Static, Extension} class MuranoType(object): diff --git a/murano/dsl/executor.py b/murano/dsl/executor.py index a78c28a22..d4a7827e0 100644 --- a/murano/dsl/executor.py +++ b/murano/dsl/executor.py @@ -82,8 +82,16 @@ class MuranoDslExecutor(object): self.create_object_context(this, context), method) method_context[constants.CTX_SKIP_FRAME] = True method_context[constants.CTX_ACTIONS_ONLY] = actions_only - return method.yaql_function_definition( - yaql_engine, method_context, this.real_this)(*args, **kwargs) + + stub = method.static_stub if isinstance( + this, dsl_types.MuranoType) else method.instance_stub + if stub is None: + raise ValueError( + 'Method {0} cannot be called on receiver {1}'.format( + method, this)) + + return stub(yaql_engine, method_context, this.real_this)( + *args, **kwargs) if (context[constants.CTX_ACTIONS_ONLY] and method.usage != dsl_types.MethodUsages.Action): @@ -119,6 +127,8 @@ class MuranoDslExecutor(object): return method.body( yaql_engine, context, native_this)(*args, **kwargs) else: + context[constants.CTX_NAMES_SCOPE] = \ + method.declaring_type return (None if method.body is None else method.body.execute(context)) @@ -312,9 +322,13 @@ class MuranoDslExecutor(object): caller = caller_context while caller is not None and caller[constants.CTX_SKIP_FRAME]: caller = caller[constants.CTX_CALLER_CONTEXT] + context[constants.CTX_NAMES_SCOPE] = caller_context[ + constants.CTX_NAMES_SCOPE] context[constants.CTX_CALLER_CONTEXT] = caller context[constants.CTX_ALLOW_PROPERTY_WRITES] = caller_context[ constants.CTX_ALLOW_PROPERTY_WRITES] + else: + context[constants.CTX_NAMES_SCOPE] = obj_type return context @staticmethod diff --git a/murano/dsl/helpers.py b/murano/dsl/helpers.py index d59b64071..902843deb 100644 --- a/murano/dsl/helpers.py +++ b/murano/dsl/helpers.py @@ -216,10 +216,16 @@ def are_property_modifications_allowed(context=None): return context[constants.CTX_ALLOW_PROPERTY_WRITES] or False +def get_names_scope(context=None): + context = context or get_context() + return context[constants.CTX_NAMES_SCOPE] + + def get_class(name, context=None): context = context or get_context() - murano_class = get_type(context) - return murano_class.package.find_class(name) + murano_type = get_names_scope(context) + name = murano_type.namespace_resolver.resolve_name(name) + return murano_type.package.find_class(name) def get_current_thread_id(): @@ -538,3 +544,11 @@ def function(c): if hasattr(c, 'im_func'): return c.im_func return c + + +def list_value(v): + if v is None: + return [] + if not yaqlutils.is_sequence(v): + v = [v] + return v diff --git a/murano/dsl/lhs_expression.py b/murano/dsl/lhs_expression.py index 6ccf47a89..04a4d6fe5 100644 --- a/murano/dsl/lhs_expression.py +++ b/murano/dsl/lhs_expression.py @@ -169,7 +169,8 @@ class LhsExpression(object): def __call__(self, value, context): new_context = self._create_context(context) new_context[''] = context['$'] - new_context[constants.CTX_TYPE] = context[constants.CTX_TYPE] + for name in (constants.CTX_NAMES_SCOPE,): + new_context[name] = context[name] self._current_obj = None self._current_obj_name = None property = self._expression(context=new_context) diff --git a/murano/dsl/macros.py b/murano/dsl/macros.py index 3044ae205..02b8f8a1a 100644 --- a/murano/dsl/macros.py +++ b/murano/dsl/macros.py @@ -25,8 +25,7 @@ from murano.dsl import yaql_expression class CodeBlock(expressions.DslExpression): def __init__(self, body): - if not isinstance(body, list): - body = [body] + body = helpers.list_value(body) self.code_block = list(map(expressions.parse_expression, body)) def execute(self, context): diff --git a/murano/dsl/meta.py b/murano/dsl/meta.py index f61b66f77..433590e97 100644 --- a/murano/dsl/meta.py +++ b/murano/dsl/meta.py @@ -32,10 +32,7 @@ class MetaProvider(object): class MetaData(MetaProvider): def __init__(self, definition, target, scope_type): scope_type = weakref.ref(scope_type) - if not definition: - definition = [] - elif not isinstance(definition, list): - definition = [definition] + definition = helpers.list_value(definition) factories = [] used_types = set() for d in definition: diff --git a/murano/dsl/murano_method.py b/murano/dsl/murano_method.py index 98951e1ce..3d58edd65 100644 --- a/murano/dsl/murano_method.py +++ b/murano/dsl/murano_method.py @@ -50,14 +50,21 @@ class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): payload, weakref.proxy(self), original_name) self._arguments_scheme = None if any(( - helpers.inspect_is_static( - declaring_type.extension_class, original_name), - helpers.inspect_is_classmethod( - declaring_type.extension_class, original_name))): - self._usage = dsl_types.MethodUsages.Static + helpers.inspect_is_static( + declaring_type.extension_class, original_name), + helpers.inspect_is_classmethod( + declaring_type.extension_class, original_name))): + self._usage = self._body.meta.get( + constants.META_USAGE, dsl_types.MethodUsages.Static) + if self._usage not in dsl_types.MethodUsages.StaticMethods: + raise ValueError( + 'Invalid Usage for static method ' + self.name) else: self._usage = (self._body.meta.get(constants.META_USAGE) or dsl_types.MethodUsages.Runtime) + if self._usage not in dsl_types.MethodUsages.InstanceMethods: + raise ValueError( + 'Invalid Usage for instance method ' + self.name) if (self._body.name.startswith('#') or self._body.name.startswith('*')): raise ValueError( @@ -68,10 +75,10 @@ class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): declaring_type) else: payload = payload or {} - self._body = macros.MethodBlock(payload.get('Body') or [], name) + self._body = macros.MethodBlock(payload.get('Body'), name) self._usage = payload.get( 'Usage') or dsl_types.MethodUsages.Runtime - arguments_scheme = payload.get('Arguments') or [] + arguments_scheme = helpers.list_value(payload.get('Arguments')) if isinstance(arguments_scheme, dict): arguments_scheme = [{key: value} for key, value in six.iteritems(arguments_scheme)] @@ -87,8 +94,9 @@ class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): payload.get('Meta'), dsl_types.MetaTargets.Method, declaring_type) - self._yaql_function_definition = \ - yaql_integration.build_wrapper_function_definition( + + self._instance_stub, self._static_stub = \ + yaql_integration.build_stub_function_definitions( weakref.proxy(self)) @property @@ -104,8 +112,12 @@ class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): return self._arguments_scheme @property - def yaql_function_definition(self): - return self._yaql_function_definition + def instance_stub(self): + return self._instance_stub + + @property + def static_stub(self): + return self._static_stub @property def usage(self): @@ -121,7 +133,7 @@ class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): @property def is_static(self): - return self.usage == dsl_types.MethodUsages.Static + return self.usage in dsl_types.MethodUsages.StaticMethods def get_meta(self, context): def meta_producer(cls): @@ -168,9 +180,12 @@ class MuranoMethodArgument(dsl_types.MuranoMethodArgument, typespec.Spec, declaration.get('Meta'), dsl_types.MetaTargets.Argument, self.murano_method.declaring_type) - def validate(self, *args, **kwargs): + def transform(self, value, this, *args, **kwargs): try: - return super(MuranoMethodArgument, self).validate(*args, **kwargs) + if self.murano_method.usage == dsl_types.MethodUsages.Extension: + this = self.murano_method.declaring_type + return super(MuranoMethodArgument, self).transform( + value, this, *args, **kwargs) except exceptions.ContractViolationException as e: msg = u'[{0}::{1}({2}{3})] {4}'.format( self.murano_method.declaring_type.name, diff --git a/murano/dsl/murano_object.py b/murano/dsl/murano_object.py index 16bec10a0..cbe645756 100644 --- a/murano/dsl/murano_object.py +++ b/murano/dsl/murano_object.py @@ -232,7 +232,7 @@ class MuranoObject(dsl_types.MuranoObject): # default = helpers.evaluate(default, context) obj = self.cast(spec.declaring_type) - values_to_assign.append((obj, spec.validate( + values_to_assign.append((obj, spec.transform( value, self.real_this, self.real_this, context, default=default))) for obj, value in values_to_assign: diff --git a/murano/dsl/murano_package.py b/murano/dsl/murano_package.py index ec391854a..5520e3165 100644 --- a/murano/dsl/murano_package.py +++ b/murano/dsl/murano_package.py @@ -98,12 +98,11 @@ class MuranoPackage(dsl_types.MuranoPackage, dslmeta.MetaProvider): return type_obj if callable(data): data = data() - if not utils.is_sequence(data): - data = [data] + data = helpers.list_value(data) unnamed_class = None last_ns = {} for cls_data in data: - last_ns = cls_data.setdefault('Namespaces', last_ns) + last_ns = cls_data.setdefault('Namespaces', last_ns.copy()) if len(cls_data) == 1: continue cls_name = cls_data.get('Name') diff --git a/murano/dsl/murano_property.py b/murano/dsl/murano_property.py index cb7fa6f40..6ea321b82 100644 --- a/murano/dsl/murano_property.py +++ b/murano/dsl/murano_property.py @@ -34,9 +34,9 @@ class MuranoProperty(dsl_types.MuranoProperty, typespec.Spec, dsl_types.MetaTargets.Property, declaring_type) self._meta_values = None - def validate(self, *args, **kwargs): + def transform(self, *args, **kwargs): try: - return super(MuranoProperty, self).validate(*args, **kwargs) + return super(MuranoProperty, self).transform(*args, **kwargs) except exceptions.ContractViolationException as e: msg = u'[{0}.{1}{2}] {3}'.format( self.declaring_type.name, self.name, e.path, six.text_type(e)) diff --git a/murano/dsl/murano_type.py b/murano/dsl/murano_type.py index da3246513..485e02a4a 100644 --- a/murano/dsl/murano_type.py +++ b/murano/dsl/murano_type.py @@ -51,7 +51,6 @@ class MuranoType(dsl_types.MuranoType): return self._namespace_resolver @abc.abstractproperty - @property def usage(self): raise NotImplementedError() @@ -66,7 +65,8 @@ class MuranoType(dsl_types.MuranoType): class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): _allowed_usages = {dsl_types.ClassUsages.Class} - def __init__(self, ns_resolver, name, package, parents, meta=None): + def __init__(self, ns_resolver, name, package, parents, meta=None, + imports=None): super(MuranoClass, self).__init__(ns_resolver, name, package) self._methods = {} self._properties = {} @@ -84,10 +84,12 @@ class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): u'Type {0} cannot have parent with Usage {1}'.format( self.name, p.usage)) self._context = None + self._exported_context = None self._parent_mappings = self._build_parent_remappings() self._property_values = {} self._meta = dslmeta.MetaData(meta, dsl_types.MetaTargets.Type, self) self._meta_values = None + self._imports = list(self._resolve_imports(imports)) @property def usage(self): @@ -126,6 +128,7 @@ class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): method = murano_method.MuranoMethod(self, name, payload, original_name) self._methods[name] = method self._context = None + self._exported_context = None return method @property @@ -155,10 +158,22 @@ class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): leaf = False queue.append((p, path + segment)) if leaf: - path = path + segment + path += segment if path: yield path + def _resolve_imports(self, imports): + seen = {self.name} + for imp in helpers.list_value(imports): + if imp in seen: + continue + type = helpers.resolve_type(imp, self) + if type in seen: + continue + seen.add(imp) + seen.add(type) + yield type + def _choose_symbol(self, func): chains = sorted( self._find_symbol_chains(func, self), @@ -366,23 +381,51 @@ class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): @property def context(self): if not self._context: - self._context = yaql_integration.create_empty_context() + ctx = None + for imp in reversed(self._imports): + if ctx is None: + ctx = imp.exported_context + else: + ctx = helpers.link_contexts(ctx, imp.exported_context) + + if ctx is None: + self._context = yaql_integration.create_empty_context() + else: + self._context = ctx.create_child_context() + for m in self._iterate_unique_methods(): - self._context.register_function( - m.yaql_function_definition, - name=m.yaql_function_definition.name) + if m.instance_stub: + self._context.register_function( + m.instance_stub, name=m.instance_stub.name) + if m.static_stub: + self._context.register_function( + m.static_stub, name=m.static_stub.name) return self._context + @property + def exported_context(self): + if not self._exported_context: + self._exported_context = yaql_integration.create_empty_context() + for m in self._iterate_unique_methods(): + if m.usage == dsl_types.MethodUsages.Extension: + if m.instance_stub: + self._exported_context.register_function( + m.instance_stub, name=m.instance_stub.name) + if m.static_stub: + self._exported_context.register_function( + m.static_stub, name=m.static_stub.name) + return self._exported_context + def get_property(self, name, context): prop = self.find_static_property(name) cls = prop.declaring_type value = cls._property_values.get(name, prop.default) - return prop.validate(value, cls, None, context) + return prop.transform(value, cls, None, context) def set_property(self, name, value, context): prop = self.find_static_property(name) cls = prop.declaring_type - cls._property_values[name] = prop.validate(value, cls, None, context) + cls._property_values[name] = prop.transform(value, cls, None, context) def get_meta(self, context): if self._meta_values is None: @@ -394,9 +437,10 @@ class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): class MuranoMetaClass(dsl_types.MuranoMetaClass, MuranoClass): _allowed_usages = {dsl_types.ClassUsages.Meta, dsl_types.ClassUsages.Class} - def __init__(self, ns_resolver, name, package, parents, meta=None): + def __init__(self, ns_resolver, name, package, parents, meta=None, + imports=None): super(MuranoMetaClass, self).__init__( - ns_resolver, name, package, parents, meta) + ns_resolver, name, package, parents, meta, imports) self._cardinality = dsl_types.MetaCardinality.One self._targets = list(dsl_types.MetaCardinality.All) self._inherited = False @@ -456,7 +500,7 @@ def _create_class(cls, name, ns_resolver, data, package, *args, **kwargs): type_obj = cls( ns_resolver, name, package, parent_classes, data.get('Meta'), - *args, **kwargs) + data.get('Import'), *args, **kwargs) properties = data.get('Properties') or {} for property_name, property_spec in six.iteritems(properties): diff --git a/murano/dsl/type_scheme.py b/murano/dsl/type_scheme.py index 0ec3cc74d..41bcee14c 100644 --- a/murano/dsl/type_scheme.py +++ b/murano/dsl/type_scheme.py @@ -32,7 +32,8 @@ class TypeScheme(object): self._spec = spec @staticmethod - def prepare_context(root_context, this, owner, default, calling_type): + def prepare_transform_context(root_context, this, owner, default, + calling_type): @specs.parameter('value', nullable=True) @specs.method def int_(value): @@ -150,7 +151,7 @@ class TypeScheme(object): @specs.parameter('version_spec', yaqltypes.String(True)) @specs.method def class_(value, name, default_name=None, version_spec=None): - object_store = this.object_store + object_store = None if this is None else this.object_store if not default_name: default_name = name murano_class = name.type @@ -164,7 +165,7 @@ class TypeScheme(object): obj = helpers.instantiate( value, owner, object_store, root_context, calling_type, default_name, default) - elif isinstance(value, six.string_types): + elif isinstance(value, six.string_types) and object_store: obj = object_store.get(value) if obj is None: if not object_store.initializing: @@ -197,6 +198,66 @@ class TypeScheme(object): context.register_function(not_owned) return context + @staticmethod + def prepare_validate_context(root_context): + @specs.parameter('value', nullable=True) + @specs.method + def int_(value): + if value is None or isinstance( + value, int) and not isinstance(value, bool): + return value + raise exceptions.ContractViolationException() + + @specs.parameter('value', nullable=True) + @specs.method + def string(value): + if value is None or isinstance(value, six.string_types): + return value + raise exceptions.ContractViolationException() + + @specs.parameter('value', nullable=True) + @specs.method + def bool_(value): + if value is None or isinstance(value, bool): + return value + raise exceptions.ContractViolationException() + + @specs.parameter('value', nullable=True) + @specs.method + def not_null(value): + if value is None: + raise exceptions.ContractViolationException() + return value + + @specs.parameter('value', nullable=True) + @specs.parameter('predicate', yaqltypes.Lambda(with_context=True)) + @specs.method + def check(value, predicate): + if predicate(root_context.create_child_context(), value): + return value + raise exceptions.ContractViolationException() + + @specs.parameter('type', dsl.MuranoTypeParameter( + nullable=False, context=root_context)) + @specs.parameter('value', nullable=True) + @specs.parameter('version_spec', yaqltypes.String(True)) + @specs.method + def class_(value, type, version_spec=None): + if helpers.is_instance_of( + value, type.type.name, + version_spec or helpers.get_names_scope(root_context)): + return value + raise exceptions.ContractViolationException() + + context = root_context.create_child_context() + context.register_function(int_) + context.register_function(string) + context.register_function(bool_) + context.register_function(check) + context.register_function(not_null) + context.register_function(class_) + return context + def _map_dict(self, data, spec, context, path): if data is None or data is dsl.NO_VALUE: data = {} @@ -298,7 +359,7 @@ class TypeScheme(object): else: return self._map_scalar(data, spec) - def __call__(self, data, context, this, owner, default, calling_type): + def transform(self, data, context, this, owner, default, calling_type): # TODO(ativelkov, slagun): temporary fix, need a better way of handling # composite defaults # A bug (#1313694) has been filed @@ -306,10 +367,21 @@ class TypeScheme(object): if data is dsl.NO_VALUE: data = helpers.evaluate(default, context) - context = self.prepare_context( + context = self.prepare_transform_context( context, this, owner, default, calling_type) return self._map(data, self._spec, context, '') + def validate(self, data, context, default): + if data is dsl.NO_VALUE: + data = helpers.evaluate(default, context) + + context = self.prepare_validate_context(context) + try: + self._map(data, self._spec, context, '') + return True + except exceptions.ContractViolationException: + return False + def format_scalar(value): if isinstance(value, six.string_types): diff --git a/murano/dsl/typespec.py b/murano/dsl/typespec.py index 373b8c56c..b6707e25b 100644 --- a/murano/dsl/typespec.py +++ b/murano/dsl/typespec.py @@ -32,20 +32,25 @@ class Spec(object): 'Unknown type {0}. Must be one of ({1})'.format( self._usage, ', '.join(dsl_types.PropertyUsages.All))) - def validate(self, value, this, owner, context, default=None): + def transform(self, value, this, owner, context, default=None): if default is None: default = self.default executor = helpers.get_executor(context) if isinstance(this, dsl_types.MuranoType): - return self._contract( + return self._contract.transform( value, executor.create_object_context(this), None, None, default, helpers.get_type(context)) else: - return self._contract( + return self._contract.transform( value, executor.create_object_context( this.cast(self._container_type())), this, owner, default, helpers.get_type(context)) + def validate(self, value, context, default=None): + if default is None: + default = self.default + return self._contract.validate(value, context, default) + @property def default(self): return self._default diff --git a/murano/dsl/yaql_functions.py b/murano/dsl/yaql_functions.py index 933a94ef7..d2846876f 100644 --- a/murano/dsl/yaql_functions.py +++ b/murano/dsl/yaql_functions.py @@ -183,10 +183,7 @@ def op_dot_static(context, receiver, expr, operator): @specs.parameter('name', yaqltypes.Keyword()) @specs.name('#operator_:') def ns_resolve(context, prefix, name): - murano_type = helpers.get_type(context) - return helpers.get_class( - murano_type.namespace_resolver.resolve_name( - prefix + ':' + name), context).get_reference() + return helpers.get_class(prefix + ':' + name, context).get_reference() @specs.parameter('name', yaqltypes.Keyword()) diff --git a/murano/dsl/yaql_integration.py b/murano/dsl/yaql_integration.py index e7ff0ae2c..d9038fb71 100644 --- a/murano/dsl/yaql_integration.py +++ b/murano/dsl/yaql_integration.py @@ -26,6 +26,7 @@ from yaql import legacy from murano.dsl import constants from murano.dsl import dsl +from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import yaql_functions @@ -79,16 +80,21 @@ ROOT_CONTEXT_12 = yaql.create_context( class ContractedValue(yaqltypes.GenericType): - def __init__(self, value_spec): - self._value_spec = value_spec - self._last_result = False + def __init__(self, value_spec, with_check=False): + def converter(value, receiver, context, *args, **kwargs): + if isinstance(receiver, dsl_types.MuranoObject): + this = receiver.real_this + else: + this = receiver + return value_spec.transform( + value, this, context[constants.CTX_ARGUMENT_OWNER], + context) + + def checker(value, context, *args, **kwargs): + return value_spec.validate(value, context) super(ContractedValue, self).__init__( - True, None, - lambda value, receiver, context, *args, **kwargs: - self._value_spec.validate( - value, receiver.real_this, - context[constants.CTX_ARGUMENT_OWNER], context)) + True, checker if with_check else None, converter) def convert(self, value, *args, **kwargs): if value is None: @@ -179,10 +185,14 @@ def get_function_definition(func, murano_method, original_name): def payload(__context, __self, *args, **kwargs): with helpers.contextual(__context): + __context[constants.CTX_NAMES_SCOPE] = \ + murano_method.declaring_type return body(__self.extension, *args, **kwargs) def static_payload(__context, __receiver, *args, **kwargs): with helpers.contextual(__context): + __context[constants.CTX_NAMES_SCOPE] = \ + murano_method.declaring_type return body(*args, **kwargs) if is_static: @@ -211,20 +221,22 @@ def _remove_first_parameter(fd): p.position -= 1 -def build_wrapper_function_definition(murano_method): +def build_stub_function_definitions(murano_method): if isinstance(murano_method.body, specs.FunctionDefinition): - return _build_native_wrapper_function_definition(murano_method) + return _build_native_stub_function_definitions(murano_method) else: - return _build_mpl_wrapper_function_definition(murano_method) + return _build_mpl_stub_function_definitions(murano_method) -def _build_native_wrapper_function_definition(murano_method): +def _build_native_stub_function_definitions(murano_method): runtime_version = murano_method.declaring_type.package.runtime_version engine = choose_yaql_engine(runtime_version) @specs.method @specs.name(murano_method.name) @specs.meta(constants.META_MURANO_METHOD, murano_method) + @specs.parameter('__receiver', yaqltypes.NotOfType( + dsl_types.MuranoTypeReference)) def payload(__context, __receiver, *args, **kwargs): executor = helpers.get_executor(__context) args = tuple(dsl.to_mutable(arg, engine) for arg in args) @@ -232,35 +244,106 @@ def _build_native_wrapper_function_definition(murano_method): return helpers.evaluate(murano_method.invoke( executor, __receiver, args, kwargs, __context, True), __context) - return specs.get_function_definition(payload) + @specs.method + @specs.name(murano_method.name) + @specs.meta(constants.META_MURANO_METHOD, murano_method) + @specs.parameter('__receiver', yaqltypes.NotOfType( + dsl_types.MuranoTypeReference)) + def extension_payload(__context, __receiver, *args, **kwargs): + executor = helpers.get_executor(__context) + args = tuple(dsl.to_mutable(arg, engine) for arg in args) + kwargs = dsl.to_mutable(kwargs, engine) + return helpers.evaluate(murano_method.invoke( + executor, murano_method.declaring_type, + (__receiver,) + args, kwargs, __context, True), __context) + + @specs.method + @specs.name(murano_method.name) + @specs.meta(constants.META_MURANO_METHOD, murano_method) + @specs.parameter('__receiver', dsl_types.MuranoTypeReference) + def static_payload(__context, __receiver, *args, **kwargs): + executor = helpers.get_executor(__context) + args = tuple(dsl.to_mutable(arg, engine) for arg in args) + kwargs = dsl.to_mutable(kwargs, engine) + return helpers.evaluate(murano_method.invoke( + executor, __receiver, args, kwargs, __context, True), __context) + + if murano_method.usage in dsl_types.MethodUsages.InstanceMethods: + return specs.get_function_definition(payload), None + elif murano_method.usage == dsl_types.MethodUsages.Static: + return (specs.get_function_definition(payload), + specs.get_function_definition(static_payload)) + elif murano_method.usage == dsl_types.MethodUsages.Extension: + return (specs.get_function_definition(extension_payload), + specs.get_function_definition(static_payload)) + else: + raise ValueError('Unknown method usage ' + murano_method.usage) -def _build_mpl_wrapper_function_definition(murano_method): +def _build_mpl_stub_function_definitions(murano_method): + if murano_method.usage in dsl_types.MethodUsages.InstanceMethods: + return _create_instance_mpl_stub(murano_method), None + elif murano_method.usage == dsl_types.MethodUsages.Static: + return (_create_instance_mpl_stub(murano_method), + _create_static_mpl_stub(murano_method)) + elif murano_method.usage == dsl_types.MethodUsages.Extension: + return (_create_extension_mpl_stub(murano_method), + _create_static_mpl_stub(murano_method)) + else: + raise ValueError('Unknown method usage ' + murano_method.usage) + + +def _create_instance_mpl_stub(murano_method): def payload(__context, __receiver, *args, **kwargs): executor = helpers.get_executor(__context) return murano_method.invoke( executor, __receiver, args, kwargs, __context, True) + fd = _create_basic_mpl_stub(murano_method, 1, payload, False) + receiver_type = dsl.MuranoObjectParameter( + weakref.proxy(murano_method.declaring_type), decorate=False) + fd.set_parameter(specs.ParameterDefinition('__receiver', receiver_type, 1)) + return fd + + +def _create_static_mpl_stub(murano_method): + def payload(__context, __receiver, *args, **kwargs): + executor = helpers.get_executor(__context) + return murano_method.invoke( + executor, __receiver, args, kwargs, __context, True) + fd = _create_basic_mpl_stub(murano_method, 1, payload, False) + + receiver_type = dsl.MuranoTypeParameter( + weakref.proxy(murano_method.declaring_type), resolve_strings=False) + fd.set_parameter(specs.ParameterDefinition('__receiver', receiver_type, 1)) + return fd + + +def _create_extension_mpl_stub(murano_method): + def payload(__context, __receiver, *args, **kwargs): + executor = helpers.get_executor(__context) + return murano_method.invoke( + executor, murano_method.declaring_type, + (__receiver,) + args, kwargs, __context, True) + return _create_basic_mpl_stub(murano_method, 0, payload, True) + + +def _create_basic_mpl_stub(murano_method, reserve_params, payload, + check_first_arg): fd = specs.FunctionDefinition( murano_method.name, payload, is_function=False, is_method=True) for i, (name, arg_spec) in enumerate( - six.iteritems(murano_method.arguments_scheme), 2): + six.iteritems(murano_method.arguments_scheme), reserve_params + 1): p = specs.ParameterDefinition( - name, ContractedValue(arg_spec), + name, ContractedValue(arg_spec, with_check=check_first_arg), position=i, default=dsl.NO_VALUE) + check_first_arg = False fd.parameters[name] = p fd.set_parameter(specs.ParameterDefinition( '__context', yaqltypes.Context(), 0)) - receiver_type = dsl.MuranoObjectParameter( - weakref.proxy(murano_method.declaring_type), decorate=False) - if murano_method.is_static: - receiver_type = yaqltypes.AnyOf(dsl.MuranoTypeParameter( - weakref.proxy(murano_method.declaring_type)), receiver_type) - fd.set_parameter(specs.ParameterDefinition('__receiver', receiver_type, 1)) - fd.meta[constants.META_MURANO_METHOD] = murano_method return fd @@ -273,6 +356,8 @@ def get_class_factory_definition(cls, murano_class): args = tuple(dsl.to_mutable(arg, engine) for arg in args) kwargs = dsl.to_mutable(kwargs, engine) with helpers.contextual(__context): + __context[constants.CTX_NAMES_SCOPE] = \ + murano_class return helpers.evaluate(cls(*args, **kwargs), __context) if '__init__' in cls.__dict__: diff --git a/murano/engine/mock_context_manager.py b/murano/engine/mock_context_manager.py index 9144728ee..9fd5b4d13 100644 --- a/murano/engine/mock_context_manager.py +++ b/murano/engine/mock_context_manager.py @@ -111,7 +111,7 @@ def inject_method_with_str(context, target, target_method, original_class = target.type original_function = original_class.find_single_method(target_method) - result_fd = original_function.yaql_function_definition.clone() + result_fd = original_function.instance_stub.clone() def payload_adapter(__context, __sender, *args, **kwargs): executor = helpers.get_executor(__context) @@ -134,7 +134,7 @@ def inject_method_with_yaql_expr(context, target, target_method, expr): original_class = target.type original_function = original_class.find_single_method(target_method) - result_fd = original_function.yaql_function_definition.clone() + result_fd = original_function.instance_stub.clone() def payload_adapter(__super, __context, __sender, *args, **kwargs): new_context = context.create_child_context() diff --git a/murano/tests/unit/dsl/foundation/test_package_loader.py b/murano/tests/unit/dsl/foundation/test_package_loader.py index 472d87a41..ef9dc0ea3 100644 --- a/murano/tests/unit/dsl/foundation/test_package_loader.py +++ b/murano/tests/unit/dsl/foundation/test_package_loader.py @@ -93,7 +93,7 @@ class TestPackageLoader(package_loader.MuranoPackageLoader): last_ns = {} for data in data_lst: - last_ns = data.get('Namespaces', last_ns) + last_ns = data.get('Namespaces', last_ns.copy()) if 'Name' not in data: continue diff --git a/murano/tests/unit/dsl/meta/TestExtensionMethods.yaml b/murano/tests/unit/dsl/meta/TestExtensionMethods.yaml new file mode 100644 index 000000000..eb543efaa --- /dev/null +++ b/murano/tests/unit/dsl/meta/TestExtensionMethods.yaml @@ -0,0 +1,122 @@ +Namespaces: + =: extcls + +--- # ------------------------------------------------------------------ # --- + +Name: Extended + +Properties: + prop: + Contract: $.int() + Default: 123 +Methods: + method: + Body: + Return: $.prop + + +--- # ------------------------------------------------------------------ # --- + +Name: Extender + +Methods: + importedExtensionMethod: + Usage: Extension + Arguments: + - obj: + Contract: $.class(Extended).notNull() + - n: + Contract: $.int().notNull() + Body: + Return: [$obj.prop * $n, $obj.method() * $n] + + nullableExtension: + Usage: Extension + Arguments: + - obj: + Contract: $.class(Extended) + Body: + Return: $obj?.prop + + extensionMethod: + Usage: Extension + Arguments: + - obj: + Contract: $.class(Extended).notNull() + Body: + Return: 222 + + toTileCase: + Usage: Extension + Arguments: + - str: + Contract: $.string().notNull() + Body: + Return: join($str.toCharArray().select( + selectCase($.toLower() = $).switchCase($.toUpper(), $.toLower())), '') + + +--- # ------------------------------------------------------------------ # --- + +Name: TestClass +Import: Extender + +Methods: + testSelfExtensionMethod: + Body: + Return: new(Extended).selfExtensionMethod() + + testImportedExtensionMethod: + Body: + Return: new(Extended).importedExtensionMethod(2) + + testNullableExtensionMethod: + Body: + Return: + - new(Extended).nullableExtension() + - null.nullableExtension() + + testExtensionsPrecedence: + Body: + Return: new(Extended).extensionMethod() + + testCallOnPrimitiveTypes: + Body: + Return: QwertY.toTileCase() + + testCallExtensionExplicitly: + Body: + Return: :Extender.extensionMethod(new(:Extended)) + + testExplicitCallDoenstWorkOnInstance: + Body: + Return: new(Extended).extensionMethod(new(Extended)) + + testCallPythonExtension: + Body: + Return: 4.pythonExtension() + + testCallPythonExtensionExplicitly: + Body: + Return: :Extender.pythonExtension(5) + + testCallPythonClassmethodExtension: + Body: + Return: 7.pythonExtension2() + + selfExtensionMethod: + Usage: Extension + Arguments: + - obj: + Contract: $.class(Extended).notNull() + Body: + Return: [$obj.prop, $obj.method()] + + extensionMethod: + Usage: Extension + Arguments: + - obj: + Contract: $.class(Extended).notNull() + Body: + Return: 111 + diff --git a/murano/tests/unit/dsl/test_extension_methods.py b/murano/tests/unit/dsl/test_extension_methods.py new file mode 100644 index 000000000..4ee80afb4 --- /dev/null +++ b/murano/tests/unit/dsl/test_extension_methods.py @@ -0,0 +1,82 @@ +# Copyright (c) 2016 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 +from yaql.language import specs +from yaql.language import yaqltypes + +from murano.dsl import dsl +from murano.tests.unit.dsl.foundation import object_model as om +from murano.tests.unit.dsl.foundation import test_case + + +class TestExtensionMethods(test_case.DslTestCase): + def setUp(self): + @dsl.name('extcls.Extender') + class PythonClass(object): + def __init__(self, arg): + self.value = arg + + @staticmethod + @specs.meta('Usage', 'Extension') + @specs.parameter('arg', yaqltypes.Integer()) + def python_extension(arg): + return arg * arg + + @classmethod + @specs.meta('Usage', 'Extension') + @specs.parameter('arg', yaqltypes.Integer()) + def python_extension2(cls, arg): + return cls(2 * arg).value + + super(TestExtensionMethods, self).setUp() + self.package_loader.load_class_package( + 'extcls.Extender', None).register_class(PythonClass) + + self._runner = self.new_runner(om.Object('extcls.TestClass')) + + def test_call_self_extension_method(self): + self.assertEqual([123, 123], self._runner.testSelfExtensionMethod()) + + def test_call_imported_extension_method(self): + self.assertEqual( + [246, 246], self._runner.testImportedExtensionMethod()) + + def test_call_nullable_extension_method(self): + self.assertEqual( + [123, None], self._runner.testNullableExtensionMethod()) + + def test_extensions_precedence(self): + self.assertEqual(111, self._runner.testExtensionsPrecedence()) + + def test_explicit_call(self): + self.assertEqual(222, self._runner.testCallExtensionExplicitly()) + + def test_explicit_call_on_instance_fails(self): + self.assertRaises( + exceptions.NoMatchingMethodException, + self._runner.testExplicitCallDoenstWorkOnInstance) + + def test_call_on_primitive_types(self): + self.assertEqual('qWERTy', self._runner.testCallOnPrimitiveTypes()) + + def test_call_python_extension(self): + self.assertEqual(16, self._runner.testCallPythonExtension()) + + def test_call_python_extension_explicitly(self): + self.assertEqual(25, self._runner.testCallPythonExtensionExplicitly()) + + def test_call_python_classmethod_extension(self): + self.assertEqual(14, self._runner.testCallPythonClassmethodExtension()) diff --git a/releasenotes/notes/extension-methods-f674c2d342670e95.yaml b/releasenotes/notes/extension-methods-f674c2d342670e95.yaml new file mode 100644 index 000000000..7a7beabbf --- /dev/null +++ b/releasenotes/notes/extension-methods-f674c2d342670e95.yaml @@ -0,0 +1,14 @@ +--- +features: + - > + New method type: extension methods. Extension methods enable you to "add" + methods to existing types without modifying the original type. + Extension methods are a special kind of static method, but they are called + as if they were instance methods on the extended type. + Extension methods are identified by "Usage: Extension" and the type + they extend is determined by their first argument contract. Thus + such methods must have at lease one parameter. + - > + New type-level keyword "Import" which can be either list or scalar + that specifies type names which extensions methods should be imported + into class context and thus become available to type members.