ad65824806
* test_make_flat_dict(): only use one dictionary key. On Python 3, the hash function is now randomized by default and so dictionary keys are returned in a random order. * Replace "not cmp(a, b) == 0" with "a != b" * test_common: replace range(n) with list(range(n)) to get a list on Python 3. On Python 3, range() now returns an iterator. * Replace dict.items()[0] with list(dict.items()[0]). On Python 3, dict.items() now returns a view which is not indexable. * VolumeTypesController.index(): replace dict.values() with list(dict.values()) to get a list on Python 3. * test_xmlutil: use byte strings for XML * test_extensions: use byte strings for HTTP body * Add the following tests to tests-py3.txt: - cinder.tests.unit.api.test_common - cinder.tests.unit.api.test_extensions - cinder.tests.unit.api.test_versions - cinder.tests.unit.api.test_xmlutil Partial-Implements: blueprint cinder-python3 Change-Id: I40c28f6b6a1cf72ce13774755153c8691b4d621b
969 lines
30 KiB
Python
969 lines
30 KiB
Python
# Copyright 2011 OpenStack Foundation
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 os.path
|
|
import re
|
|
|
|
from lxml import etree
|
|
import six
|
|
|
|
from cinder.i18n import _
|
|
from cinder import utils
|
|
|
|
|
|
XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
|
|
XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
|
|
XMLNS_COMMON_V10 = 'http://docs.openstack.org/common/api/v1.0'
|
|
XMLNS_ATOM = 'http://www.w3.org/2005/Atom'
|
|
XMLNS_VOLUME_V1 = ('http://docs.openstack.org/api/openstack-block-storage/1.0/'
|
|
'content')
|
|
XMLNS_VOLUME_V2 = ('http://docs.openstack.org/api/openstack-block-storage/2.0/'
|
|
'content')
|
|
|
|
_split_pattern = re.compile(r'([^:{]*{[^}]*}[^:]*|[^:]+)')
|
|
|
|
|
|
def validate_schema(xml, schema_name):
|
|
if isinstance(xml, str):
|
|
xml = etree.fromstring(xml)
|
|
base_path = 'cinder/api/schemas/v1.1/'
|
|
if schema_name in ('atom', 'atom-link'):
|
|
base_path = 'cinder/api/schemas/'
|
|
schema_path = os.path.join(utils.cinderdir(),
|
|
'%s%s.rng' % (base_path, schema_name))
|
|
schema_doc = etree.parse(schema_path)
|
|
relaxng = etree.RelaxNG(schema_doc)
|
|
relaxng.assertValid(xml)
|
|
|
|
|
|
class Selector(object):
|
|
"""Selects datum to operate on from an object."""
|
|
|
|
def __init__(self, *chain):
|
|
"""Initialize the selector.
|
|
|
|
Each argument is a subsequent index into the object.
|
|
"""
|
|
|
|
self.chain = chain
|
|
|
|
def __repr__(self):
|
|
"""Return a representation of the selector."""
|
|
|
|
return "Selector" + repr(self.chain)
|
|
|
|
def __call__(self, obj, do_raise=False):
|
|
"""Select a datum to operate on.
|
|
|
|
Selects the relevant datum within the object.
|
|
|
|
:param obj: The object from which to select the object.
|
|
:param do_raise: If False (the default), return None if the
|
|
indexed datum does not exist. Otherwise,
|
|
raise a KeyError.
|
|
"""
|
|
|
|
# Walk the selector list
|
|
for elem in self.chain:
|
|
# If it's callable, call it
|
|
if callable(elem):
|
|
obj = elem(obj)
|
|
else:
|
|
# Use indexing
|
|
try:
|
|
obj = obj[elem]
|
|
except (KeyError, IndexError):
|
|
# No sense going any further
|
|
if do_raise:
|
|
# Convert to a KeyError, for consistency
|
|
raise KeyError(elem)
|
|
return None
|
|
|
|
# Return the finally-selected object
|
|
return obj
|
|
|
|
|
|
def get_items(obj):
|
|
"""Get items in obj."""
|
|
|
|
return list(obj.items())
|
|
|
|
|
|
class EmptyStringSelector(Selector):
|
|
"""Returns the empty string if Selector would return None."""
|
|
def __call__(self, obj, do_raise=False):
|
|
"""Returns empty string if the selected value does not exist."""
|
|
|
|
try:
|
|
return super(EmptyStringSelector, self).__call__(obj, True)
|
|
except KeyError:
|
|
return ""
|
|
|
|
|
|
class ConstantSelector(object):
|
|
"""Returns a constant."""
|
|
|
|
def __init__(self, value):
|
|
"""Initialize the selector.
|
|
|
|
:param value: The value to return.
|
|
"""
|
|
|
|
self.value = value
|
|
|
|
def __repr__(self):
|
|
"""Return a representation of the selector."""
|
|
|
|
return repr(self.value)
|
|
|
|
def __call__(self, _obj, _do_raise=False):
|
|
"""Select a datum to operate on.
|
|
|
|
Returns a constant value. Compatible with
|
|
Selector.__call__().
|
|
"""
|
|
|
|
return self.value
|
|
|
|
|
|
class TemplateElement(object):
|
|
"""Represent an element in the template."""
|
|
|
|
def __init__(self, tag, attrib=None, selector=None, subselector=None,
|
|
**extra):
|
|
"""Initialize an element.
|
|
|
|
Initializes an element in the template. Keyword arguments
|
|
specify attributes to be set on the element; values must be
|
|
callables. See TemplateElement.set() for more information.
|
|
|
|
:param tag: The name of the tag to create.
|
|
:param attrib: An optional dictionary of element attributes.
|
|
:param selector: An optional callable taking an object and
|
|
optional boolean do_raise indicator and
|
|
returning the object bound to the element.
|
|
:param subselector: An optional callable taking an object and
|
|
optional boolean do_raise indicator and
|
|
returning the object bound to the element.
|
|
This is used to further refine the datum
|
|
object returned by selector in the event
|
|
that it is a list of objects.
|
|
"""
|
|
|
|
# Convert selector into a Selector
|
|
if selector is None:
|
|
selector = Selector()
|
|
elif not callable(selector):
|
|
selector = Selector(selector)
|
|
|
|
# Convert subselector into a Selector
|
|
if subselector is not None and not callable(subselector):
|
|
subselector = Selector(subselector)
|
|
|
|
self.tag = tag
|
|
self.selector = selector
|
|
self.subselector = subselector
|
|
self.attrib = {}
|
|
self._text = None
|
|
self._children = []
|
|
self._childmap = {}
|
|
|
|
# Run the incoming attributes through set() so that they
|
|
# become selectorized
|
|
if not attrib:
|
|
attrib = {}
|
|
attrib.update(extra)
|
|
for k, v in attrib.items():
|
|
self.set(k, v)
|
|
|
|
def __repr__(self):
|
|
"""Return a representation of the template element."""
|
|
|
|
return ('<%s.%s %r at %#x>' %
|
|
(self.__class__.__module__, self.__class__.__name__,
|
|
self.tag, id(self)))
|
|
|
|
def __len__(self):
|
|
"""Return the number of child elements."""
|
|
|
|
return len(self._children)
|
|
|
|
def __contains__(self, key):
|
|
"""Determine whether a child node named by key exists."""
|
|
|
|
return key in self._childmap
|
|
|
|
def __getitem__(self, idx):
|
|
"""Retrieve a child node by index or name."""
|
|
|
|
if isinstance(idx, six.string_types):
|
|
# Allow access by node name
|
|
return self._childmap[idx]
|
|
else:
|
|
return self._children[idx]
|
|
|
|
def append(self, elem):
|
|
"""Append a child to the element."""
|
|
|
|
# Unwrap templates...
|
|
elem = elem.unwrap()
|
|
|
|
# Avoid duplications
|
|
if elem.tag in self._childmap:
|
|
raise KeyError(elem.tag)
|
|
|
|
self._children.append(elem)
|
|
self._childmap[elem.tag] = elem
|
|
|
|
def extend(self, elems):
|
|
"""Append children to the element."""
|
|
|
|
# Pre-evaluate the elements
|
|
elemmap = {}
|
|
elemlist = []
|
|
for elem in elems:
|
|
# Unwrap templates...
|
|
elem = elem.unwrap()
|
|
|
|
# Avoid duplications
|
|
if elem.tag in self._childmap or elem.tag in elemmap:
|
|
raise KeyError(elem.tag)
|
|
|
|
elemmap[elem.tag] = elem
|
|
elemlist.append(elem)
|
|
|
|
# Update the children
|
|
self._children.extend(elemlist)
|
|
self._childmap.update(elemmap)
|
|
|
|
def insert(self, idx, elem):
|
|
"""Insert a child element at the given index."""
|
|
|
|
# Unwrap templates...
|
|
elem = elem.unwrap()
|
|
|
|
# Avoid duplications
|
|
if elem.tag in self._childmap:
|
|
raise KeyError(elem.tag)
|
|
|
|
self._children.insert(idx, elem)
|
|
self._childmap[elem.tag] = elem
|
|
|
|
def remove(self, elem):
|
|
"""Remove a child element."""
|
|
|
|
# Unwrap templates...
|
|
elem = elem.unwrap()
|
|
|
|
# Check if element exists
|
|
if elem.tag not in self._childmap or self._childmap[elem.tag] != elem:
|
|
raise ValueError(_('element is not a child'))
|
|
|
|
self._children.remove(elem)
|
|
del self._childmap[elem.tag]
|
|
|
|
def get(self, key):
|
|
"""Get an attribute.
|
|
|
|
Returns a callable which performs datum selection.
|
|
|
|
:param key: The name of the attribute to get.
|
|
"""
|
|
|
|
return self.attrib[key]
|
|
|
|
def set(self, key, value=None):
|
|
"""Set an attribute.
|
|
|
|
:param key: The name of the attribute to set.
|
|
|
|
:param value: A callable taking an object and optional boolean
|
|
do_raise indicator and returning the datum bound
|
|
to the attribute. If None, a Selector() will be
|
|
constructed from the key. If a string, a
|
|
Selector() will be constructed from the string.
|
|
"""
|
|
|
|
# Convert value to a selector
|
|
if value is None:
|
|
value = Selector(key)
|
|
elif not callable(value):
|
|
value = Selector(value)
|
|
|
|
self.attrib[key] = value
|
|
|
|
def keys(self):
|
|
"""Return the attribute names."""
|
|
|
|
return self.attrib.keys()
|
|
|
|
def items(self):
|
|
"""Return the attribute names and values."""
|
|
|
|
return self.attrib.items()
|
|
|
|
def unwrap(self):
|
|
"""Unwraps a template to return a template element."""
|
|
|
|
# We are a template element
|
|
return self
|
|
|
|
def wrap(self):
|
|
"""Wraps a template element to return a template."""
|
|
|
|
# Wrap in a basic Template
|
|
return Template(self)
|
|
|
|
def apply(self, elem, obj):
|
|
"""Apply text and attributes to an etree.Element.
|
|
|
|
Applies the text and attribute instructions in the template
|
|
element to an etree.Element instance.
|
|
|
|
:param elem: An etree.Element instance.
|
|
:param obj: The base object associated with this template
|
|
element.
|
|
"""
|
|
|
|
# Start with the text...
|
|
if self.text is not None:
|
|
elem.text = six.text_type(self.text(obj))
|
|
|
|
# Now set up all the attributes...
|
|
for key, value in self.attrib.items():
|
|
try:
|
|
elem.set(key, six.text_type(value(obj, True)))
|
|
except KeyError:
|
|
# Attribute has no value, so don't include it
|
|
pass
|
|
|
|
def getAttrib(self, obj):
|
|
"""Get attribute."""
|
|
tmpattrib = {}
|
|
# Now set up all the attributes...
|
|
for key, value in self.attrib.items():
|
|
try:
|
|
tmpattrib[key] = value(obj)
|
|
except KeyError:
|
|
# Attribute has no value, so don't include it
|
|
pass
|
|
return tmpattrib
|
|
|
|
@staticmethod
|
|
def _splitTagName(name):
|
|
return _split_pattern.findall(name)
|
|
|
|
def _render(self, parent, datum, patches, nsmap):
|
|
"""Internal rendering.
|
|
|
|
Renders the template node into an etree.Element object.
|
|
Returns the etree.Element object.
|
|
|
|
:param parent: The parent etree.Element instance.
|
|
:param datum: The datum associated with this template element.
|
|
:param patches: A list of other template elements that must
|
|
also be applied.
|
|
:param nsmap: An optional namespace dictionary to be
|
|
associated with the etree.Element instance.
|
|
"""
|
|
|
|
# Allocate a node
|
|
if callable(self.tag):
|
|
tagname = self.tag(datum)
|
|
else:
|
|
tagname = self.tag
|
|
|
|
# If the datum is None
|
|
if datum is not None:
|
|
tmpattrib = self.getAttrib(datum)
|
|
else:
|
|
tmpattrib = {}
|
|
|
|
tagnameList = self._splitTagName(tagname)
|
|
insertIndex = 0
|
|
|
|
# If parent is not none and has same tagname
|
|
if parent is not None:
|
|
for i in range(0, len(tagnameList)):
|
|
tmpInsertPos = parent.find(tagnameList[i])
|
|
if tmpInsertPos is None:
|
|
break
|
|
elif parent.attrib != tmpattrib:
|
|
break
|
|
parent = tmpInsertPos
|
|
insertIndex = i + 1
|
|
|
|
if insertIndex >= len(tagnameList):
|
|
insertIndex = insertIndex - 1
|
|
|
|
# Create root elem
|
|
elem = etree.Element(tagnameList[insertIndex], nsmap=nsmap)
|
|
rootelem = elem
|
|
subelem = elem
|
|
|
|
# Create subelem
|
|
for i in range((insertIndex + 1), len(tagnameList)):
|
|
subelem = etree.SubElement(elem, tagnameList[i])
|
|
elem = subelem
|
|
|
|
# If we have a parent, append the node to the parent
|
|
if parent is not None:
|
|
# If we can merge this element, then insert
|
|
if insertIndex > 0:
|
|
parent.insert(len(list(parent)), rootelem)
|
|
else:
|
|
parent.append(rootelem)
|
|
|
|
# If the datum is None, do nothing else
|
|
if datum is None:
|
|
return rootelem
|
|
|
|
# Apply this template element to the element
|
|
self.apply(subelem, datum)
|
|
|
|
# Additionally, apply the patches
|
|
for patch in patches:
|
|
patch.apply(subelem, datum)
|
|
|
|
# We have fully rendered the element; return it
|
|
return rootelem
|
|
|
|
def render(self, parent, obj, patches=None, nsmap=None):
|
|
"""Render an object.
|
|
|
|
Renders an object against this template node. Returns a list
|
|
of two-item tuples, where the first item is an etree.Element
|
|
instance and the second item is the datum associated with that
|
|
instance.
|
|
|
|
:param parent: The parent for the etree.Element instances.
|
|
:param obj: The object to render this template element
|
|
against.
|
|
:param patches: A list of other template elements to apply
|
|
when rendering this template element.
|
|
:param nsmap: An optional namespace dictionary to attach to
|
|
the etree.Element instances.
|
|
"""
|
|
|
|
patches = patches or []
|
|
# First, get the datum we're rendering
|
|
data = None if obj is None else self.selector(obj)
|
|
|
|
# Check if we should render at all
|
|
if not self.will_render(data):
|
|
return []
|
|
elif data is None:
|
|
return [(self._render(parent, None, patches, nsmap), None)]
|
|
|
|
# Make the data into a list if it isn't already
|
|
if not isinstance(data, list):
|
|
data = [data]
|
|
elif parent is None:
|
|
raise ValueError(_('root element selecting a list'))
|
|
|
|
# Render all the elements
|
|
elems = []
|
|
for datum in data:
|
|
if self.subselector is not None:
|
|
datum = self.subselector(datum)
|
|
elems.append((self._render(parent, datum, patches, nsmap), datum))
|
|
|
|
# Return all the elements rendered, as well as the
|
|
# corresponding datum for the next step down the tree
|
|
return elems
|
|
|
|
def will_render(self, datum):
|
|
"""Hook method.
|
|
|
|
An overridable hook method to determine whether this template
|
|
element will be rendered at all. By default, returns False
|
|
(inhibiting rendering) if the datum is None.
|
|
|
|
:param datum: The datum associated with this template element.
|
|
"""
|
|
|
|
# Don't render if datum is None
|
|
return datum is not None
|
|
|
|
def _text_get(self):
|
|
"""Template element text.
|
|
|
|
Either None or a callable taking an object and optional
|
|
boolean do_raise indicator and returning the datum bound to
|
|
the text of the template element.
|
|
"""
|
|
|
|
return self._text
|
|
|
|
def _text_set(self, value):
|
|
# Convert value to a selector
|
|
if value is not None and not callable(value):
|
|
value = Selector(value)
|
|
|
|
self._text = value
|
|
|
|
def _text_del(self):
|
|
self._text = None
|
|
|
|
text = property(_text_get, _text_set, _text_del)
|
|
|
|
def tree(self):
|
|
"""Return string representation of the template tree.
|
|
|
|
Returns a representation of the template rooted at this
|
|
element as a string, suitable for inclusion in debug logs.
|
|
"""
|
|
|
|
# Build the inner contents of the tag...
|
|
contents = [self.tag, '!selector=%r' % self.selector]
|
|
|
|
# Add the text...
|
|
if self.text is not None:
|
|
contents.append('!text=%r' % self.text)
|
|
|
|
# Add all the other attributes
|
|
for key, value in self.attrib.items():
|
|
contents.append('%s=%r' % (key, value))
|
|
|
|
# If there are no children, return it as a closed tag
|
|
if len(self) == 0:
|
|
return '<%s/>' % ' '.join([str(i) for i in contents])
|
|
|
|
# OK, recurse to our children
|
|
children = [c.tree() for c in self]
|
|
|
|
# Return the result
|
|
return ('<%s>%s</%s>' %
|
|
(' '.join(contents), ''.join(children), self.tag))
|
|
|
|
|
|
def SubTemplateElement(parent, tag, attrib=None, selector=None,
|
|
subselector=None, **extra):
|
|
"""Create a template element as a child of another.
|
|
|
|
Corresponds to the etree.SubElement interface. Parameters are as
|
|
for TemplateElement, with the addition of the parent.
|
|
"""
|
|
|
|
# Convert attributes
|
|
attrib = attrib or {}
|
|
attrib.update(extra)
|
|
|
|
# Get a TemplateElement
|
|
elem = TemplateElement(tag, attrib=attrib, selector=selector,
|
|
subselector=subselector)
|
|
|
|
# Append the parent safely
|
|
if parent is not None:
|
|
parent.append(elem)
|
|
|
|
return elem
|
|
|
|
|
|
class Template(object):
|
|
"""Represent a template."""
|
|
|
|
def __init__(self, root, nsmap=None):
|
|
"""Initialize a template.
|
|
|
|
:param root: The root element of the template.
|
|
:param nsmap: An optional namespace dictionary to be
|
|
associated with the root element of the
|
|
template.
|
|
"""
|
|
|
|
self.root = root.unwrap() if root is not None else None
|
|
self.nsmap = nsmap or {}
|
|
self.serialize_options = dict(encoding='UTF-8', xml_declaration=True)
|
|
|
|
def _serialize(self, parent, obj, siblings, nsmap=None):
|
|
"""Internal serialization.
|
|
|
|
Recursive routine to build a tree of etree.Element instances
|
|
from an object based on the template. Returns the first
|
|
etree.Element instance rendered, or None.
|
|
|
|
:param parent: The parent etree.Element instance. Can be
|
|
None.
|
|
:param obj: The object to render.
|
|
:param siblings: The TemplateElement instances against which
|
|
to render the object.
|
|
:param nsmap: An optional namespace dictionary to be
|
|
associated with the etree.Element instance
|
|
rendered.
|
|
"""
|
|
|
|
# First step, render the element
|
|
elems = siblings[0].render(parent, obj, siblings[1:], nsmap)
|
|
|
|
# Now, traverse all child elements
|
|
seen = set()
|
|
for idx, sibling in enumerate(siblings):
|
|
for child in sibling:
|
|
# Have we handled this child already?
|
|
if child.tag in seen:
|
|
continue
|
|
seen.add(child.tag)
|
|
|
|
# Determine the child's siblings
|
|
nieces = [child]
|
|
for sib in siblings[idx + 1:]:
|
|
if child.tag in sib:
|
|
nieces.append(sib[child.tag])
|
|
|
|
# Now call this function for all data elements recursively
|
|
for elem, datum in elems:
|
|
self._serialize(elem, datum, nieces)
|
|
|
|
# Return the first element; at the top level, this will be the
|
|
# root element
|
|
if elems:
|
|
return elems[0][0]
|
|
|
|
def serialize(self, obj, *args, **kwargs):
|
|
"""Serialize an object.
|
|
|
|
Serializes an object against the template. Returns a string
|
|
with the serialized XML. Positional and keyword arguments are
|
|
passed to etree.tostring().
|
|
|
|
:param obj: The object to serialize.
|
|
"""
|
|
|
|
elem = self.make_tree(obj)
|
|
if elem is None:
|
|
return ''
|
|
|
|
for k, v in self.serialize_options.items():
|
|
kwargs.setdefault(k, v)
|
|
|
|
# Serialize it into XML
|
|
return etree.tostring(elem, *args, **kwargs)
|
|
|
|
def make_tree(self, obj):
|
|
"""Create a tree.
|
|
|
|
Serializes an object against the template. Returns an Element
|
|
node with appropriate children.
|
|
|
|
:param obj: The object to serialize.
|
|
"""
|
|
|
|
# If the template is empty, return the empty string
|
|
if self.root is None:
|
|
return None
|
|
|
|
# Get the siblings and nsmap of the root element
|
|
siblings = self._siblings()
|
|
nsmap = self._nsmap()
|
|
|
|
# Form the element tree
|
|
return self._serialize(None, obj, siblings, nsmap)
|
|
|
|
def _siblings(self):
|
|
"""Hook method for computing root siblings.
|
|
|
|
An overridable hook method to return the siblings of the root
|
|
element. By default, this is the root element itself.
|
|
"""
|
|
|
|
return [self.root]
|
|
|
|
def _nsmap(self):
|
|
"""Hook method for computing the namespace dictionary.
|
|
|
|
An overridable hook method to return the namespace dictionary.
|
|
"""
|
|
|
|
return self.nsmap.copy()
|
|
|
|
def unwrap(self):
|
|
"""Unwraps a template to return a template element."""
|
|
|
|
# Return the root element
|
|
return self.root
|
|
|
|
def wrap(self):
|
|
"""Wraps a template element to return a template."""
|
|
|
|
# We are a template
|
|
return self
|
|
|
|
def apply(self, master):
|
|
"""Hook method for determining slave applicability.
|
|
|
|
An overridable hook method used to determine if this template
|
|
is applicable as a slave to a given master template.
|
|
|
|
:param master: The master template to test.
|
|
"""
|
|
|
|
return True
|
|
|
|
def tree(self):
|
|
"""Return string representation of the template tree.
|
|
|
|
Returns a representation of the template as a string, suitable
|
|
for inclusion in debug logs.
|
|
"""
|
|
|
|
return "%r: %s" % (self, self.root.tree())
|
|
|
|
|
|
class MasterTemplate(Template):
|
|
"""Represent a master template.
|
|
|
|
Master templates are versioned derivatives of templates that
|
|
additionally allow slave templates to be attached. Slave
|
|
templates allow modification of the serialized result without
|
|
directly changing the master.
|
|
"""
|
|
|
|
def __init__(self, root, version, nsmap=None):
|
|
"""Initialize a master template.
|
|
|
|
:param root: The root element of the template.
|
|
:param version: The version number of the template.
|
|
:param nsmap: An optional namespace dictionary to be
|
|
associated with the root element of the
|
|
template.
|
|
"""
|
|
|
|
super(MasterTemplate, self).__init__(root, nsmap)
|
|
self.version = version
|
|
self.slaves = []
|
|
|
|
def __repr__(self):
|
|
"""Return string representation of the template."""
|
|
|
|
return ("<%s.%s object version %s at %#x>" %
|
|
(self.__class__.__module__, self.__class__.__name__,
|
|
self.version, id(self)))
|
|
|
|
def _siblings(self):
|
|
"""Hook method for computing root siblings.
|
|
|
|
An overridable hook method to return the siblings of the root
|
|
element. This is the root element plus the root elements of
|
|
all the slave templates.
|
|
"""
|
|
|
|
return [self.root] + [slave.root for slave in self.slaves]
|
|
|
|
def _nsmap(self):
|
|
"""Hook method for computing the namespace dictionary.
|
|
|
|
An overridable hook method to return the namespace dictionary.
|
|
The namespace dictionary is computed by taking the master
|
|
template's namespace dictionary and updating it from all the
|
|
slave templates.
|
|
"""
|
|
|
|
nsmap = self.nsmap.copy()
|
|
for slave in self.slaves:
|
|
nsmap.update(slave._nsmap())
|
|
return nsmap
|
|
|
|
def attach(self, *slaves):
|
|
"""Attach one or more slave templates.
|
|
|
|
Attaches one or more slave templates to the master template.
|
|
Slave templates must have a root element with the same tag as
|
|
the master template. The slave template's apply() method will
|
|
be called to determine if the slave should be applied to this
|
|
master; if it returns False, that slave will be skipped.
|
|
(This allows filtering of slaves based on the version of the
|
|
master template.)
|
|
"""
|
|
|
|
slave_list = []
|
|
for slave in slaves:
|
|
slave = slave.wrap()
|
|
|
|
# Make sure we have a tree match
|
|
if slave.root.tag != self.root.tag:
|
|
msg = (_("Template tree mismatch; adding slave %(slavetag)s "
|
|
"to master %(mastertag)s") %
|
|
{'slavetag': slave.root.tag,
|
|
'mastertag': self.root.tag})
|
|
raise ValueError(msg)
|
|
|
|
# Make sure slave applies to this template
|
|
if not slave.apply(self):
|
|
continue
|
|
|
|
slave_list.append(slave)
|
|
|
|
# Add the slaves
|
|
self.slaves.extend(slave_list)
|
|
|
|
def copy(self):
|
|
"""Return a copy of this master template."""
|
|
|
|
# Return a copy of the MasterTemplate
|
|
tmp = self.__class__(self.root, self.version, self.nsmap)
|
|
tmp.slaves = self.slaves[:]
|
|
return tmp
|
|
|
|
|
|
class SlaveTemplate(Template):
|
|
"""Represent a slave template.
|
|
|
|
Slave templates are versioned derivatives of templates. Each
|
|
slave has a minimum version and optional maximum version of the
|
|
master template to which they can be attached.
|
|
"""
|
|
|
|
def __init__(self, root, min_vers, max_vers=None, nsmap=None):
|
|
"""Initialize a slave template.
|
|
|
|
:param root: The root element of the template.
|
|
:param min_vers: The minimum permissible version of the master
|
|
template for this slave template to apply.
|
|
:param max_vers: An optional upper bound for the master
|
|
template version.
|
|
:param nsmap: An optional namespace dictionary to be
|
|
associated with the root element of the
|
|
template.
|
|
"""
|
|
|
|
super(SlaveTemplate, self).__init__(root, nsmap)
|
|
self.min_vers = min_vers
|
|
self.max_vers = max_vers
|
|
|
|
def __repr__(self):
|
|
"""Return string representation of the template."""
|
|
|
|
return ("<%s.%s object versions %s-%s at %#x>" %
|
|
(self.__class__.__module__, self.__class__.__name__,
|
|
self.min_vers, self.max_vers, id(self)))
|
|
|
|
def apply(self, master):
|
|
"""Hook method for determining slave applicability.
|
|
|
|
An overridable hook method used to determine if this template
|
|
is applicable as a slave to a given master template. This
|
|
version requires the master template to have a version number
|
|
between min_vers and max_vers.
|
|
|
|
:param master: The master template to test.
|
|
"""
|
|
|
|
# Does the master meet our minimum version requirement?
|
|
if master.version < self.min_vers:
|
|
return False
|
|
|
|
# How about our maximum version requirement?
|
|
if self.max_vers is not None and master.version > self.max_vers:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class TemplateBuilder(object):
|
|
"""Template builder.
|
|
|
|
This class exists to allow templates to be lazily built without
|
|
having to build them each time they are needed. It must be
|
|
subclassed, and the subclass must implement the construct()
|
|
method, which must return a Template (or subclass) instance. The
|
|
constructor will always return the template returned by
|
|
construct(), or, if it has a copy() method, a copy of that
|
|
template.
|
|
"""
|
|
|
|
_tmpl = None
|
|
|
|
def __new__(cls, copy=True):
|
|
"""Construct and return a template.
|
|
|
|
:param copy: If True (the default), a copy of the template
|
|
will be constructed and returned, if possible.
|
|
"""
|
|
|
|
# Do we need to construct the template?
|
|
if cls._tmpl is None:
|
|
tmp = super(TemplateBuilder, cls).__new__(cls)
|
|
|
|
# Construct the template
|
|
cls._tmpl = tmp.construct()
|
|
|
|
# If the template has a copy attribute, return the result of
|
|
# calling it
|
|
if copy and hasattr(cls._tmpl, 'copy'):
|
|
return cls._tmpl.copy()
|
|
|
|
# Return the template
|
|
return cls._tmpl
|
|
|
|
def construct(self):
|
|
"""Construct a template.
|
|
|
|
Called to construct a template instance, which it must return.
|
|
Only called once.
|
|
"""
|
|
|
|
raise NotImplementedError(_("subclasses must implement construct()!"))
|
|
|
|
|
|
def make_links(parent, selector=None):
|
|
"""Attach an Atom <links> element to the parent."""
|
|
|
|
elem = SubTemplateElement(parent, '{%s}link' % XMLNS_ATOM,
|
|
selector=selector)
|
|
elem.set('rel')
|
|
elem.set('type')
|
|
elem.set('href')
|
|
|
|
# Just for completeness...
|
|
return elem
|
|
|
|
|
|
def make_flat_dict(name, selector=None, subselector=None, ns=None):
|
|
"""Utility for simple XML templates.
|
|
|
|
Simple templates are templates that traditionally used
|
|
XMLDictSerializer with no metadata.
|
|
|
|
Returns a template element where the top-level element has the
|
|
given tag name, and where sub-elements have tag names derived
|
|
from the object's keys and text derived from the object's values.
|
|
|
|
This only works for flat dictionary objects, not dictionaries
|
|
containing nested lists or dictionaries.
|
|
"""
|
|
|
|
# Set up the names we need...
|
|
if ns is None:
|
|
elemname = name
|
|
tagname = Selector(0)
|
|
else:
|
|
elemname = '{%s}%s' % (ns, name)
|
|
tagname = lambda obj, do_raise=False: '{%s}%s' % (ns, obj[0])
|
|
|
|
if selector is None:
|
|
selector = name
|
|
|
|
# Build the root element
|
|
root = TemplateElement(elemname, selector=selector,
|
|
subselector=subselector)
|
|
|
|
# Build an element to represent all the keys and values
|
|
elem = SubTemplateElement(root, tagname, selector=get_items)
|
|
elem.text = 1
|
|
|
|
# Return the template
|
|
return root
|