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:
parent
a8c66b0047
commit
f229a0eaf9
0
muranoapi/engine/__init__.py
Normal file
0
muranoapi/engine/__init__.py
Normal file
99
muranoapi/engine/classes.py
Normal file
99
muranoapi/engine/classes.py
Normal 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)
|
17
muranoapi/engine/consts.py
Normal file
17
muranoapi/engine/consts.py
Normal 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
|
37
muranoapi/engine/exceptions.py
Normal file
37
muranoapi/engine/exceptions.py
Normal 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)
|
72
muranoapi/engine/helpers.py
Normal file
72
muranoapi/engine/helpers.py
Normal 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
|
37
muranoapi/engine/namespaces.py
Normal file
37
muranoapi/engine/namespaces.py
Normal 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
153
muranoapi/engine/objects.py
Normal 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')
|
37
muranoapi/engine/typespec.py
Normal file
37
muranoapi/engine/typespec.py
Normal 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
|
51
muranoapi/engine/yaql_expression.py
Normal file
51
muranoapi/engine/yaql_expression.py
Normal 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)
|
464
muranoapi/tests/test_engine.py
Normal file
464
muranoapi/tests/test_engine.py
Normal 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(''))
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user