344 lines
12 KiB
Python
344 lines
12 KiB
Python
# coding: utf-8
|
|
|
|
"""
|
|
Exposes a ContextStack class.
|
|
|
|
The Mustache spec makes a special distinction between two types of context
|
|
stack elements: hashes and objects. For the purposes of interpreting the
|
|
spec, we define these categories mutually exclusively as follows:
|
|
|
|
(1) Hash: an item whose type is a subclass of dict.
|
|
|
|
(2) Object: an item that is neither a hash nor an instance of a
|
|
built-in type.
|
|
|
|
"""
|
|
|
|
from pystache.common import PystacheError
|
|
import collections
|
|
|
|
|
|
# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
|
|
_BUILTIN_MODULE = type(0).__module__
|
|
|
|
|
|
# We use this private global variable as a return value to represent a key
|
|
# 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.
|
|
#
|
|
# 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
|
|
class NotFound(object):
|
|
pass
|
|
_NOT_FOUND = NotFound()
|
|
|
|
|
|
def _get_value(context, key):
|
|
"""
|
|
Retrieve a key's value from a context item.
|
|
|
|
Returns _NOT_FOUND if the key does not exist.
|
|
|
|
The ContextStack.get() docstring documents this function's intended behavior.
|
|
|
|
"""
|
|
if isinstance(context, dict):
|
|
# Then we consider the argument a "hash" for the purposes of the spec.
|
|
#
|
|
# We do a membership test to avoid using exceptions for flow control
|
|
# (e.g. catching KeyError).
|
|
if key in context:
|
|
return context[key]
|
|
elif type(context).__module__ != _BUILTIN_MODULE:
|
|
# Then we consider the argument an "object" for the purposes of
|
|
# the spec.
|
|
#
|
|
# 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.
|
|
try:
|
|
attr = getattr(context, key)
|
|
except AttributeError:
|
|
# TODO: distinguish the case of the attribute not existing from
|
|
# an AttributeError being raised by the call to the attribute.
|
|
# See the following issue for implementation ideas:
|
|
# http://bugs.python.org/issue7559
|
|
pass
|
|
else:
|
|
# TODO: consider using EAFP here instead.
|
|
# http://docs.python.org/glossary.html#term-eafp
|
|
if isinstance(attr, collections.Callable):
|
|
return attr()
|
|
return attr
|
|
|
|
return _NOT_FOUND
|
|
|
|
|
|
class KeyNotFoundError(PystacheError):
|
|
|
|
"""
|
|
An exception raised when a key is not found in a context stack.
|
|
|
|
"""
|
|
|
|
def __init__(self, key, details):
|
|
self.key = key
|
|
self.details = details
|
|
|
|
def __str__(self):
|
|
return "Key %s not found: %s" % (repr(self.key), self.details)
|
|
|
|
|
|
class ContextStack(object):
|
|
|
|
"""
|
|
Provides dictionary-like access to a stack of zero or more items.
|
|
|
|
Instances of this class are meant to act as the rendering context
|
|
when rendering Mustache templates in accordance with mustache(5)
|
|
and the Mustache spec.
|
|
|
|
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).
|
|
|
|
Caution: this class does not currently support recursive nesting in
|
|
that items in the stack cannot themselves be ContextStack instances.
|
|
|
|
See the docstrings of the methods of this class for more details.
|
|
|
|
"""
|
|
|
|
# We reserve keyword arguments for future options (e.g. a "strict=True"
|
|
# option for enabling a strict mode).
|
|
def __init__(self, *items):
|
|
"""
|
|
Construct an instance, and initialize the private stack.
|
|
|
|
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
|
|
the argument list are queried first when querying the stack.
|
|
|
|
Caution: items should not themselves be ContextStack instances, as
|
|
recursive nesting does not behave as one might expect.
|
|
|
|
"""
|
|
self._stack = list(items)
|
|
|
|
def __repr__(self):
|
|
"""
|
|
Return a string representation of the instance.
|
|
|
|
For example--
|
|
|
|
>>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
|
|
>>> repr(context)
|
|
"ContextStack({'alpha': 'abc'}, {'numeric': 123})"
|
|
|
|
"""
|
|
return "%s%s" % (self.__class__.__name__, tuple(self._stack))
|
|
|
|
@staticmethod
|
|
def create(*context, **kwargs):
|
|
"""
|
|
Build a ContextStack instance from a sequence of context-like items.
|
|
|
|
This factory-style method is more general than the ContextStack class's
|
|
constructor in that, unlike the constructor, the argument list
|
|
can itself contain ContextStack instances.
|
|
|
|
Here is an example illustrating various aspects of this method:
|
|
|
|
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
|
|
>>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
|
|
>>>
|
|
>>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
|
|
>>>
|
|
>>> context.get('animal')
|
|
'cat'
|
|
>>> context.get('vegetable')
|
|
'spinach'
|
|
>>> context.get('mineral')
|
|
'gold'
|
|
|
|
Arguments:
|
|
|
|
*context: zero or more dictionaries, ContextStack instances, or objects
|
|
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
|
|
|
|
context = ContextStack()
|
|
|
|
for item in items:
|
|
if item is None:
|
|
continue
|
|
if isinstance(item, ContextStack):
|
|
context._stack.extend(item._stack)
|
|
else:
|
|
context.push(item)
|
|
|
|
if kwargs:
|
|
context.push(kwargs)
|
|
|
|
return context
|
|
|
|
# TODO: add more unit tests for this.
|
|
# TODO: update the docstring for dotted names.
|
|
def get(self, name):
|
|
"""
|
|
Resolve a dotted name against the current context stack.
|
|
|
|
This function follows the rules outlined in the section of the
|
|
spec regarding tag interpolation. This function returns the value
|
|
as is and does not coerce the return value to a string.
|
|
|
|
Arguments:
|
|
|
|
name: a dotted or non-dotted name.
|
|
|
|
default: the value to return if name resolution fails at any point.
|
|
Defaults to the empty string per the Mustache spec.
|
|
|
|
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.
|
|
|
|
In accordance with the spec, this method queries items in the
|
|
stack for a key differently depending on whether the item is a
|
|
hash, object, or neither (as defined in the module docstring):
|
|
|
|
(1) Hash: if the item is a hash, then the key's value is the
|
|
dictionary value of the key. If the dictionary doesn't contain
|
|
the key, then the key is considered not found.
|
|
|
|
(2) Object: if the item is an an object, then 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 that value is 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.
|
|
|
|
*Caution*:
|
|
|
|
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
|
|
>>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
|
|
<function greet at 0x...>
|
|
>>> ContextStack(obj).get('greet')
|
|
'Hi Bob!'
|
|
|
|
TODO: explain the rationale for this difference in treatment.
|
|
|
|
"""
|
|
if name == '.':
|
|
try:
|
|
return self.top()
|
|
except IndexError:
|
|
raise KeyNotFoundError(".", "empty context stack")
|
|
|
|
parts = name.split('.')
|
|
|
|
try:
|
|
result = self._get_simple(parts[0])
|
|
except KeyNotFoundError:
|
|
raise KeyNotFoundError(name, "first part")
|
|
|
|
for part in parts[1:]:
|
|
# The full context stack is not used to resolve the remaining parts.
|
|
# From the spec--
|
|
#
|
|
# 5) 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. If any part fails resolution, the
|
|
# result should be considered falsey, and should interpolate as
|
|
# the empty string.
|
|
#
|
|
# TODO: make sure we have a test case for the above point.
|
|
result = _get_value(result, part)
|
|
# TODO: consider using EAFP here instead.
|
|
# http://docs.python.org/glossary.html#term-eafp
|
|
if result is _NOT_FOUND:
|
|
raise KeyNotFoundError(name, "missing %s" % repr(part))
|
|
|
|
return result
|
|
|
|
def _get_simple(self, name):
|
|
"""
|
|
Query the stack for a non-dotted name.
|
|
|
|
"""
|
|
for item in reversed(self._stack):
|
|
result = _get_value(item, name)
|
|
if result is not _NOT_FOUND:
|
|
return result
|
|
|
|
raise KeyNotFoundError(name, "part missing")
|
|
|
|
def push(self, item):
|
|
"""
|
|
Push an item onto the stack.
|
|
|
|
"""
|
|
self._stack.append(item)
|
|
|
|
def pop(self):
|
|
"""
|
|
Pop an item off of the stack, and return it.
|
|
|
|
"""
|
|
return self._stack.pop()
|
|
|
|
def top(self):
|
|
"""
|
|
Return the item last added to the stack.
|
|
|
|
"""
|
|
return self._stack[-1]
|
|
|
|
def copy(self):
|
|
"""
|
|
Return a copy of this instance.
|
|
|
|
"""
|
|
return ContextStack(*self._stack)
|