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:
parent
a1c0fb2f02
commit
73f798da57
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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__:
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
122
murano/tests/unit/dsl/meta/TestExtensionMethods.yaml
Normal file
122
murano/tests/unit/dsl/meta/TestExtensionMethods.yaml
Normal 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
|
||||
|
82
murano/tests/unit/dsl/test_extension_methods.py
Normal file
82
murano/tests/unit/dsl/test_extension_methods.py
Normal 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())
|
14
releasenotes/notes/extension-methods-f674c2d342670e95.yaml
Normal file
14
releasenotes/notes/extension-methods-f674c2d342670e95.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user