Source code for aglyph.context

# -*- coding: UTF-8 -*-

# Copyright (c) 2006, 2011, 2013-2018 Matthew Zipay.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

"""The classes in this module are used to define collections
("contexts") of related components and templates.

A context can be created in pure Python using the following API classes:

* :class:`aglyph.component.Template`
* :class:`aglyph.component.Component`
* :class:`aglyph.component.Reference` (used to indicate that one
  component depends on another component)
* :class:`aglyph.component.Evaluator` (used like a partial function to
  lazily evaluate component initialization arguments and attributes)
* :class:`aglyph.context.Context` or a subclass

.. versionadded:: 3.0.0
   For easier programmatic configuration, refer to
   :doc:`context-fluent-api`.

Alternatively, a context can be defined using a declarative XML syntax
that conforms to the :download:`Aglyph context DTD
<../../resources/aglyph-context.dtd>` (included in the *resources/*
directory of the distribution). This approach requires only the
:class:`aglyph.context.XMLContext` class, which parses the XML document
and then uses the API classes mentioned above to populate the context.

"""

__author__ = "Matthew Zipay <mattz@ninthtest.info>"

from ast import literal_eval
from collections import OrderedDict
from functools import partial
import logging
import sys
import xml.etree.ElementTree as ET

from autologging import logged, traced

from aglyph import AglyphError, _identify, __version__
from aglyph._compat import (
    AglyphDefaultXMLParser,
    DataType,
    DoctypeTreeBuilder,
    is_python_3,
    name_of,
    TextType,
)
from aglyph.component import (
    Component,
    Evaluator as evaluate,
    Reference as ref,
    Strategy,
    Template,
)

__all__ = ["Context", "evaluate", "ref", "XMLContext"]

_log = logging.getLogger(__name__)


@traced
@logged
class _ContextBuilder(object):
    """Entry points for the Aglyph Context fluent API."""

[docs] def component(self, component_id_spec, parent=None): """Return a fluent :class:`Component` builder for *component_id_spec*. :arg component_id_spec: a context-unique identifier for this component; or the object whose dotted name will identify this component :keyword parent: the context-unique identifier for this component's parent template or component definition; or the object whose dotted name identifies this component's parent definition .. versionadded:: 3.0.0 This method is an entry point into :doc:`context-fluent-api`. """ return _ComponentBuilder( self, component_id_spec, parent=parent)
[docs] def prototype(self, component_id_spec, parent=None): """Return a :data:`prototype <aglyph.component.Strategy>` :class:`Component` builder for a component identified by *component_id_spec*. :arg component_id_spec: a context-unique identifier for this component; or the object whose dotted name will identify this component :keyword parent: the context-unique identifier for this component's parent template or component definition; or the object whose dotted name identifies this component's parent definition .. versionadded:: 3.0.0 This method is an entry point into :doc:`context-fluent-api`. """ return self.component( component_id_spec, parent=parent).create( strategy="prototype")
[docs] def singleton(self, component_id_spec, parent=None): """Return a :data:`singleton <aglyph.component.Strategy>` :class:`Component` builder for a component identified by *component_id_spec*. :arg component_id_spec: a context-unique identifier for this component; or the object whose dotted name will identify this component :keyword parent: the context-unique identifier for this component's parent template or component definition; or the object whose dotted name identifies this component's parent definition .. versionadded:: 3.0.0 This method is an entry point into :doc:`context-fluent-api`. """ return self.component( component_id_spec, parent=parent).create( strategy="singleton")
[docs] def borg(self, component_id_spec, parent=None): """Return a :data:`borg <aglyph.component.Strategy>` :class:`Component` builder for a component identified by *component_id_spec*. :arg component_id_spec: a context-unique identifier for this component; or the object whose dotted name will identify this component :keyword parent: the context-unique identifier for this component's parent template or component definition; or the object whose dotted name identifies this component's parent definition .. versionadded:: 3.0.0 This method is an entry point into :doc:`context-fluent-api`. """ return self.component( component_id_spec, parent=parent).create( strategy="borg")
[docs] def weakref(self, component_id_spec, parent=None): """Return a :data:`weakref <aglyph.component.Strategy>` :class:`Component` builder for a component identified by *component_id_spec*. :arg component_id_spec: a context-unique identifier for this component; or the object whose dotted name will identify this component :keyword parent: the context-unique identifier for this component's parent template or component definition; or the object whose dotted name identifies this component's parent definition .. versionadded:: 3.0.0 This method is an entry point into :doc:`context-fluent-api`. """ return self.component( component_id_spec, parent=parent).create( strategy="weakref")
[docs] def template(self, template_id_spec, parent=None): """Return a :class:`Template` builder for a template identified by *template_spec*. :arg template_id_spec: a context-unique identifier for this template; or the object whose dotted name will identify this template :keyword parent: the context-unique identifier for this template's parent template or component definition; or the object whose dotted name identifies this template's parent definition .. versionadded:: 3.0.0 This method is an entry point into :doc:`context-fluent-api`. """ return _TemplateBuilder( self, template_id_spec, parent=parent)
@traced @logged class _CreationBuilderMixin(object): """Methods to describe object creation.""" __slots__ = []
[docs] def create( self, dotted_name=None, factory=None, member=None, strategy=None): """Specify the object creation aspects of a component being defined. :keyword dotted_name: an **importable** dotted name or an object whose dotted name will be introspected :keyword factory: names a :obj:`callable` member of the object represented by the dotted name :keyword member: names **any** member of the object represented by the dotted name :keyword strategy: specifies the component assembly strategy :return: *self* (to support chained calls) Any keyword whose value is ``None`` will be ignored (i.e. ``None`` values are not explicitly set). .. note:: The *member* and *strategy* keywords should be treated as mutually exclusive. Any component definition that specifies a *member* is implicitly assigned the special strategy "_imported". """ # do not explicitly assign None values; calls can be chained if dotted_name is not None: self._dotted_name_spec = dotted_name if factory is not None: self._factory_name = factory if member is not None: self._member_name = member if strategy is not None: self._strategy = strategy return self
@traced @logged class _InjectionBuilderMixin(object): """Methods to describe injected dependencies.""" __slots__ = []
[docs] def init(self, *args, **keywords): """Specify the initialization arguments (positional and keyword) for templates and/or components. :arg tuple args: the positional initialization arguments :arg dict keywords: the keyword initialization arguments :return: *self* (to support chained calls) .. note:: Successive calls to this method on the same instance have a cumulative effect; the list of positional arguments is extended, and the dictionary of keyword arguments is updated. """ self._args.extend(args) self._keywords.update(keywords) return self
[docs] def set(self, *pairs, **attributes): """Specify the setter (method/attribute/property) depdendencies for tempaltes and/or components. :arg pairs: a sequence of (name, value) 2-tuples (optional) :arg attributes: a mapping of name->value dependencies :return: *self* (to support chained calls) .. note:: Successive calls to this method on the same instance have a cumulative effect (i.e. the attributes mapping is updated). """ self._attributes.update(pairs, **attributes) return self
@traced @logged class _LifecycleBuilderMixin(object): """Methods to describe lifecyle method names.""" __slots__ = []
[docs] def call(self, after_inject=None, before_clear=None): """Specify the names of lifecycle methods to be called for templates and/or components. :arg after_inject: the name of the method to call after a component has been assembled but before it is returned to the caller :arg before_clear: the name of the method to call immediately before a *singleton*, *borg*, or *weakref* object is evicted from the internal cache :return: *self* (to support chained calls) Any keyword whose value is ``None`` will be ignored (i.e. ``None`` values are not explicitly set). """ # do not explicitly assign None values; calls can be chained if after_inject is not None: self._after_inject = after_inject if before_clear is not None: self._before_clear = before_clear return self
@traced @logged class _RegistrationMixin(object): """Methods to handle registering a template or component in a context. """ __slots__ = []
[docs] def register(self): """Add a component or template definition to a context. :return: ``None`` (terminates the fluent call sequence) """ definition = self._init_definition() definition._args = self._args definition._keywords = self._keywords definition._attributes = self._attributes self._context.register(definition)
# implicit return None terminates this fluent call sequence def _init_definition(self): """Create the :class:`Component` or :class:`Template` definition that will be registered in the :class:`Context` """ raise NotImplementedError() @traced @logged class _TemplateBuilder( _InjectionBuilderMixin, _LifecycleBuilderMixin, _RegistrationMixin): """Fluent context builder for :class:`Template` definitions.""" __slots__ = [ "_context", "_unique_id_spec", "_parent_id_spec", "_args", "_keywords", "_attributes", "_after_inject", "_before_clear", ] def __init__(self, context, unique_id_spec, parent=None): self._context = context self._unique_id_spec = unique_id_spec self._parent_id_spec = parent self._args = [] self._keywords = {} self._attributes = OrderedDict() self._after_inject = None self._before_clear = None def _init_definition(self): return Template( _identify(self._unique_id_spec), parent_id= _identify(self._parent_id_spec) if self._parent_id_spec is not None else None, after_inject=self._after_inject, before_clear=self._before_clear) @traced @logged class _ComponentBuilder(_CreationBuilderMixin, _TemplateBuilder): """Fluent context builder for :class:`Component` definitions.""" __slots__ = [ "_dotted_name_spec", "_factory_name", "_member_name", "_strategy", ] def __init__(self, context, unique_id_spec, parent=None): _TemplateBuilder.__init__( self, context, unique_id_spec, parent=parent) self._dotted_name_spec = None self._factory_name = None self._member_name = None self._strategy = None def _init_definition(self): return Component( _identify(self._unique_id_spec), dotted_name= _identify(self._dotted_name_spec) if self._dotted_name_spec is not None else None, factory_name=self._factory_name, member_name=self._member_name, strategy=self._strategy, parent_id= _identify(self._parent_id_spec) if self._parent_id_spec is not None else None, after_inject=self._after_inject, before_clear=self._before_clear)
[docs]@traced @logged class Context(dict, _ContextBuilder): """A mapping of unique IDs to :class:`Component` and :class:`Template` objects. """ def __init__(self, context_id, after_inject=None, before_clear=None): """ :arg str context_id: an identifier for this context :keyword str after_inject: specifies the name of the method that will be called (if it exists) on **all** component objects after all of their dependencies have been injected :keyword str before_clear: specifies the name of the method that will be called (if it exists) on **all** singleton, borg, and weakref objects immediately before they are cleared from cache """ #PYVER: arguments to super() are implicit under Python 3 super(Context, self).__init__() if not context_id: raise AglyphError( "%s ID must not be None or empty" % name_of(self.__class__)) self._context_id = context_id self._after_inject = after_inject self._before_clear = before_clear @property def context_id(self): """The context identifier *(read-only)*.""" return self._context_id @property def after_inject(self): """The name of the component object method that will be called after **all** dependencies have been injected into that component object *(read-only)*. """ return self._after_inject @property def before_clear(self): """The name of the component object method that will be called immediately before the object is cleared from cache *(read-only)*. .. warning:: This property is not applicable to "prototype" component objects, or to component objects acquired via :attr:`Component.member_name`. The named method is **not guaranteed** to be called for "weakref" component objects. """ return self._before_clear
[docs] def register(self, definition): """Add a component or template *definition* to this context. :arg definition: a :class:`Component` or :class:`Template` object :raise AglyphError: if a component or template with the same unique ID is already registered in this context .. note:: To **replace** an already-registered component or template with the same unique ID, use :meth:`dict.__setitem__` directly. """ if definition.unique_id in self: raise AglyphError( "%s with ID %r already mapped in %s" % ( name_of(definition.__class__), definition.unique_id, self)) self[definition.unique_id] = definition
[docs] def get_component(self, component_id): """Return the :class:`Component` identified by *component_id*. :arg str component_id: a unique ID that identifies a :class:`Component` :return: the :class:`Component` identified by *component_id* :rtype: :class:`Component` if *component_id* is mapped, else ``None`` """ obj = self.get(component_id) return obj if isinstance(obj, Component) else None
[docs] def iter_components(self, strategy=None): """Yield all definitions in this context that are instances of :class:`Component`, optionally filtered by *strategy*. :keyword str strategy: only yield component definitions that use this assembly strategy (by default, **all** component definitions are yielded) :return: a :class:`Component` generator """ for obj in self.values(): if (isinstance(obj, Component) and (strategy in [None, obj.strategy])): yield obj
def __str__(self): return "<%s %r @%08x>" % ( name_of(self.__class__), self._context_id, id(self)) def __repr__(self): return "%s.%s(%r, after_inject=%r, before_clear=%r)" % ( self.__class__.__module__, name_of(self.__class__), self._context_id, self._after_inject, self._before_clear)
[docs]@traced @logged class XMLContext(Context): """A mapping of unique IDs to :class:`Component` and :class:`Template` objects. Components and templates are declared in an XML document that conforms to the :download:`Aglyph context DTD <../../resources/aglyph-context.dtd>` (included in the *resources/* directory of the distribution). """ def __init__( self, source, parser=None, default_encoding=sys.getdefaultencoding()): """ :arg source: a filename or stream from which XML data is read :keyword xml.etree.ElementTree.XMLParser parser: the ElementTree parser to use (instead of Aglyph's default) :keyword str default_encoding: the default character set used to encode certain element content :raise AglyphError: if unexpected elements are encountered, or if expected elements are *not* encountered, in the document structure In most cases, *parser* should be left unspecified. Aglyph's default parser will be sufficient for all but extreme edge cases. *default_encoding* is the character set used to encode ``<bytes>`` (or ``<str>`` under Python 2) element content when an **@encoding** attribute is *not* specified on those elements. It defaults to the system-dependent value of :func:`sys.getdefaultencoding`. **This is not related to the document encoding!** .. note:: Aglyph uses a non-validating XML parser by default, so DTD conformance is **not** enforced at runtime. It is recommended that XML contexts be validated at least once (manually) during testing. An :class:`AglyphError` *will* be raised under certain conditions (an unexpected element is encounted, or an expected element is *not* encountered), but Aglyph does not "reinvent the wheel" by implementing strict validation in the parsing logic. .. warning:: Although Aglyph contexts are :class:`dict` types, ``XMLContext`` does not permit the same unique ID to be (re-)mapped multiple times. Attempting to define more than one ``<component>`` or ``<template>`` with the same ID will raise :class:`AglyphError` when the document is parsed. **After** an Aglyph ``<context>`` document has been successfully parsed, a unique component or template ID can be re-mapped using standard :class:`dict` protocols. .. seealso:: Validity constraint: ID https://www.w3.org/TR/REC-xml/#id """ if parser is None: parser = AglyphDefaultXMLParser(target=DoctypeTreeBuilder()) tree = ET.parse(source, parser=parser) root = tree.getroot() if root.tag != "context": raise AglyphError("expected root <context>, not <%s>" % root.tag) #PYVER: arguments to super() are implicit under Python 3 super(XMLContext, self).__init__( root.get("id"), after_inject=root.get("after-inject"), before_clear=root.get("before-clear")) # alias the correct _parse_str method based on Python version if is_python_3: self._parse_str = self.__parse_str_as_text else: self._parse_str = self.__parse_str_as_data self._default_encoding = default_encoding for element in list(root): if element.tag == "component": depsupport = self._create_component(element) elif element.tag == "template": depsupport = self._create_template(element) else: raise AglyphError( "unexpected element: /context/%s" % element.tag) self.register(depsupport) self._process_dependencies(depsupport, element) self.__repr = "%s.%s(%r, parser=%r, default_encoding=%r)" % ( self.__class__.__module__, name_of(self.__class__), source, parser, default_encoding) @property def default_encoding(self): """The default encoding of ``<bytes>`` (or ``<str>`` under Python 2) element content when an **@encoding** attribute is *not* specified. .. note:: This is **unrelated** to the document encoding! """ return self._default_encoding def _create_template(self, template_element): """Create a template object from a ``<template>`` element. :arg xml.etree.ElementTree.Element template_element: a ``<template>`` element :return: an Aglyph template object :rtype: :class:`aglyph.component.Template` """ return Template( template_element.get("id"), parent_id=template_element.get("parent-id"), after_inject=template_element.get("after-inject"), before_clear=template_element.get("before-clear") ) def _create_component(self, component_element): """Create a component object from a ``<component>`` element. :arg xml.etree.ElementTree.Element component_element: a ``<component>`` element :return: an Aglyph component object :rtype: :class:`aglyph.component.Component` """ return Component( component_element.get("id"), dotted_name=component_element.get("dotted-name"), factory_name=component_element.get("factory-name"), member_name=component_element.get("member-name"), strategy=component_element.get("strategy"), parent_id=component_element.get("parent-id"), after_inject=component_element.get("after-inject"), before_clear=component_element.get("before-clear") ) def _process_dependencies(self, depsupport, depsupport_element): """Parse the child elements of *depsupport_element* to populate the *depsupport* initialization arguments and attributess. :arg depsupport: a :class:`Template` or :class:`Component` :arg xml.etree.ElementTree.Element depsupport_element: the ``<template>`` or ``<component>`` that was parsed to create *depsupport* """ children = list(depsupport_element) child_tags = [elem.tag for elem in children] if child_tags == ["init"]: init_element = children[0] attributes_element = None elif child_tags == ["init", "attributes"]: init_element, attributes_element = children elif child_tags == ["attributes"]: init_element = None attributes_element = children[0] elif not child_tags: init_element = None attributes_element = None else: dtag = depsupport_element.tag raise AglyphError( "unexpected element: %s/%s" % (depsupport_element.tag, child_tags[0])) if init_element is not None: for (keyword, value) in self._process_init(init_element): if keyword is None: depsupport.args.append(value) else: depsupport.keywords[keyword] = value if attributes_element is not None: for (name, value) in self._process_attributes(attributes_element): depsupport.attributes[name] = value self.__log.debug( "%r has args=%r, keywords=%r, attributess=%r", depsupport, depsupport.args, depsupport.keywords, depsupport.attributes) def _process_init(self, init_element): """Yield initialization arguments (positional and keyword) parsed from *init_element*. :arg xml.etree.ElementTree.Element init_element: an ``<init>`` element :return: an iterator that yields the 2-tuple ``(keyword, value)`` .. note:: Both positional and keyword arguments are yielded by this method as a 2-tuple ``(keyword, value)``. For positional arguments, ``keyword`` will be ``None``. """ for element in list(init_element): if element.tag != "arg": raise AglyphError("unexpected element: init/%s" % element.tag) keyword = element.get("keyword") if keyword == "": raise AglyphError("arg/@keyword cannot be empty") value = self._unserialize_element_value(element) yield (keyword, value) def _process_attributes(self, attributes_element): """Yield attributes (fields, setter methods, or properties) parsed from *attributes_element*. :arg xml.etree.ElementTree.Element attributes_element: an ``<attributes>`` element :return: an iterator that yields the 2-tuple ``(name, value)`` """ for element in list(attributes_element): if element.tag != "attribute": raise AglyphError( "unexpected element: attributes/%s" % element.tag) name = element.get("name") if not name: raise AglyphError( "attribute/@name is required and cannot be empty") value = self._unserialize_element_value(element) yield (name, value) def _unserialize_element_value(self, valuecontainer_element): """Return the appropriate object, value, Aglyph reference, or Aglyph evaluator for *element*. :arg xml.etree.ElementTree.Element valuecontainer_element: an element with a single child element that describes a value :return: the runtime object that is the result of processing *valuecontainer_element* :rtype: an object of a Python built-in type, a Python built-in constant, a :class:`Reference`, or an :class:`Evaluator` *valuecontainer_element* must be an ``<arg>``, ``<attribute>``, ``<key>``, or ``<value>`` element. """ component_id = valuecontainer_element.get("reference") if component_id is not None: return ref(component_id) children = list(valuecontainer_element) if len(children) != 1: vtag = valuecontainer_element.tag raise AglyphError( "<%s> must contain exactly one child element; found %s" % ( vtag, ", ".join("%s/%s" % (vtag, c.tag) for c in children) if children else "no children")) return self._process_value_element( children[0], valuecontainer_element.tag) def _process_value_element(self, value_element, parent_tag): """Create a usable Python object from *value_element*. :arg xml.etree.ElementTree.Element value_element: an element that describes a value :arg str parent_tag: the name of the *value_element* parent element :return: a Python object that is the value of *value_element* *value_element* must be a ``<False />``, ``<True />``, ``<None />``, ``<bytes>``, ``<str>``, ``<unicode>``, ``<int>``, ``<float>``, ``<tuple>``, ``<list>``, ``<dict>``, or ``<set>`` element. This method will return one of the following types, dependent upon the element: * an object of a Python built-in type * a Python built-in constant * an Aglyph :class:`Reference` * an Aglyph :class:`Evaluator` """ parse_value = getattr(self, "_parse_%s" % value_element.tag, None) if parse_value is None: raise AglyphError( "unexpected element: %s/%s" % (parent_tag, value_element.tag)) return parse_value(value_element) def _parse_False(self, false_element): """Return the builtin constant ``False``. :arg xml.etree.ElementTree.Element false_element: a ``<False />`` element """ return False def _parse_True(self, true_element): """Return the builtin constant ``True``. :arg xml.etree.ElementTree.Element true_element: a ``<True />`` element """ return True def _parse_None(self, none_element): """Return the builtin constant ``None``. :arg xml.etree.ElementTree.Element none_element: a ``<None />`` element """ return None def _parse_bytes(self, bytes_element): """Return an encoded bytes object parsed from *bytes_element*. :arg xml.etree.ElementTree.Element bytes_element: a ``<bytes>`` element :rtype: :obj:`bytes` (Python 3) or :obj:`str` (Python 2) If the **bytes/@encoding** attribute is set, the text of the ``<bytes>`` element is encoded using the specified character set; otherwise, the text of the ``<bytes>`` element is encoded using :attr:`default_encoding`. Whitespace in the ``<bytes>`` element content is preserved. If *bytes_element* represents the empty element ``<bytes />``, ``bytes()`` (Python 3) or ``str()`` (Python 2) is returned. """ if bytes_element.text is not None: encoding = bytes_element.get("encoding", self._default_encoding) # .encode() will return the appropriate type return bytes_element.text.encode(encoding) else: return DataType() def __parse_str_as_data(self, str_element): """Return an encoded bytes object parsed from *str_element*. :arg xml.etree.ElementTree.Element str_element: a ``<str>`` element :rtype: :obj:`str` (Python 2) .. note:: This method is aliased as ``_parse_str`` when Aglyph is running under Python 2. If the **str/@encoding** attribute has been set, the text of the ``<str>`` element is encoded using the specified character set; otherwise, the text of the ``<str>`` element is encoded using :attr:`default_encoding`. Whitespace in the ``<str>`` element content is preserved. If *str_element* represents the empty element ``<str />``, ``str()`` is returned. """ if str_element.text is not None: encoding = str_element.get("encoding", self._default_encoding) return str_element.text.encode(encoding) else: return str() def __parse_str_as_text(self, str_element): """Return a Unicode text object parsed from *str_element*. :arg xml.etree.ElementTree.Element str_element: a ``<str>`` element :rtype: :obj:`str` (Python 3) .. note:: This method is aliased as ``_parse_str`` when Aglyph is running under Python 3. The text of the ``<str>`` element (which is already a Unicode string) is returned unchanged. Whitespace in the ``<str>`` element content is preserved. If *str_element* represents the empty element ``<str />``, ``str()`` is returned. """ if str_element.text is not None: encoding = str_element.get("encoding") if encoding is not None: self.__log.warning( "ignoring str/@encoding attribute %r (Python 3)", encoding) return str_element.text else: return str() def _parse_unicode(self, unicode_element): """Return a Unicode text object parsed from *unicode_element*. :arg xml.etree.ElementTree.Element unicode_element: a ``<unicode>`` element :rtype: :obj:`str` (Python 3) or :obj:`unicode` (Python 2) The text of the ``<unicode>`` element (which is already a Unicode string) is returned unchanged. Whitespace in the ``<unicode>`` element content is preserved. If *unicode_element* represents the empty element ``<unicode />``, ``str()`` (Python 3) or ``unicode()`` (Python 2) is returned. """ if unicode_element.text is not None: return unicode_element.text else: return TextType() def _parse_int(self, int_element): """Return a builtin integer object parsed from *int_element*. :arg xml.etree.ElementTree.Element int_element: an ``<int>`` element :rtype: :obj:`int` The **int/@base** attribute, if specified, is used as the number base to interpret the content of *int_element*. If *int_element* represents the empty element ``<int />``, ``int()`` is returned. .. warning:: This method **may** return :obj:`long` in Python 2! """ if int_element.text is not None: base = int(int_element.get("base", "10")) #PYVER: IronPython 2.7 does not accept a 'base=' keyword argument return int(int_element.text, base) else: return int() def _parse_float(self, float_element): """Return a builtin floating-point object parsed from *float_element*. :arg xml.etree.ElementTree.Element float_element: a ``<float>`` element :rtype: :obj:`float` If *float_element* represents the empty element ``<float />``, ``float()`` is returned. """ if float_element.text is not None: return float(float_element.text) else: return float() def _parse_list(self, list_element): """Return a :obj:`list` evaluator object parsed from *list_element*. :arg xml.etree.ElementTree.Element list_element: a ``<list>`` element :rtype: :class:`aglyph.component.Evaluator` .. note:: The evaluator returned by this method produces a new :obj:`list` object each time it is called. """ process_value_element = self._process_value_element items = [ process_value_element(child_element, "list") for child_element in list_element] return evaluate(list, items) def _parse_tuple(self, tuple_element): """Return a :obj:`tuple` evaluator object parsed from *tuple_element*. :arg xml.etree.ElementTree.Element tuple_element: a ``<tuple>`` element :rtype: :class:`aglyph.component.Evaluator` (or :obj:`tuple` if the ``<tuple>`` element is empty) .. note:: The evaluator returned by this method produces a new :obj:`tuple` object each time it is called. """ children = list(tuple_element) if children: process_value_element = self._process_value_element items = [ process_value_element(child_element, "tuple") for child_element in children] return evaluate(tuple, items) else: # a tuple is immutable, so there's no sense in paying the overhead # of evaluation for an empty tuple return tuple() def _parse_set(self, set_element): """Return a :obj:`set` evaluator object parsed from *set_element*. :arg xml.etree.ElementTree.Element set_element: a ``<set>`` element :rtype: :class:`aglyph.component.Evaluator` .. note:: The evaluator returned by this method produces a new :obj:`set` object each time it is called. """ process_value_element = self._process_value_element items = [ process_value_element(child_element, "set") for child_element in set_element] return evaluate(set, items) def _parse_dict(self, dict_element): """Return a :obj:`dict` evaluator object parsed from *dict_element*. :arg xml.etree.ElementTree.Element dict_element: a ``<dict>`` element :rtype: :class:`aglyph.component.Evaluator` .. note:: The evaluator returned by this method produces a new :obj:`dict` object each time it is called. """ # a list of 2-tuples, (key, value), used to initialize a dictionary items = [] for element in list(dict_element): if element.tag != "item": raise AglyphError("unexpected element: dict/%s" % element.tag) children = list(element) child_tags = [child.tag for child in children] if child_tags == ["key", "value"]: key_element, value_element = children else: raise AglyphError( "expected item/key, item/value; found %s" % ", ".join("item/%s" % ctag for ctag in child_tags)) items.append(( self._unserialize_element_value(key_element), self._unserialize_element_value(value_element) )) return evaluate(dict, items) def _parse_reference(self, reference_element): """Return a reference to another component in this context. :arg xml.etree.ElementTree.Element reference_element: a ``<reference>`` element :rtype: an Aglyph :class:`Reference` The **reference/@id** attribute is required, and will be used as the value to create an :class:`aglyph.component.Reference`. """ component_id = reference_element.attrib["id"] return ref(component_id) def _parse_eval(self, eval_element): """Return a partial object that will evaluate an expression parsed from *eval_element*. :arg xml.etree.ElementTree.Element eval_element:\ an ``<eval>`` element :rtype: :obj:`functools.partial` ..versionadded:: 3.0.0 The partial object will use Python's :func:`ast.literal_eval` function to evaluate the expression when it is called. (Prior versions of Aglyph used the builtin :obj:`eval` function.) .. seealso:: `Eval really is dangerous <http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html>`_ Ned Batchelder's insanely thorough discussion of :obj:`eval` """ if eval_element.text is None: raise AglyphError("<eval> cannot be an empty element") return partial(literal_eval, eval_element.text) def __repr__(self): return self.__repr