python/pystache/pystache/renderengine.py
author James Lal <james@lightsofapollo.com>
Wed, 26 Nov 2014 10:11:28 -0800
changeset 225714 5edda558c6555e518d3579f89854b556c453c66a
permissions -rw-r--r--
Bug 1068653 - Part 1 Add python dependencies for taskcluster mach commands r=gps

# coding: utf-8

"""
Defines a class responsible for rendering logic.

"""

import re

from pystache.common import is_string
from pystache.parser import parse


def context_get(stack, name):
    """
    Find and return a name from a ContextStack instance.

    """
    return stack.get(name)


class RenderEngine(object):

    """
    Provides a render() method.

    This class is meant only for internal use.

    As a rule, the code in this class operates on unicode strings where
    possible rather than, say, strings of type str or markupsafe.Markup.
    This means that strings obtained from "external" sources like partials
    and variable tag values are immediately converted to unicode (or
    escaped and converted to unicode) before being operated on further.
    This makes maintaining, reasoning about, and testing the correctness
    of the code much simpler.  In particular, it keeps the implementation
    of this class independent of the API details of one (or possibly more)
    unicode subclasses (e.g. markupsafe.Markup).

    """

    # TODO: it would probably be better for the constructor to accept
    #   and set as an attribute a single RenderResolver instance
    #   that encapsulates the customizable aspects of converting
    #   strings and resolving partials and names from context.
    def __init__(self, literal=None, escape=None, resolve_context=None,
                 resolve_partial=None, to_str=None):
        """
        Arguments:

          literal: the function used to convert unescaped variable tag
            values to unicode, e.g. the value corresponding to a tag
            "{{{name}}}".  The function should accept a string of type
            str or unicode (or a subclass) and return a string of type
            unicode (but not a proper subclass of unicode).
                This class will only pass basestring instances to this
            function.  For example, it will call str() on integer variable
            values prior to passing them to this function.

          escape: the function used to escape and convert variable tag
            values to unicode, e.g. the value corresponding to a tag
            "{{name}}".  The function should obey the same properties
            described above for the "literal" function argument.
                This function should take care to convert any str
            arguments to unicode just as the literal function should, as
            this class will not pass tag values to literal prior to passing
            them to this function.  This allows for more flexibility,
            for example using a custom escape function that handles
            incoming strings of type markupsafe.Markup differently
            from plain unicode strings.

          resolve_context: the function to call to resolve a name against
            a context stack.  The function should accept two positional
            arguments: a ContextStack instance and a name to resolve.

          resolve_partial: the function to call when loading a partial.
            The function should accept a template name string and return a
            template string of type unicode (not a subclass).

          to_str: a function that accepts an object and returns a string (e.g.
            the built-in function str).  This function is used for string
            coercion whenever a string is required (e.g. for converting None
            or 0 to a string).

        """
        self.escape = escape
        self.literal = literal
        self.resolve_context = resolve_context
        self.resolve_partial = resolve_partial
        self.to_str = to_str

    # TODO: Rename context to stack throughout this module.

    # From the spec:
    #
    #   When used as the data value for an Interpolation tag, the lambda
    #   MUST be treatable as an arity 0 function, and invoked as such.
    #   The returned value MUST be rendered against the default delimiters,
    #   then interpolated in place of the lambda.
    #
    def fetch_string(self, context, name):
        """
        Get a value from the given context as a basestring instance.

        """
        val = self.resolve_context(context, name)

        if callable(val):
            # Return because _render_value() is already a string.
            return self._render_value(val(), context)

        if not is_string(val):
            return self.to_str(val)

        return val

    def fetch_section_data(self, context, name):
        """
        Fetch the value of a section as a list.

        """
        data = self.resolve_context(context, name)

        # From the spec:
        #
        #   If the data is not of a list type, it is coerced into a list
        #   as follows: if the data is truthy (e.g. `!!data == true`),
        #   use a single-element list containing the data, otherwise use
        #   an empty list.
        #
        if not data:
            data = []
        else:
            # The least brittle way to determine whether something
            # supports iteration is by trying to call iter() on it:
            #
            #   http://docs.python.org/library/functions.html#iter
            #
            # It is not sufficient, for example, to check whether the item
            # implements __iter__ () (the iteration protocol).  There is
            # also __getitem__() (the sequence protocol).  In Python 2,
            # strings do not implement __iter__(), but in Python 3 they do.
            try:
                iter(data)
            except TypeError:
                # Then the value does not support iteration.
                data = [data]
            else:
                if is_string(data) or isinstance(data, dict):
                    # Do not treat strings and dicts (which are iterable) as lists.
                    data = [data]
                # Otherwise, treat the value as a list.

        return data

    def _render_value(self, val, context, delimiters=None):
        """
        Render an arbitrary value.

        """
        if not is_string(val):
            # In case the template is an integer, for example.
            val = self.to_str(val)
        if type(val) is not unicode:
            val = self.literal(val)
        return self.render(val, context, delimiters)

    def render(self, template, context_stack, delimiters=None):
        """
        Render a unicode template string, and return as unicode.

        Arguments:

          template: a template string of type unicode (but not a proper
            subclass of unicode).

          context_stack: a ContextStack instance.

        """
        parsed_template = parse(template, delimiters)

        return parsed_template.render(self, context_stack)