context.py 10.6 KB
Newer Older
1 2 3
# coding: utf-8

"""
4
Exposes a ContextStack class and functions to retrieve names from context.
5 6 7

"""

8 9 10 11
# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
_BUILTIN_MODULE = type(0).__module__


12
# We use this private global variable as a return value to represent a key
13 14 15
# not being found on lookup.  This lets us distinguish between the case
# of a key's value being None with the case of a key not being found --
# without having to rely on exceptions (e.g. KeyError) for flow control.
16 17 18 19
#
# TODO: eliminate the need for a private global variable, e.g. by using the
#   preferred Python approach of "easier to ask for forgiveness than permission":
#     http://docs.python.org/glossary.html#term-eafp
20 21
class NotFound(object):
    pass
22
_NOT_FOUND = NotFound()
23 24 25 26 27 28 29


# TODO: share code with template.check_callable().
def _is_callable(obj):
    return hasattr(obj, '__call__')


30
def _get_value(item, key):
31
    """
32
    Retrieve a key's value from an item.
33

34
    Returns _NOT_FOUND if the key does not exist.
35

36
    The ContextStack.get() docstring documents this function's intended behavior.
37 38

    """
39 40
    if isinstance(item, dict):
        # Then we consider the argument a "hash" for the purposes of the spec.
41
        #
42 43 44 45
        # We do a membership test to avoid using exceptions for flow control
        # (e.g. catching KeyError).
        if key in item:
            return item[key]
46
    elif type(item).__module__ != _BUILTIN_MODULE:
47 48
        # Then we consider the argument an "object" for the purposes of
        # the spec.
49
        #
50 51 52 53 54 55 56 57 58
        # The elif test above lets us avoid treating instances of built-in
        # types like integers and strings as objects (cf. issue #81).
        # Instances of user-defined classes on the other hand, for example,
        # are considered objects by the test above.
        if hasattr(item, key):
            attr = getattr(item, key)
            if _is_callable(attr):
                return attr()
            return attr
59 60 61

    return _NOT_FOUND

62

63
class ContextStack(object):
64 65

    """
66
    Provides dictionary-like access to a stack of zero or more items.
67

68
    Instances of this class are meant to act as the rendering context
69 70
    when rendering Mustache templates in accordance with mustache(5)
    and the Mustache spec.
71

72 73 74 75
    Instances encapsulate a private stack of hashes, objects, and built-in
    type instances.  Querying the stack for the value of a key queries
    the items in the stack in order from last-added objects to first
    (last in, first out).
76

77
    Caution: this class does not currently support recursive nesting in
78
    that items in the stack cannot themselves be ContextStack instances.
79

80
    See the docstrings of the methods of this class for more details.
81 82 83 84 85

    """

    # We reserve keyword arguments for future options (e.g. a "strict=True"
    # option for enabling a strict mode).
86
    def __init__(self, *items):
87
        """
88
        Construct an instance, and initialize the private stack.
89

90 91 92
        The *items arguments are the items with which to populate the
        initial stack.  Items in the argument list are added to the
        stack in order so that, in particular, items at the end of
93 94
        the argument list are queried first when querying the stack.

95
        Caution: items should not themselves be ContextStack instances, as
96
        recursive nesting does not behave as one might expect.
97

98
        """
99
        self._stack = list(items)
100

101 102 103 104 105 106
    def __repr__(self):
        """
        Return a string representation of the instance.

        For example--

107
        >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
108
        >>> repr(context)
109
        "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
110 111 112 113

        """
        return "%s%s" % (self.__class__.__name__, tuple(self._stack))

114 115 116
    @staticmethod
    def create(*context, **kwargs):
        """
117
        Build a ContextStack instance from a sequence of context-like items.
118

119
        This factory-style method is more general than the ContextStack class's
120
        constructor in that, unlike the constructor, the argument list
121
        can itself contain ContextStack instances.
122 123 124 125

        Here is an example illustrating various aspects of this method:

        >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
126
        >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
127
        >>>
128
        >>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
129 130 131 132 133 134 135 136 137 138
        >>>
        >>> context.get('animal')
        'cat'
        >>> context.get('vegetable')
        'spinach'
        >>> context.get('mineral')
        'gold'

        Arguments:

139
          *context: zero or more dictionaries, ContextStack instances, or objects
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
            with which to populate the initial context stack.  None
            arguments will be skipped.  Items in the *context list are
            added to the stack in order so that later items in the argument
            list take precedence over earlier items.  This behavior is the
            same as the constructor's.

          **kwargs: additional key-value data to add to the context stack.
            As these arguments appear after all items in the *context list,
            in the case of key conflicts these values take precedence over
            all items in the *context list.  This behavior is the same as
            the constructor's.

        """
        items = context

155
        context = ContextStack()
156 157 158 159

        for item in items:
            if item is None:
                continue
160
            if isinstance(item, ContextStack):
161 162 163 164 165 166 167 168 169
                context._stack.extend(item._stack)
            else:
                context.push(item)

        if kwargs:
            context.push(kwargs)

        return context

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
    # TODO: add some unit tests for this.
    def resolve(self, name):
        """
        Resolve a name against a context stack.

        This function follows the rules outlined in the section of the spec
        regarding tag interpolation.

        Arguments:

          context_stack: a ContextStack instance.

        This function does not coerce the return value to a string.

        """
        if name == '.':
            return self.top()

        parts = name.split('.')

        value = self.get(parts[0], _NOT_FOUND)

192 193 194 195 196 197 198
        # The full context stack is not used to resolve the remaining parts.
        # From the spec--
        #
        #   If any name parts were retained in step 1, each should be resolved
        #   against a context stack containing only the result from the former
        #   resolution.
        #
199
        for part in parts[1:]:
200
            # TODO: consider using EAFP here instead.
201 202 203 204 205 206 207 208 209 210 211 212
            #   http://docs.python.org/glossary.html#term-eafp
            if value is _NOT_FOUND:
                break
            value = _get_value(value, part)

        # The spec says that if name resolution fails at any point, the result
        # should be considered falsey, and should interpolate as the empty string.
        if value is _NOT_FOUND:
            return ''

        return value

213
    # TODO: rename this method _get_part().
214 215 216 217
    def get(self, key, default=None):
        """
        Query the stack for the given key, and return the resulting value.

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
        This method queries items in the stack in order from last-added
        objects to first (last in, first out).  The value returned is
        the value of the key in the first item that contains the key.
        If the key is not found in any item in the stack, then the default
        value is returned.  The default value defaults to None.

        When speaking about returning values from a context, the Mustache
        spec distinguishes between two types of context stack elements:
        hashes and objects.

        In accordance with the spec, this method queries items in the
        stack for a key in the following way.  For the purposes of querying,
        each item is classified into one of the following three mutually
        exclusive categories: a hash, an object, or neither:

        (1) Hash: if the item's type is a subclass of dict, then the item
            is considered a hash (in the terminology of the spec), and
            the key's value is the dictionary value of the key.  If the
            dictionary doesn't contain the key, the key is not found.

        (2) Object: if the item isn't a hash and isn't an instance of a
            built-in type, then the item is considered an object (again
            using the language of the spec).  In this case, the method
            looks for an attribute with the same name as the key.  If an
            attribute with that name exists, the value of the attribute is
            returned.  If the attribute is callable, however (i.e. if the
            attribute is a method), then the attribute is called with no
            arguments and instead that value returned.  If there is no
            attribute with the same name as the key, then the key is
            considered not found.

        (3) Neither: if the item is neither a hash nor an object, then
            the key is considered not found.
251

252 253
        *Caution*:

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
          Callables are handled differently depending on whether they are
          dictionary values, as in (1) above, or attributes, as in (2).
          The former are returned as-is, while the latter are first
          called and that value returned.

          Here is an example to illustrate:

          >>> def greet():
          ...     return "Hi Bob!"
          >>>
          >>> class Greeter(object):
          ...     greet = None
          >>>
          >>> dct = {'greet': greet}
          >>> obj = Greeter()
          >>> obj.greet = greet
          >>>
          >>> dct['greet'] is obj.greet
          True
273
          >>> ContextStack(dct).get('greet')  #doctest: +ELLIPSIS
274
          <function greet at 0x...>
275
          >>> ContextStack(obj).get('greet')
276 277 278
          'Hi Bob!'

          TODO: explain the rationale for this difference in treatment.
279

280
        """
281
        for obj in reversed(self._stack):
282
            val = _get_value(obj, key)
283 284 285 286 287
            if val is _NOT_FOUND:
                continue
            # Otherwise, the key was found.
            return val
        # Otherwise, no item in the stack contained the key.
288 289 290

        return default

291
    def push(self, item):
292 293 294 295
        """
        Push an item onto the stack.

        """
296 297
        self._stack.append(item)

298
    def pop(self):
299 300 301 302
        """
        Pop an item off of the stack, and return it.

        """
303
        return self._stack.pop()
304

305 306 307 308 309 310 311 312 313 314 315 316
    def top(self):
        """
        Return the item last added to the stack.

        """
        return self._stack[-1]

    def copy(self):
        """
        Return a copy of this instance.

        """
317
        return ContextStack(*self._stack)