068831ccd8
With this change MuranoPackage becomes first-class DSL citizen. Packages have version, runtime_version (that is specified in Format attribute of the manifest file) and a list of classes. Previously engine used to have package loader which had most of "load" functionality and class loader that mostly acted as an adapter from package loader to interface that DSL used to get classes. Now class loader is gone and is replaced with package loader at the DSL level. Package loader is responsible for loading packages by either package or class name (as it was before) plus semantic_version spec (for example ">=1.2,<2.0"). Package loader can now keep track of several versions of the same package. Also packages now have requirements with version specs. All class names that are encountered in application code are looked up within requirements only. As a consequence packages that use other packages without referencing them explicitly will become broken. An exception from this rule is core library which is referenced automatically. Partially implements: blueprint murano-versioning Change-Id: I8789ba45b6210e71bf4977a766f82b66d2a2d270
307 lines
10 KiB
Python
307 lines
10 KiB
Python
# 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 weakref
|
|
|
|
import semantic_version
|
|
|
|
from murano.dsl import constants
|
|
from murano.dsl import dsl
|
|
from murano.dsl import dsl_types
|
|
from murano.dsl import exceptions
|
|
from murano.dsl import helpers
|
|
from murano.dsl import murano_method
|
|
from murano.dsl import murano_object
|
|
from murano.dsl import typespec
|
|
from murano.dsl import yaql_integration
|
|
|
|
|
|
class MuranoClass(dsl_types.MuranoClass):
|
|
def __init__(self, namespace_resolver, name, package,
|
|
parents=None):
|
|
self._package = weakref.ref(package)
|
|
self._methods = {}
|
|
self._namespace_resolver = namespace_resolver
|
|
self._name = name
|
|
self._properties = {}
|
|
self._config = {}
|
|
if self._name == constants.CORE_LIBRARY_OBJECT:
|
|
self._parents = []
|
|
else:
|
|
self._parents = parents or [
|
|
package.find_class(constants.CORE_LIBRARY_OBJECT)]
|
|
self._unique_methods = None
|
|
self._parent_mappings = self._build_parent_remappings()
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@property
|
|
def package(self):
|
|
return self._package()
|
|
|
|
@property
|
|
def namespace_resolver(self):
|
|
return self._namespace_resolver
|
|
|
|
@property
|
|
def declared_parents(self):
|
|
return self._parents
|
|
|
|
@property
|
|
def methods(self):
|
|
return self._methods
|
|
|
|
@property
|
|
def parent_mappings(self):
|
|
return self._parent_mappings
|
|
|
|
def extend_with_class(self, cls):
|
|
ctor = yaql_integration.get_class_factory_definition(cls)
|
|
self.add_method('__init__', ctor)
|
|
|
|
@property
|
|
def unique_methods(self):
|
|
if self._unique_methods is None:
|
|
self._unique_methods = list(self._iterate_unique_methods())
|
|
return self._unique_methods
|
|
|
|
def get_method(self, name):
|
|
return self._methods.get(name)
|
|
|
|
def add_method(self, name, payload):
|
|
method = murano_method.MuranoMethod(self, name, payload)
|
|
self._methods[name] = method
|
|
self._unique_methods = None
|
|
return method
|
|
|
|
@property
|
|
def properties(self):
|
|
return self._properties.keys()
|
|
|
|
def register_methods(self, context):
|
|
for method in self.unique_methods:
|
|
context.register_function(
|
|
method.yaql_function_definition,
|
|
name=method.yaql_function_definition.name)
|
|
|
|
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_method_chains(self, name, origin):
|
|
queue = collections.deque([(self, ())])
|
|
while queue:
|
|
cls, path = queue.popleft()
|
|
segment = (cls.methods[name],) if name in cls.methods else ()
|
|
leaf = True
|
|
for p in cls.parents(origin):
|
|
leaf = False
|
|
queue.append((p, path + segment))
|
|
if leaf:
|
|
path = path + segment
|
|
if path:
|
|
yield path
|
|
|
|
def find_single_method(self, name):
|
|
chains = sorted(self._find_method_chains(name, self),
|
|
key=lambda t: len(t))
|
|
result = []
|
|
for i in range(len(chains)):
|
|
if chains[i][0] in result:
|
|
continue
|
|
add = True
|
|
for j in range(i + 1, len(chains)):
|
|
common = 0
|
|
if not add:
|
|
break
|
|
for p in range(len(chains[i])):
|
|
if chains[i][-p - 1] is chains[j][-p - 1]:
|
|
common += 1
|
|
else:
|
|
break
|
|
if common == len(chains[i]):
|
|
add = False
|
|
break
|
|
if add:
|
|
result.append(chains[i][0])
|
|
if len(result) < 1:
|
|
raise exceptions.NoMethodFound(name)
|
|
elif len(result) > 1:
|
|
raise exceptions.AmbiguousMethodName(name)
|
|
return result[0]
|
|
|
|
def find_methods(self, predicate):
|
|
result = []
|
|
for c in self.ancestors():
|
|
for method in c.methods.itervalues():
|
|
if predicate(method) and method not in result:
|
|
result.append(method)
|
|
return result
|
|
|
|
def _iterate_unique_methods(self):
|
|
names = set()
|
|
for c in self.ancestors():
|
|
names.update(c.methods.keys())
|
|
for name in names:
|
|
yield self.find_single_method(name)
|
|
|
|
def find_property(self, name):
|
|
result = []
|
|
for mc in self.ancestors():
|
|
if name in mc.properties and mc not in result:
|
|
result.append(mc)
|
|
return result
|
|
|
|
def find_single_property(self, name):
|
|
result = None
|
|
parents = None
|
|
gen = helpers.traverse(self)
|
|
while True:
|
|
try:
|
|
mc = gen.send(parents)
|
|
if name in mc.properties:
|
|
if result and result != mc:
|
|
raise exceptions.AmbiguousPropertyNameError(name)
|
|
result = mc
|
|
parents = []
|
|
else:
|
|
parents = mc.parents(self)
|
|
except StopIteration:
|
|
return result
|
|
|
|
def invoke(self, name, executor, this, args, kwargs, context=None):
|
|
method = self.find_single_method(name)
|
|
return method.invoke(executor, this, args, kwargs, context)
|
|
|
|
def is_compatible(self, obj):
|
|
if isinstance(obj, (murano_object.MuranoObject,
|
|
dsl.MuranoObjectInterface)):
|
|
obj = obj.type
|
|
return any(cls is self for cls in obj.ancestors())
|
|
|
|
def new(self, owner, object_store, context=None, **kwargs):
|
|
if context is None:
|
|
context = object_store.context
|
|
obj = murano_object.MuranoObject(
|
|
self, owner, object_store.context, **kwargs)
|
|
|
|
def initializer(**params):
|
|
init_context = context.create_child_context()
|
|
init_context[constants.CTX_ALLOW_PROPERTY_WRITES] = True
|
|
obj.initialize(init_context, object_store, params)
|
|
return obj
|
|
|
|
initializer.object = obj
|
|
return initializer
|
|
|
|
def __repr__(self):
|
|
return 'MuranoClass({0}/{1})'.format(self.name, self.version)
|
|
|
|
@property
|
|
def version(self):
|
|
return self.package.version
|
|
|
|
def _build_parent_remappings(self):
|
|
"""Remaps class parents.
|
|
|
|
In case of multiple inheritance class may indirectly get several
|
|
versions of the same class. It is reasonable to try to replace them
|
|
with single version to avoid conflicts. We can do that when within
|
|
versions that satisfy our class package requirements.
|
|
But in order to merge several classes that are not our parents but
|
|
grand parents we will need to modify classes that may be used
|
|
somewhere else (with another set of requirements). We cannot do this.
|
|
So instead we build translation table that will tell which ancestor
|
|
class need to be replaced with which so that we minimize number of
|
|
versions used for single class (or technically packages since version
|
|
is a package attribute). For translation table to work there should
|
|
be a method that returns all class virtual ancestors so that everybody
|
|
will see them instead of accessing class parents directly and getting
|
|
declared ancestors.
|
|
"""
|
|
result = {}
|
|
|
|
aggregation = {
|
|
self.package.name: {(
|
|
self.package,
|
|
semantic_version.Spec('==' + str(self.package.version))
|
|
)}
|
|
}
|
|
for cls, parent in helpers.traverse(
|
|
((self, parent) for parent in self._parents),
|
|
lambda (c, p): ((p, anc) for anc in p.declared_parents)):
|
|
if cls.package != parent.package:
|
|
requirement = cls.package.requirements[parent.package.name]
|
|
aggregation.setdefault(parent.package.name, set()).add(
|
|
(parent.package, requirement))
|
|
|
|
package_bindings = {}
|
|
for versions in aggregation.itervalues():
|
|
mappings = self._remap_package(versions)
|
|
package_bindings.update(mappings)
|
|
|
|
for cls in helpers.traverse(
|
|
self.declared_parents, lambda c: c.declared_parents):
|
|
if cls.package in package_bindings:
|
|
package2 = package_bindings[cls.package]
|
|
cls2 = package2.classes[cls.name]
|
|
result[cls] = cls2
|
|
return result
|
|
|
|
@staticmethod
|
|
def _remap_package(versions):
|
|
result = {}
|
|
reverse_mappings = {}
|
|
versions_list = sorted(versions, key=lambda x: x[0].version)
|
|
i = 0
|
|
while i < len(versions_list):
|
|
package1, requirement1 = versions_list[i]
|
|
dst_package = None
|
|
for j, (package2, requirement2) in enumerate(versions_list):
|
|
if i == j:
|
|
continue
|
|
if package2.version in requirement1 and (
|
|
dst_package is None or
|
|
dst_package.version < package2.version):
|
|
dst_package = package2
|
|
if dst_package:
|
|
result[package1] = dst_package
|
|
reverse_mappings.setdefault(dst_package, []).append(package1)
|
|
for package in reverse_mappings.get(package1, []):
|
|
result[package] = dst_package
|
|
del versions_list[i]
|
|
else:
|
|
i += 1
|
|
return result
|
|
|
|
def parents(self, origin):
|
|
mappings = origin.parent_mappings
|
|
yielded = set()
|
|
for p in self._parents:
|
|
parent = mappings.get(p, p)
|
|
if parent not in yielded:
|
|
yielded.add(parent)
|
|
yield parent
|
|
|
|
def ancestors(self):
|
|
for c in helpers.traverse(self, lambda t: t.parents(self)):
|
|
yield c
|