Extension methods were introduced to MuranoPL

* 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.

Change-Id: If757327857376ac66784acd4bd29471e6f28b612
This commit is contained in:
Stan Lagun 2016-03-02 21:22:14 +03:00 committed by Kirill Zaitsev
parent a1c0fb2f02
commit 73f798da57
22 changed files with 555 additions and 89 deletions

View File

@ -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'

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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:

View File

@ -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,

View File

@ -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:

View File

@ -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')

View File

@ -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))

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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())

View File

@ -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__:

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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.