"""
Mapping and mapping registry.
This file is part of the everest project.
See LICENSE.txt for licensing, CONTRIBUTORS.txt for contributor information.
Created on May 4, 2012.
"""
from collections import OrderedDict
from everest.representers.attributes import MappedAttribute
from everest.representers.config import RepresenterConfiguration
from everest.representers.dataelements import SimpleCollectionDataElement
from everest.representers.dataelements import SimpleLinkedDataElement
from everest.representers.dataelements import SimpleMemberDataElement
from everest.representers.traversal import AttributeKey
from everest.representers.traversal import DataElementBuilderResourceTreeVisitor
from everest.representers.traversal import MappingDataElementTreeTraverser
from everest.representers.traversal import ResourceBuilderDataElementTreeVisitor
from everest.representers.traversal import ResourceTreeTraverser
from everest.resources.attributes import ResourceAttributeKinds
from everest.resources.attributes import get_resource_class_attributes
from everest.resources.interfaces import ICollectionResource
from everest.resources.interfaces import IMemberResource
from everest.resources.interfaces import IResourceLink
from everest.resources.link import Link
from zope.interface import providedBy as provided_by # pylint: disable=E0611,F0401
__docformat__ = 'reStructuredText en'
__all__ = ['Mapping',
'MappingRegistry',
'SimpleMappingRegistry',
]
[docs]class Mapping(object):
"""
Performs configurable resource <-> data element tree <-> representation
mappings.
:property mapped_class: The resource class mapped by this mapping.
:property data_element_class: The data element class for this mapping
"""
[docs] def __init__(self, mapping_registry, mapped_class, data_element_class,
configuration):
"""
:param configuration: mapping configuration object.
"""
self.__mp_reg = mapping_registry
self.__mapped_cls = mapped_class
self.__de_cls = data_element_class
self.__configuration = configuration
#
self.__mapped_attr_cache = {}
def clone(self, options=None, attribute_options=None):
copied_cfg = self.__configuration.copy()
upd_cfg = type(copied_cfg)(options=options,
attribute_options=attribute_options)
copied_cfg.update(upd_cfg)
return self.__class__(self.__mp_reg, self.__mapped_cls,
self.__de_cls, copied_cfg)
@property
[docs] def configuration(self):
"""
Returns this mapping's configuration object.
"""
# We clear the cache every time the configuration is accessed since
# we can not guarantee that it stays unchanged.
self.__mapped_attr_cache.clear()
return self.__configuration
[docs] def get_attribute_map(self, mapped_class=None, key=None):
"""
Returns a map of all attributes of the given mapped class.
:param key: tuple of attribute names specifying a path to a nested
attribute in a resource tree. If this is not given, all attributes
in this mapping will be returned.
"""
if mapped_class is None:
mapped_class = self.__mapped_cls
if key is None:
key = AttributeKey(()) # Top level access.
# FIXME: Investigate caching of mapped attributes.
attrs = None # self.__mapped_attr_cache.get((mapped_class, key))
if attrs is None:
attrs = self.__collect_mapped_attributes(mapped_class, key)
# self.__mapped_attr_cache[(mapped_class, key)] = attrs
return attrs
def attribute_iterator(self, mapped_class=None, key=None):
attr_map = self.get_attribute_map(mapped_class=mapped_class, key=key)
for attr in attr_map.itervalues():
yield attr
def terminal_attribute_iterator(self, mapped_class=None, key=None):
for attr in self.attribute_iterator(mapped_class, key=key):
if attr.kind == ResourceAttributeKinds.TERMINAL:
yield attr
def nonterminal_attribute_iterator(self, mapped_class=None, key=None):
for attr in self.attribute_iterator(mapped_class=mapped_class,
key=key):
if attr.kind != ResourceAttributeKinds.TERMINAL:
yield attr
def create_data_element(self):
return self.__de_cls.create()
def create_data_element_from_resource(self, resource):
mp = self.__mp_reg.find_or_create_mapping(type(resource))
return mp.data_element_class.create_from_resource(resource)
def create_linked_data_element_from_resource(self, resource):
mp = self.__mp_reg.find_or_create_mapping(Link)
return mp.data_element_class.create_from_resource(resource)
def map_to_resource(self, data_element, resolve_urls=True):
trv = MappingDataElementTreeTraverser(data_element, mapping=self)
visitor = ResourceBuilderDataElementTreeVisitor(resolve_urls)
trv.run(visitor)
return visitor.resource
def map_to_data_element(self, resource):
trv = ResourceTreeTraverser(resource, self)
visitor = DataElementBuilderResourceTreeVisitor(self)
trv.run(visitor)
return visitor.data_element
@property
def mapped_class(self):
return self.__mapped_cls
@property
def data_element_class(self):
return self.__de_cls
@property
def mapping_registry(self):
return self.__mp_reg
def __collect_mapped_attributes(self, mapped_class, key):
collected_mp_attrs = OrderedDict()
if len(key) == 0:
# Top level access - fetch resource attributes and create new
# mapped attributes.
rc_attrs = get_resource_class_attributes(mapped_class)
for rc_attr in rc_attrs.itervalues():
attr_key = key + (rc_attr.name,)
attr_mp_opts = \
self.__configuration.get_attribute_options(attr_key)
new_mp_attr = MappedAttribute(rc_attr, options=attr_mp_opts)
collected_mp_attrs[rc_attr.name] = new_mp_attr
else:
# Nested access - fetch mapped attributes from mapping and
# clone.
if mapped_class is self.__mapped_cls:
mp = self
else:
mp = self.__mp_reg.find_or_create_mapping(mapped_class)
mp_attrs = mp.get_attribute_map()
for mp_attr in mp_attrs.itervalues():
attr_key = key + (mp_attr.name,)
attr_mp_opts = \
dict(((k, v)
for (k, v) in
self.__configuration \
.get_attribute_options(attr_key).iteritems()
if not v is None))
clnd_mp_attr = mp_attr.clone(options=attr_mp_opts)
collected_mp_attrs[mp_attr.name] = clnd_mp_attr
return collected_mp_attrs
class MappingRegistry(object):
member_data_element_base_class = None
collection_data_element_base_class = None
linked_data_element_base_class = None
configuration_class = None
mapping_class = Mapping
def __init__(self):
self.__configuration = self.configuration_class() # pylint: disable=E1102
self.__mappings = {}
self.__is_initialized = False
def _initialize(self):
# Implement this for static initializations.
raise NotImplementedError('Abstract method.')
def set_default_config_option(self, name, value):
self.__configuration.set_option(name, value)
def create_mapping(self, mapped_class, configuration=None):
"""
Creates a new mapping for the given mapped class and representer
configuration.
:param configuration: configuration for the new data element class.
:type configuration: :class:`RepresenterConfiguration`
:returns: newly created instance of :class:`Mapping`
"""
cfg = self.__configuration.copy()
if not configuration is None:
cfg.update(configuration)
provided_ifcs = provided_by(object.__new__(mapped_class))
if IMemberResource in provided_ifcs:
base_data_element_class = self.member_data_element_base_class
elif ICollectionResource in provided_ifcs:
base_data_element_class = self.collection_data_element_base_class
elif IResourceLink in provided_ifcs:
base_data_element_class = self.linked_data_element_base_class
else:
raise ValueError('Mapped class for data element class does not '
'implement one of the required interfaces.')
name = "%s%s" % (mapped_class.__name__,
base_data_element_class.__name__)
de_cls = type(name, (base_data_element_class,), {})
mp = self.mapping_class(self, mapped_class, de_cls, cfg)
# Set the data element class' mapping.
# FIXME: This looks like a hack.
de_cls.mapping = mp
return mp
def set_mapping(self, mapping):
"""
Registers the given mapping, using the mapped class as key.
:param mapping: mapping
:type mapping: :class:`Mapping`
"""
self.__mappings[mapping.mapped_class] = mapping
def find_mapping(self, mapped_class):
"""
Returns the mapping registered for the given mapped class or any of
its base classes. Returns `None` if no mapping can be found.
:param mapped_class: mapped type
:type mapped_class: type
:returns: instance of :class:`Mapping` or `None`
"""
if not self.__is_initialized:
self.__is_initialized = True
self._initialize()
mapping = None
for base_cls in mapped_class.__mro__:
try:
mapping = self.__mappings[base_cls]
except KeyError:
continue
else:
break
return mapping
def find_or_create_mapping(self, mapped_class):
"""
First calls :meth:`find_mapping` to check if a mapping for the given
mapped class or any of its base classes has been created. If not, a
new one is created with a default configuration, registered
automatically and returned.
"""
mapping = self.find_mapping(mapped_class)
if mapping is None:
mapping = self.create_mapping(mapped_class)
self.set_mapping(mapping)
return mapping
def get_mappings(self):
"""
Returns an iterator over all registered mappings.
:returns: iterator yielding tuples containing a mapped class as the
first and a :class:`Mapping` instance as the second item.
"""
return self.__mappings.itervalues()
[docs]class SimpleMappingRegistry(MappingRegistry):
"""
Default implementation for a mapping registry using default data element
and configuration classes.
"""
member_data_element_base_class = SimpleMemberDataElement
collection_data_element_base_class = SimpleCollectionDataElement
linked_data_element_base_class = SimpleLinkedDataElement
configuration_class = RepresenterConfiguration
def _initialize(self):
# Create and register the linked data element class.
configuration = self.configuration_class()
mapping = self.create_mapping(Link, configuration)
self.set_mapping(mapping)