diff options
Diffstat (limited to 'pystache/context.py')
-rw-r--r-- | pystache/context.py | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/pystache/context.py b/pystache/context.py new file mode 100644 index 0000000..1621d61 --- /dev/null +++ b/pystache/context.py @@ -0,0 +1,264 @@ +# coding: utf-8 + +""" +Defines a Context class to represent mustache(5)'s notion of context. + +""" + +class NotFound(object): pass +# 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. +_NOT_FOUND = NotFound() + + +# TODO: share code with template.check_callable(). +def _is_callable(obj): + return hasattr(obj, '__call__') + + +def _get_value(item, key): + """ + Retrieve a key's value from an item. + + Returns _NOT_FOUND if the key does not exist. + + The Context.get() docstring documents this function's intended behavior. + + """ + if isinstance(item, 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 item: + return item[key] + elif type(item).__module__ != '__builtin__': + # 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. + if hasattr(item, key): + attr = getattr(item, key) + if _is_callable(attr): + return attr() + return attr + + return _NOT_FOUND + + +class Context(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 Context 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 Context 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 = Context({'alpha': 'abc'}, {'numeric': 123}) + >>> repr(context) + "Context({'alpha': 'abc'}, {'numeric': 123})" + + """ + return "%s%s" % (self.__class__.__name__, tuple(self._stack)) + + @staticmethod + def create(*context, **kwargs): + """ + Build a Context instance from a sequence of context-like items. + + This factory-style method is more general than the Context class's + constructor in that, unlike the constructor, the argument list + can itself contain Context instances. + + Here is an example illustrating various aspects of this method: + + >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'} + >>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'}) + >>> + >>> context = Context.create(obj1, None, obj2, mineral='gold') + >>> + >>> context.get('animal') + 'cat' + >>> context.get('vegetable') + 'spinach' + >>> context.get('mineral') + 'gold' + + Arguments: + + *context: zero or more dictionaries, Context 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 = Context() + + for item in items: + if item is None: + continue + if isinstance(item, Context): + context._stack.extend(item._stack) + else: + context.push(item) + + if kwargs: + context.push(kwargs) + + return context + + def get(self, key, default=None): + """ + Query the stack for the given key, and return the resulting value. + + 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. + + *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 + >>> Context(dct).get('greet') #doctest: +ELLIPSIS + <function greet at 0x...> + >>> Context(obj).get('greet') + 'Hi Bob!' + + TODO: explain the rationale for this difference in treatment. + + """ + for obj in reversed(self._stack): + val = _get_value(obj, key) + if val is _NOT_FOUND: + continue + # Otherwise, the key was found. + return val + # Otherwise, no item in the stack contained the key. + + return default + + 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 Context(*self._stack) |