Initial MuranoPL implementation

Implement classes & objects (with inheritance and composition),
namespaces, basic type specs and yaql expressions.

Change-Id: I79acd27c990f37bdd788ea89dd6edb5a70f528b6
Partially-Implements: blueprint new-metadata-dsl
This commit is contained in:
Timur Sufiev 2014-03-11 19:01:27 +04:00 committed by Ruslan Kamaldinov
parent a8c66b0047
commit f229a0eaf9
11 changed files with 971 additions and 2 deletions

View File

View File

@ -0,0 +1,99 @@
# Copyright (c) 2014 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.
import collections
import inspect
from muranoapi.engine import consts
from muranoapi.engine import helpers
from muranoapi.engine import objects
from muranoapi.engine import typespec
class MuranoClass(object):
def __init__(self, class_loader, namespace_resolver, name, parents=None):
self._class_loader = class_loader
self._namespace_resolver = namespace_resolver
self._name = namespace_resolver.resolve_name(name)
self._properties = {}
if self._name == consts.ROOT_CLASS:
self._parents = []
else:
self._parents = parents if parents is not None else [
class_loader.get_class(consts.ROOT_CLASS)]
self.object_class = type(
'mc' + helpers.generate_id(),
tuple([p.object_class for p in self._parents]) or (
objects.MuranoObject,),
{})
@property
def name(self):
return self._name
@property
def namespace_resolver(self):
return self._namespace_resolver
@property
def parents(self):
return self._parents
@property
def properties(self):
return self._properties.keys()
def add_property(self, name, property_typespec):
if not isinstance(property_typespec, typespec.PropertySpec):
raise TypeError('property_typespec')
self._properties[name] = property_typespec
def get_property(self, name):
return self._properties[name]
def find_property(self, name):
types = collections.deque([self])
while len(types) > 0:
mc = types.popleft()
if name in mc.properties:
return mc.get_property(name)
types.extend(mc.parents)
return None
def is_compatible(self, obj):
if isinstance(obj, objects.MuranoObject):
return self.is_compatible(obj.type)
if obj is self:
return True
for parent in obj.parents:
if self.is_compatible(parent):
return True
return False
def new(self, parent, object_store, context, parameters=None,
object_id=None, **kwargs):
obj = self.object_class(self, parent, object_store, context,
object_id=object_id, **kwargs)
if parameters is not None:
argspec = inspect.getargspec(obj.initialize).args
if '_context' in argspec:
parameters['_context'] = context
if '_parent' in argspec:
parameters['_parent'] = parent
obj.initialize(**parameters)
return obj
def __str__(self):
return 'MuranoClass({0})'.format(self.name)

View File

@ -0,0 +1,17 @@
# Copyright (c) 2014 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.
ROOT_CLASS = 'org.openstack.murano.Object'
EVALUATION_MAX_DEPTH = 100

View File

@ -0,0 +1,37 @@
# Copyright (c) 2014 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.
class ReturnException(Exception):
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
class BreakException(Exception):
pass
class NoClassFound(Exception):
def __init__(self, name):
super(NoClassFound, self).__init__('Class %s is not found' % name)
class NoWriteAccess(Exception):
def __init__(self, name):
super(NoWriteAccess, self).__init__(
'Property %s is immutable to the caller' % name)

View File

@ -0,0 +1,72 @@
# Copyright (c) 2014 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.
import collections
import types
import uuid
import yaql
from muranoapi.engine import consts
from muranoapi.engine import yaql_expression
def generate_id():
return uuid.uuid4().hex
def evaluate(value, context, max_depth=consts.EVALUATION_MAX_DEPTH):
if isinstance(value, (yaql_expression.YaqlExpression,
yaql.expressions.Expression)):
func = lambda: evaluate(value.evaluate(context), context, 1)
if max_depth <= 0:
return func
else:
return func()
elif isinstance(value, types.DictionaryType):
result = {}
for d_key, d_value in value.iteritems():
result[evaluate(d_key, context, max_depth - 1)] = \
evaluate(d_value, context, max_depth - 1)
return result
elif isinstance(value, types.ListType):
return [evaluate(t, context, max_depth - 1) for t in value]
elif isinstance(value, types.TupleType):
return tuple(evaluate(list(value), context, max_depth - 1))
elif callable(value):
return value()
elif isinstance(value, types.StringTypes):
return value
elif isinstance(value, collections.Iterable):
return list(value)
else:
return value
def needs_evaluation(value):
if isinstance(value, (yaql_expression.YaqlExpression,
yaql.expressions.Expression)):
return True
elif isinstance(value, types.DictionaryType):
for d_key, d_value in value.iteritems():
if needs_evaluation(d_value) or needs_evaluation(d_key):
return True
elif isinstance(value, types.StringTypes):
return False
elif isinstance(value, collections.Iterable):
for t in value:
if needs_evaluation(t):
return True
return False

View File

@ -0,0 +1,37 @@
# Copyright (c) 2014 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.
class NamespaceResolver(object):
def __init__(self, namespaces):
self._namespaces = namespaces
self._namespaces[''] = ''
def resolve_name(self, name, relative=None):
if name is None:
raise ValueError()
if name and name.startswith(':'):
return name[1:]
if ':' in name:
parts = name.split(':')
if len(parts) != 2 or not parts[1]:
raise NameError('Incorrectly formatted name ' + name)
if parts[0] not in self._namespaces:
raise KeyError('Unknown namespace prefix ' + parts[0])
return '.'.join((self._namespaces[parts[0]], parts[1]))
if not relative and '=' in self._namespaces and '.' not in name:
return '.'.join((self._namespaces['='], name))
if relative and '.' not in name:
return '.'.join((relative, name))
return name

153
muranoapi/engine/objects.py Normal file
View File

@ -0,0 +1,153 @@
# Copyright (c) 2014 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 import context
from muranoapi.engine import exceptions
from muranoapi.engine import helpers
class MuranoObject(object):
def __init__(self, murano_class, parent_obj, object_store, context,
object_id=None, known_classes=None, defaults=None):
if known_classes is None:
known_classes = {}
self.__parent_obj = parent_obj
self.__object_id = object_id or helpers.generate_id()
self.__type = murano_class
self.__properties = {}
self.__object_store = object_store
self.__parents = {}
self.__context = context
self.__defaults = defaults or {}
known_classes[murano_class.name] = self
for parent_class in murano_class.parents:
name = parent_class.name
if not name in known_classes:
obj = parent_class.new(
parent_obj, object_store, context, None,
object_id=self.__object_id,
known_classes=known_classes,
defaults=defaults)
known_classes[name] = self.__parents[name] = obj
else:
self.__parents[name] = known_classes[name]
def initialize(self, **kwargs):
used_names = set()
for i in xrange(2):
for property_name in self.__type.properties:
spec = self.__type.get_property(property_name)
if i == 0 and helpers.needs_evaluation(spec.default) \
or i == 1 and property_name in used_names:
continue
used_names.add(property_name)
property_value = kwargs.get(property_name)
self.set_property(property_name, property_value)
for parent in self.__parents.values():
parent.initialize(**kwargs)
@property
def object_id(self):
return self.__object_id
@property
def type(self):
return self.__type
@property
def parent(self):
return self.__parent_obj
def __getattr__(self, item):
if item.startswith('__'):
raise AttributeError('Access to internal attributes is '
'restricted')
return self.get_property(item)
def get_property(self, item, caller_class=None):
try:
return self.__get_property(item, caller_class)
except AttributeError:
if not caller_class:
raise AttributeError(item)
try:
obj = self.cast(caller_class)
return obj.__properties[item]
except KeyError:
raise AttributeError(item)
except TypeError:
raise AttributeError(item)
def __get_property(self, item, caller_class=None):
if item in self.__properties:
return self.__properties[item]
i = 0
result = None
for parent in self.__parents.values():
try:
result = parent.__get_property(item, caller_class)
i += 1
if i > 1:
raise LookupError()
except AttributeError:
continue
if not i:
raise AttributeError()
return result
def set_property(self, key, value, caller_class=None):
try:
self.__set_property(key, value, caller_class)
except AttributeError as e:
if not caller_class:
raise e
try:
obj = self.cast(caller_class)
obj.__properties[key] = value
except TypeError:
raise AttributeError(key)
def __set_property(self, key, value, caller_class=None):
if key in self.__type.properties:
spec = self.__type.get_property(key)
if (caller_class is not None and
not caller_class.is_compatible(self)):
raise exceptions.NoWriteAccess(key)
default = self.__defaults.get(key, spec.default)
child_context = context.Context(parent_context=self.__context)
child_context.set_data(self)
default = helpers.evaluate(default, child_context, 1)
self.__properties[key] = spec.validate(
value, self, self.__context, self.__object_store, default)
else:
for parent in self.__parents.values():
try:
parent.__set_property(key, value, caller_class)
return
except AttributeError:
continue
raise AttributeError(key)
def cast(self, _type):
if self.type == _type:
return self
for parent in self.__parents.values():
try:
return parent.cast(_type)
except TypeError:
continue
raise TypeError('Cannot cast')

View File

@ -0,0 +1,37 @@
# Copyright (c) 2014 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.
class Spec(object):
def __init__(self, declaration, namespace_resolver):
self._namespace_resolver = namespace_resolver
self._default = declaration.get('Default')
self._has_default = 'Default' in declaration
def validate(self, value, this, context, object_store, default=None):
if default is None:
default = self.default
return value if value is not None else default
@property
def default(self):
return self._default
@property
def has_default(self):
return self._has_default
class PropertySpec(Spec):
pass

View File

@ -0,0 +1,51 @@
# Copyright (c) 2014 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.
import re
import types
import yaql
import yaql.exceptions
class YaqlExpression(object):
def __init__(self, expression):
self._expression = str(expression)
self._parsed_expression = yaql.parse(self._expression)
def expression(self):
return self._expression
def __repr__(self):
return 'YAQL(%s)' % self._expression
def __str__(self):
return self._expression
@staticmethod
def match(expr):
if not isinstance(expr, types.StringTypes):
return False
if re.match('^[\s\w\d.:]*$', expr):
return False
try:
yaql.parse(expr)
return True
except yaql.exceptions.YaqlGrammarException:
return False
except yaql.exceptions.YaqlLexicalException:
return False
def evaluate(self, context=None):
return self._parsed_expression.evaluate(context=context)

View File

@ -0,0 +1,464 @@
# Copyright (c) 2014 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.
import re
import unittest
import mock
import yaql
from muranoapi.engine import classes
from muranoapi.engine import consts
from muranoapi.engine import exceptions
from muranoapi.engine import helpers
from muranoapi.engine import namespaces
from muranoapi.engine import objects
from muranoapi.engine import typespec
from muranoapi.engine import yaql_expression
class TestNamespaceResolving(unittest.TestCase):
def test_fails_w_empty_name(self):
resolver = namespaces.NamespaceResolver({'=': 'com.example.murano'})
self.assertRaises(ValueError, resolver.resolve_name, None)
def test_fails_w_unknown_prefix(self):
resolver = namespaces.NamespaceResolver({'=': 'com.example.murano'})
name = 'unknown_prefix:example.murano'
self.assertRaises(KeyError, resolver.resolve_name, name)
def test_fails_w_prefix_wo_name(self):
resolver = namespaces.NamespaceResolver({'=': 'com.example.murano'})
name = 'sys:'
self.assertRaises(NameError, resolver.resolve_name, name)
def test_fails_w_excessive_prefix(self):
ns = {'sys': 'com.example.murano.system'}
resolver = namespaces.NamespaceResolver(ns)
invalid_name = 'sys:excessive_ns:muranoResource'
self.assertRaises(NameError, resolver.resolve_name, invalid_name)
def test_cuts_empty_prefix(self):
resolver = namespaces.NamespaceResolver({'=': 'com.example.murano'})
# name without prefix delimiter
name = 'some.arbitrary.name'
resolved_name = resolver.resolve_name(':' + name)
self.assertEqual(name, resolved_name)
def test_resolves_specified_ns_prefix(self):
ns = {'sys': 'com.example.murano.system'}
resolver = namespaces.NamespaceResolver(ns)
short_name, full_name = 'sys:File', 'com.example.murano.system.File'
resolved_name = resolver.resolve_name(short_name)
self.assertEqual(full_name, resolved_name)
def test_resolves_current_ns(self):
resolver = namespaces.NamespaceResolver({'=': 'com.example.murano'})
short_name, full_name = 'Resource', 'com.example.murano.Resource'
resolved_name = resolver.resolve_name(short_name)
self.assertEqual(full_name, resolved_name)
def test_resolves_explicit_base(self):
resolver = namespaces.NamespaceResolver({'=': 'com.example.murano'})
resolved_name = resolver.resolve_name('Resource', relative='com.base')
self.assertEqual('com.base.Resource', resolved_name)
def test_resolves_explicit_base_w_empty_namespaces(self):
resolver = namespaces.NamespaceResolver({})
resolved_name = resolver.resolve_name('File', 'com.base')
self.assertEqual('com.base.File', resolved_name)
def test_resolves_w_empty_namespaces(self):
resolver = namespaces.NamespaceResolver({})
resolved_name = resolver.resolve_name('Resource')
self.assertEqual('Resource', resolved_name)
class Bunch(object):
def __init__(self, **kwargs):
super(Bunch, self).__init__()
for key, value in kwargs.iteritems():
setattr(self, key, value)
class TestClassesManipulation(unittest.TestCase):
resolver = mock.Mock(resolve_name=lambda name: name)
def test_class_name(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
self.assertEqual(consts.ROOT_CLASS, cls.name)
def test_class_namespace_resolver(self):
resolver = namespaces.NamespaceResolver({})
cls = classes.MuranoClass(None, resolver, consts.ROOT_CLASS)
self.assertEqual(resolver, cls.namespace_resolver)
def test_root_class_has_no_parents(self):
root_class = classes.MuranoClass(
None, self.resolver, consts.ROOT_CLASS, ['You should not see me!'])
self.assertEqual([], root_class.parents)
def test_non_root_class_resolves_parents(self):
root_cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
class_loader = mock.Mock(get_class=lambda name: root_cls)
desc_cls1 = classes.MuranoClass(class_loader, self.resolver, 'Obj')
desc_cls2 = classes.MuranoClass(
class_loader, self.resolver, 'Obj', [root_cls])
self.assertEqual([root_cls], desc_cls1.parents)
self.assertEqual([root_cls], desc_cls2.parents)
def test_class_initial_properties(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
self.assertEqual([], cls.properties)
def test_fails_add_incompatible_property_to_class(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
kwargs = {'name': 'sampleProperty', 'property_typespec': {}}
self.assertRaises(TypeError, cls.add_property, **kwargs)
def test_add_property_to_class(self):
prop = typespec.PropertySpec({'Default': 1}, self.resolver)
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.add_property('firstPrime', prop)
class_properties = cls.properties
class_property = cls.get_property('firstPrime')
self.assertEqual(['firstPrime'], class_properties)
self.assertEqual(prop, class_property)
def test_class_property_search(self):
void_prop = typespec.PropertySpec({'Default': 'Void'}, self.resolver)
mother_prop = typespec.PropertySpec({'Default': 'Mother'},
self.resolver)
father_prop = typespec.PropertySpec({'Default': 'Father'},
self.resolver)
child_prop = typespec.PropertySpec({'Default': 'Child'},
self.resolver)
root = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
mother = classes.MuranoClass(None, self.resolver, 'Mother', [root])
father = classes.MuranoClass(None, self.resolver, 'Father', [root])
child = classes.MuranoClass(
None, self.resolver, 'Child', [mother, father])
root.add_property('Void', void_prop)
mother.add_property('Mother', mother_prop)
father.add_property('Father', father_prop)
child.add_property('Child', child_prop)
self.assertEqual(child_prop, child.find_property('Child'))
self.assertEqual(father_prop, child.find_property('Father'))
self.assertEqual(mother_prop, child.find_property('Mother'))
self.assertEqual(void_prop, child.find_property('Void'))
def test_class_is_compatible(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
descendant_cls = classes.MuranoClass(
None, self.resolver, 'DescendantCls', [cls])
obj = mock.Mock(spec=objects.MuranoObject)
descendant_obj = mock.Mock(spec=objects.MuranoObject)
obj.type = cls
descendant_obj.type = descendant_cls
descendant_obj.parents = [obj]
self.assertTrue(cls.is_compatible(obj))
self.assertTrue(cls.is_compatible(descendant_obj))
self.assertFalse(descendant_cls.is_compatible(obj))
def test_new_method_calls_initialize(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.object_class = mock.Mock()
with mock.patch('inspect.getargspec') as spec_mock:
spec_mock.return_value = Bunch(args=())
obj = cls.new(None, None, None, {})
self.assertTrue(obj.initialize.called)
def test_new_method_not_calls_initialize(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.object_class = mock.Mock()
obj = cls.new(None, None, None)
self.assertFalse(obj.initialize.called)
class TestObjectsManipulation(unittest.TestCase):
def setUp(self):
self.resolver = mock.Mock(resolve_name=lambda name: name)
self.cls = mock.Mock()
self.cls.name = consts.ROOT_CLASS
self.cls.parents = []
def test_object_valid_type_instantiation(self):
obj = objects.MuranoObject(self.cls, None, None, None)
self.assertEqual(self.cls, obj.type)
def test_object_own_properties_initialization(self):
# TODO: there should be test for initializing first non-dependent
# object properties, then the dependent ones (given as
# YAQL-expressions)
pass
def test_object_parent_properties_initialization(self):
root = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls = classes.MuranoClass(None, self.resolver, 'SomeClass', [root])
root.new = mock.Mock()
init_kwargs = {'theArg': 0}
obj = objects.MuranoObject(cls, None, None, None)
expected_calls = [mock.call().initialize(**init_kwargs)]
obj.initialize(**init_kwargs)
# each object should also initialize his parent objects
self.assertEqual(expected_calls, root.new.mock_calls[1:])
def test_object_id(self):
_id = 'some_id'
patch_at = 'muranoapi.engine.objects.helpers.generate_id'
obj = objects.MuranoObject(self.cls, None, None, None, object_id=_id)
with mock.patch(patch_at) as gen_id_mock:
gen_id_mock.return_value = _id
obj1 = objects.MuranoObject(self.cls, None, None, None)
self.assertEqual(_id, obj.object_id)
self.assertEqual(_id, obj1.object_id)
def test_parent_obj(self):
parent = mock.Mock()
obj = objects.MuranoObject(self.cls, parent, None, None)
self.assertEqual(parent, obj.parent)
def test_fails_internal_property_access(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.add_property('__hidden',
typespec.PropertySpec({'Default': 10}, self.resolver))
obj = objects.MuranoObject(cls, None, None, None)
self.assertRaises(AttributeError, lambda: obj.__hidden)
def test_proper_property_access(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.add_property('someProperty',
typespec.PropertySpec({'Default': 0}, self.resolver))
obj = cls.new(None, None, None, {})
self.assertEqual(0, obj.someProperty)
def test_parent_class_property_access(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
child_cls = classes.MuranoClass(None, self.resolver, 'Child', [cls])
cls.add_property('anotherProperty',
typespec.PropertySpec({'Default': 0}, self.resolver))
obj = child_cls.new(None, None, None, {})
self.assertEqual(0, obj.anotherProperty)
def test_fails_on_parents_property_collision(self):
root = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
mother = classes.MuranoClass(None, self.resolver, 'Mother', [root])
father = classes.MuranoClass(None, self.resolver, 'Father', [root])
child = classes.MuranoClass(
None, self.resolver, 'Child', [mother, father])
mother.add_property(
'conflictProp',
typespec.PropertySpec({'Default': 0}, self.resolver))
father.add_property(
'conflictProp',
typespec.PropertySpec({'Default': 0}, self.resolver))
obj = child.new(None, None, None, {})
self.assertRaises(LookupError, lambda: obj.conflictProp)
def test_fails_setting_undeclared_property(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
obj = cls.new(None, None, None, {})
self.assertRaises(AttributeError, obj.set_property, 'newOne', 10)
def test_set_undeclared_property_as_internal(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
obj = cls.new(None, None, None, {})
obj.cast = mock.Mock(return_value=obj)
prop_value = 10
obj.set_property('internalProp', prop_value, caller_class=cls)
resolved_value = obj.get_property('internalProp', caller_class=cls)
self.assertEqual(prop_value, resolved_value)
def test_fails_forbidden_set_property(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.add_property('someProperty',
typespec.PropertySpec({'Default': 0}, self.resolver))
cls.is_compatible = mock.Mock(return_value=False)
obj = cls.new(None, None, None, {})
self.assertRaises(exceptions.NoWriteAccess, obj.set_property,
'someProperty', 10, caller_class=cls)
def test_set_property(self):
cls = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls.add_property('someProperty',
typespec.PropertySpec({'Default': 0}, self.resolver))
obj = cls.new(None, None, None, {})
with mock.patch('yaql.context.Context'):
with mock.patch('muranoapi.engine.helpers') as helpers_mock:
helpers_mock.evaluate = lambda val, ctx, _: val
obj.set_property('someProperty', 10)
self.assertEqual(10, obj.someProperty)
def test_set_parent_property(self):
root = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls = classes.MuranoClass(None, self.resolver, 'SomeClass', [root])
root.add_property('rootProperty',
typespec.PropertySpec({'Default': 0}, self.resolver))
obj = cls.new(None, None, None, {})
with mock.patch('muranoapi.engine.helpers') as helpers_mock:
with mock.patch('yaql.context.Context'):
helpers_mock.evaluate = lambda val, ctx, _: val
obj.set_property('rootProperty', 20)
self.assertEqual(20, obj.rootProperty)
def test_object_up_cast(self):
root = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
root_alt = classes.MuranoClass(None, self.resolver, 'RootAlt', [])
cls = classes.MuranoClass(
None, self.resolver, 'SomeClass', [root, root_alt])
root_obj = root.new(None, None, None)
cls_obj = cls.new(None, None, None)
root_obj_casted2root = root_obj.cast(root)
cls_obj_casted2root = cls_obj.cast(root)
cls_obj_casted2root_alt = cls_obj.cast(root_alt)
self.assertEqual(root_obj, root_obj_casted2root)
# each object creates an _internal_ parent objects hierarchy,
# so direct comparison of objects is not possible
self.assertEqual(root, cls_obj_casted2root.type)
self.assertEqual(root_alt, cls_obj_casted2root_alt.type)
def test_fails_object_down_cast(self):
root = classes.MuranoClass(None, self.resolver, consts.ROOT_CLASS)
cls = classes.MuranoClass(
None, self.resolver, 'SomeClass', [root])
root_obj = root.new(None, None, None)
self.assertRaises(TypeError, root_obj.cast, cls)
class TestHelperFunctions(unittest.TestCase):
def test_generate_id(self):
generated_id = helpers.generate_id()
self.assertTrue(re.match(r'[a-z0-9]{32}', generated_id))
def test_evaluate(self):
yaql_value = mock.Mock(spec=yaql_expression.YaqlExpression,
evaluate=lambda context: 'atom')
complex_value = {yaql_value: ['some', (1, yaql_value), lambda: 'hi!'],
'sample': [yaql_value, xrange(5)]}
complex_literal = {'atom': ['some', (1, 'atom'), 'hi!'],
'sample': ['atom', [0, 1, 2, 3, 4]]}
# tuple(evaluate(list)) transformation adds + 1
complex_literal_depth = 3 + 1
evaluated_value = helpers.evaluate(yaql_value, None, 1)
non_evaluated_value = helpers.evaluate(yaql_value, None, 0)
evaluated_complex_value = helpers.evaluate(complex_value, None)
non_evaluated_complex_value = helpers.evaluate(
complex_value, None, complex_literal_depth)
self.assertEqual('atom', evaluated_value)
self.assertNotEqual('atom', non_evaluated_value)
self.assertEqual(complex_literal, evaluated_complex_value)
self.assertNotEqual(complex_literal, non_evaluated_complex_value)
def test_needs_evaluation(self):
testee = helpers.needs_evaluation
parsed_expr = yaql.parse("string")
yaql_expr = yaql_expression.YaqlExpression("string")
self.assertTrue(testee(parsed_expr))
self.assertTrue(testee(yaql_expr))
self.assertTrue(testee({yaql_expr: 1}))
self.assertTrue(testee({'label': yaql_expr}))
self.assertTrue(testee([yaql_expr]))
class TestYaqlExpression(unittest.TestCase):
def test_expression(self):
yaql_expr = yaql_expression.YaqlExpression('string')
self.assertEqual('string', yaql_expr.expression())
def test_evaluate_calls(self):
string = 'string'
expected_calls = [mock.call(string),
mock.call().evaluate(context=None)]
with mock.patch('yaql.parse') as mock_parse:
yaql_expr = yaql_expression.YaqlExpression(string)
yaql_expr.evaluate()
self.assertEqual(expected_calls, mock_parse.mock_calls)
def test_match_returns(self):
expr = yaql_expression.YaqlExpression('string')
with mock.patch('yaql.parse'):
self.assertTrue(expr.match('$some'))
self.assertTrue(expr.match('$.someMore'))
with mock.patch('yaql.parse') as parse_mock:
parse_mock.side_effect = yaql.exceptions.YaqlGrammarException
self.assertFalse(expr.match(''))
with mock.patch('yaql.parse') as parse_mock:
parse_mock.side_effect = yaql.exceptions.YaqlLexicalException
self.assertFalse(expr.match(''))

View File

@ -31,6 +31,8 @@ passlib
jsonschema>=2.0.0,<3.0.0
python-keystoneclient>=0.6.0
oslo.config>=1.2.0
murano-common==0.4.1
oslo.messaging>=1.3.0a4
# not listed in global requirements
murano-common==0.4.1
yaql==0.2