diff options
Diffstat (limited to 'python/pystache/pystache')
-rw-r--r-- | python/pystache/pystache/__init__.py | 13 | ||||
-rw-r--r-- | python/pystache/pystache/commands/__init__.py | 4 | ||||
-rw-r--r-- | python/pystache/pystache/commands/render.py | 95 | ||||
-rw-r--r-- | python/pystache/pystache/commands/test.py | 18 | ||||
-rw-r--r-- | python/pystache/pystache/common.py | 71 | ||||
-rw-r--r-- | python/pystache/pystache/context.py | 342 | ||||
-rw-r--r-- | python/pystache/pystache/defaults.py | 65 | ||||
-rw-r--r-- | python/pystache/pystache/init.py | 19 | ||||
-rw-r--r-- | python/pystache/pystache/loader.py | 170 | ||||
-rw-r--r-- | python/pystache/pystache/locator.py | 171 | ||||
-rw-r--r-- | python/pystache/pystache/parsed.py | 50 | ||||
-rw-r--r-- | python/pystache/pystache/parser.py | 378 | ||||
-rw-r--r-- | python/pystache/pystache/renderengine.py | 181 | ||||
-rw-r--r-- | python/pystache/pystache/renderer.py | 460 | ||||
-rw-r--r-- | python/pystache/pystache/specloader.py | 90 | ||||
-rw-r--r-- | python/pystache/pystache/template_spec.py | 53 |
16 files changed, 2180 insertions, 0 deletions
diff --git a/python/pystache/pystache/__init__.py b/python/pystache/pystache/__init__.py new file mode 100644 index 000000000..4cf24344e --- /dev/null +++ b/python/pystache/pystache/__init__.py @@ -0,0 +1,13 @@ + +""" +TODO: add a docstring. + +""" + +# We keep all initialization code in a separate module. + +from pystache.init import parse, render, Renderer, TemplateSpec + +__all__ = ['parse', 'render', 'Renderer', 'TemplateSpec'] + +__version__ = '0.5.4' # Also change in setup.py. diff --git a/python/pystache/pystache/commands/__init__.py b/python/pystache/pystache/commands/__init__.py new file mode 100644 index 000000000..a0d386a38 --- /dev/null +++ b/python/pystache/pystache/commands/__init__.py @@ -0,0 +1,4 @@ +""" +TODO: add a docstring. + +""" diff --git a/python/pystache/pystache/commands/render.py b/python/pystache/pystache/commands/render.py new file mode 100644 index 000000000..1a9c309d5 --- /dev/null +++ b/python/pystache/pystache/commands/render.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" +This module provides command-line access to pystache. + +Run this script using the -h option for command-line help. + +""" + + +try: + import json +except: + # The json module is new in Python 2.6, whereas simplejson is + # compatible with earlier versions. + try: + import simplejson as json + except ImportError: + # Raise an error with a type different from ImportError as a hack around + # this issue: + # http://bugs.python.org/issue7559 + from sys import exc_info + ex_type, ex_value, tb = exc_info() + new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value)) + raise new_ex.__class__, new_ex, tb + +# The optparse module is deprecated in Python 2.7 in favor of argparse. +# However, argparse is not available in Python 2.6 and earlier. +from optparse import OptionParser +import sys + +# We use absolute imports here to allow use of this script from its +# location in source control (e.g. for development purposes). +# Otherwise, the following error occurs: +# +# ValueError: Attempted relative import in non-package +# +from pystache.common import TemplateNotFoundError +from pystache.renderer import Renderer + + +USAGE = """\ +%prog [-h] template context + +Render a mustache template with the given context. + +positional arguments: + template A filename or template string. + context A filename or JSON string.""" + + +def parse_args(sys_argv, usage): + """ + Return an OptionParser for the script. + + """ + args = sys_argv[1:] + + parser = OptionParser(usage=usage) + options, args = parser.parse_args(args) + + template, context = args + + return template, context + + +# TODO: verify whether the setup() method's entry_points argument +# supports passing arguments to main: +# +# http://packages.python.org/distribute/setuptools.html#automatic-script-creation +# +def main(sys_argv=sys.argv): + template, context = parse_args(sys_argv, USAGE) + + if template.endswith('.mustache'): + template = template[:-9] + + renderer = Renderer() + + try: + template = renderer.load_template(template) + except TemplateNotFoundError: + pass + + try: + context = json.load(open(context)) + except IOError: + context = json.loads(context) + + rendered = renderer.render(template, context) + print rendered + + +if __name__=='__main__': + main() diff --git a/python/pystache/pystache/commands/test.py b/python/pystache/pystache/commands/test.py new file mode 100644 index 000000000..087245338 --- /dev/null +++ b/python/pystache/pystache/commands/test.py @@ -0,0 +1,18 @@ +# coding: utf-8 + +""" +This module provides a command to test pystache (unit tests, doctests, etc). + +""" + +import sys + +from pystache.tests.main import main as run_tests + + +def main(sys_argv=sys.argv): + run_tests(sys_argv=sys_argv) + + +if __name__=='__main__': + main() diff --git a/python/pystache/pystache/common.py b/python/pystache/pystache/common.py new file mode 100644 index 000000000..fb266dd8b --- /dev/null +++ b/python/pystache/pystache/common.py @@ -0,0 +1,71 @@ +# coding: utf-8 + +""" +Exposes functionality needed throughout the project. + +""" + +from sys import version_info + +def _get_string_types(): + # TODO: come up with a better solution for this. One of the issues here + # is that in Python 3 there is no common base class for unicode strings + # and byte strings, and 2to3 seems to convert all of "str", "unicode", + # and "basestring" to Python 3's "str". + if version_info < (3, ): + return basestring + # The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3. + return (unicode, type(u"a".encode('utf-8'))) + + +_STRING_TYPES = _get_string_types() + + +def is_string(obj): + """ + Return whether the given object is a byte string or unicode string. + + This function is provided for compatibility with both Python 2 and 3 + when using 2to3. + + """ + return isinstance(obj, _STRING_TYPES) + + +# This function was designed to be portable across Python versions -- both +# with older versions and with Python 3 after applying 2to3. +def read(path): + """ + Return the contents of a text file as a byte string. + + """ + # Opening in binary mode is necessary for compatibility across Python + # 2 and 3. In both Python 2 and 3, open() defaults to opening files in + # text mode. However, in Python 2, open() returns file objects whose + # read() method returns byte strings (strings of type `str` in Python 2), + # whereas in Python 3, the file object returns unicode strings (strings + # of type `str` in Python 3). + f = open(path, 'rb') + # We avoid use of the with keyword for Python 2.4 support. + try: + return f.read() + finally: + f.close() + + +class MissingTags(object): + + """Contains the valid values for Renderer.missing_tags.""" + + ignore = 'ignore' + strict = 'strict' + + +class PystacheError(Exception): + """Base class for Pystache exceptions.""" + pass + + +class TemplateNotFoundError(PystacheError): + """An exception raised when a template is not found.""" + pass diff --git a/python/pystache/pystache/context.py b/python/pystache/pystache/context.py new file mode 100644 index 000000000..671591609 --- /dev/null +++ b/python/pystache/pystache/context.py @@ -0,0 +1,342 @@ +# 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 + + +# 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 callable(attr): + 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) diff --git a/python/pystache/pystache/defaults.py b/python/pystache/pystache/defaults.py new file mode 100644 index 000000000..bcfdf4cd3 --- /dev/null +++ b/python/pystache/pystache/defaults.py @@ -0,0 +1,65 @@ +# coding: utf-8 + +""" +This module provides a central location for defining default behavior. + +Throughout the package, these defaults take effect only when the user +does not otherwise specify a value. + +""" + +try: + # Python 3.2 adds html.escape() and deprecates cgi.escape(). + from html import escape +except ImportError: + from cgi import escape + +import os +import sys + +from pystache.common import MissingTags + + +# How to handle encoding errors when decoding strings from str to unicode. +# +# This value is passed as the "errors" argument to Python's built-in +# unicode() function: +# +# http://docs.python.org/library/functions.html#unicode +# +DECODE_ERRORS = 'strict' + +# The name of the encoding to use when converting to unicode any strings of +# type str encountered during the rendering process. +STRING_ENCODING = sys.getdefaultencoding() + +# The name of the encoding to use when converting file contents to unicode. +# This default takes precedence over the STRING_ENCODING default for +# strings that arise from files. +FILE_ENCODING = sys.getdefaultencoding() + +# The delimiters to start with when parsing. +DELIMITERS = (u'{{', u'}}') + +# How to handle missing tags when rendering a template. +MISSING_TAGS = MissingTags.ignore + +# The starting list of directories in which to search for templates when +# loading a template by file name. +SEARCH_DIRS = [os.curdir] # i.e. ['.'] + +# The escape function to apply to strings that require escaping when +# rendering templates (e.g. for tags enclosed in double braces). +# Only unicode strings will be passed to this function. +# +# The quote=True argument causes double but not single quotes to be escaped +# in Python 3.1 and earlier, and both double and single quotes to be +# escaped in Python 3.2 and later: +# +# http://docs.python.org/library/cgi.html#cgi.escape +# http://docs.python.org/dev/library/html.html#html.escape +# +TAG_ESCAPE = lambda u: escape(u, quote=True) + +# The default template extension, without the leading dot. +TEMPLATE_EXTENSION = 'mustache' diff --git a/python/pystache/pystache/init.py b/python/pystache/pystache/init.py new file mode 100644 index 000000000..38bb1f5a0 --- /dev/null +++ b/python/pystache/pystache/init.py @@ -0,0 +1,19 @@ +# encoding: utf-8 + +""" +This module contains the initialization logic called by __init__.py. + +""" + +from pystache.parser import parse +from pystache.renderer import Renderer +from pystache.template_spec import TemplateSpec + + +def render(template, context=None, **kwargs): + """ + Return the given template string rendered using the given context. + + """ + renderer = Renderer() + return renderer.render(template, context, **kwargs) diff --git a/python/pystache/pystache/loader.py b/python/pystache/pystache/loader.py new file mode 100644 index 000000000..d4a7e5310 --- /dev/null +++ b/python/pystache/pystache/loader.py @@ -0,0 +1,170 @@ +# coding: utf-8 + +""" +This module provides a Loader class for locating and reading templates. + +""" + +import os +import sys + +from pystache import common +from pystache import defaults +from pystache.locator import Locator + + +# We make a function so that the current defaults take effect. +# TODO: revisit whether this is necessary. + +def _make_to_unicode(): + def to_unicode(s, encoding=None): + """ + Raises a TypeError exception if the given string is already unicode. + + """ + if encoding is None: + encoding = defaults.STRING_ENCODING + return unicode(s, encoding, defaults.DECODE_ERRORS) + return to_unicode + + +class Loader(object): + + """ + Loads the template associated to a name or user-defined object. + + All load_*() methods return the template as a unicode string. + + """ + + def __init__(self, file_encoding=None, extension=None, to_unicode=None, + search_dirs=None): + """ + Construct a template loader instance. + + Arguments: + + extension: the template file extension, without the leading dot. + Pass False for no extension (e.g. to use extensionless template + files). Defaults to the package default. + + file_encoding: the name of the encoding to use when converting file + contents to unicode. Defaults to the package default. + + search_dirs: the list of directories in which to search when loading + a template by name or file name. Defaults to the package default. + + to_unicode: the function to use when converting strings of type + str to unicode. The function should have the signature: + + to_unicode(s, encoding=None) + + It should accept a string of type str and an optional encoding + name and return a string of type unicode. Defaults to calling + Python's built-in function unicode() using the package string + encoding and decode errors defaults. + + """ + if extension is None: + extension = defaults.TEMPLATE_EXTENSION + + if file_encoding is None: + file_encoding = defaults.FILE_ENCODING + + if search_dirs is None: + search_dirs = defaults.SEARCH_DIRS + + if to_unicode is None: + to_unicode = _make_to_unicode() + + self.extension = extension + self.file_encoding = file_encoding + # TODO: unit test setting this attribute. + self.search_dirs = search_dirs + self.to_unicode = to_unicode + + def _make_locator(self): + return Locator(extension=self.extension) + + def unicode(self, s, encoding=None): + """ + Convert a string to unicode using the given encoding, and return it. + + This function uses the underlying to_unicode attribute. + + Arguments: + + s: a basestring instance to convert to unicode. Unlike Python's + built-in unicode() function, it is okay to pass unicode strings + to this function. (Passing a unicode string to Python's unicode() + with the encoding argument throws the error, "TypeError: decoding + Unicode is not supported.") + + encoding: the encoding to pass to the to_unicode attribute. + Defaults to None. + + """ + if isinstance(s, unicode): + return unicode(s) + + return self.to_unicode(s, encoding) + + def read(self, path, encoding=None): + """ + Read the template at the given path, and return it as a unicode string. + + """ + b = common.read(path) + + if encoding is None: + encoding = self.file_encoding + + return self.unicode(b, encoding) + + def load_file(self, file_name): + """ + Find and return the template with the given file name. + + Arguments: + + file_name: the file name of the template. + + """ + locator = self._make_locator() + + path = locator.find_file(file_name, self.search_dirs) + + return self.read(path) + + def load_name(self, name): + """ + Find and return the template with the given template name. + + Arguments: + + name: the name of the template. + + """ + locator = self._make_locator() + + path = locator.find_name(name, self.search_dirs) + + return self.read(path) + + # TODO: unit-test this method. + def load_object(self, obj): + """ + Find and return the template associated to the given object. + + Arguments: + + obj: an instance of a user-defined class. + + search_dirs: the list of directories in which to search. + + """ + locator = self._make_locator() + + path = locator.find_object(obj, self.search_dirs) + + return self.read(path) diff --git a/python/pystache/pystache/locator.py b/python/pystache/pystache/locator.py new file mode 100644 index 000000000..30c5b01e0 --- /dev/null +++ b/python/pystache/pystache/locator.py @@ -0,0 +1,171 @@ +# coding: utf-8 + +""" +This module provides a Locator class for finding template files. + +""" + +import os +import re +import sys + +from pystache.common import TemplateNotFoundError +from pystache import defaults + + +class Locator(object): + + def __init__(self, extension=None): + """ + Construct a template locator. + + Arguments: + + extension: the template file extension, without the leading dot. + Pass False for no extension (e.g. to use extensionless template + files). Defaults to the package default. + + """ + if extension is None: + extension = defaults.TEMPLATE_EXTENSION + + self.template_extension = extension + + def get_object_directory(self, obj): + """ + Return the directory containing an object's defining class. + + Returns None if there is no such directory, for example if the + class was defined in an interactive Python session, or in a + doctest that appears in a text file (rather than a Python file). + + """ + if not hasattr(obj, '__module__'): + return None + + module = sys.modules[obj.__module__] + + if not hasattr(module, '__file__'): + # TODO: add a unit test for this case. + return None + + path = module.__file__ + + return os.path.dirname(path) + + def make_template_name(self, obj): + """ + Return the canonical template name for an object instance. + + This method converts Python-style class names (PEP 8's recommended + CamelCase, aka CapWords) to lower_case_with_underscords. Here + is an example with code: + + >>> class HelloWorld(object): + ... pass + >>> hi = HelloWorld() + >>> + >>> locator = Locator() + >>> locator.make_template_name(hi) + 'hello_world' + + """ + template_name = obj.__class__.__name__ + + def repl(match): + return '_' + match.group(0).lower() + + return re.sub('[A-Z]', repl, template_name)[1:] + + def make_file_name(self, template_name, template_extension=None): + """ + Generate and return the file name for the given template name. + + Arguments: + + template_extension: defaults to the instance's extension. + + """ + file_name = template_name + + if template_extension is None: + template_extension = self.template_extension + + if template_extension is not False: + file_name += os.path.extsep + template_extension + + return file_name + + def _find_path(self, search_dirs, file_name): + """ + Search for the given file, and return the path. + + Returns None if the file is not found. + + """ + for dir_path in search_dirs: + file_path = os.path.join(dir_path, file_name) + if os.path.exists(file_path): + return file_path + + return None + + def _find_path_required(self, search_dirs, file_name): + """ + Return the path to a template with the given file name. + + """ + path = self._find_path(search_dirs, file_name) + + if path is None: + raise TemplateNotFoundError('File %s not found in dirs: %s' % + (repr(file_name), repr(search_dirs))) + + return path + + def find_file(self, file_name, search_dirs): + """ + Return the path to a template with the given file name. + + Arguments: + + file_name: the file name of the template. + + search_dirs: the list of directories in which to search. + + """ + return self._find_path_required(search_dirs, file_name) + + def find_name(self, template_name, search_dirs): + """ + Return the path to a template with the given name. + + Arguments: + + template_name: the name of the template. + + search_dirs: the list of directories in which to search. + + """ + file_name = self.make_file_name(template_name) + + return self._find_path_required(search_dirs, file_name) + + def find_object(self, obj, search_dirs, file_name=None): + """ + Return the path to a template associated with the given object. + + """ + if file_name is None: + # TODO: should we define a make_file_name() method? + template_name = self.make_template_name(obj) + file_name = self.make_file_name(template_name) + + dir_path = self.get_object_directory(obj) + + if dir_path is not None: + search_dirs = [dir_path] + search_dirs + + path = self._find_path_required(search_dirs, file_name) + + return path diff --git a/python/pystache/pystache/parsed.py b/python/pystache/pystache/parsed.py new file mode 100644 index 000000000..372d96c66 --- /dev/null +++ b/python/pystache/pystache/parsed.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +""" +Exposes a class that represents a parsed (or compiled) template. + +""" + + +class ParsedTemplate(object): + + """ + Represents a parsed or compiled template. + + An instance wraps a list of unicode strings and node objects. A node + object must have a `render(engine, stack)` method that accepts a + RenderEngine instance and a ContextStack instance and returns a unicode + string. + + """ + + def __init__(self): + self._parse_tree = [] + + def __repr__(self): + return repr(self._parse_tree) + + def add(self, node): + """ + Arguments: + + node: a unicode string or node object instance. See the class + docstring for information. + + """ + self._parse_tree.append(node) + + def render(self, engine, context): + """ + Returns: a string of type unicode. + + """ + # We avoid use of the ternary operator for Python 2.4 support. + def get_unicode(node): + if type(node) is unicode: + return node + return node.render(engine, context) + parts = map(get_unicode, self._parse_tree) + s = ''.join(parts) + + return unicode(s) diff --git a/python/pystache/pystache/parser.py b/python/pystache/pystache/parser.py new file mode 100644 index 000000000..9a4fba235 --- /dev/null +++ b/python/pystache/pystache/parser.py @@ -0,0 +1,378 @@ +# coding: utf-8 + +""" +Exposes a parse() function to parse template strings. + +""" + +import re + +from pystache import defaults +from pystache.parsed import ParsedTemplate + + +END_OF_LINE_CHARACTERS = [u'\r', u'\n'] +NON_BLANK_RE = re.compile(ur'^(.)', re.M) + + +# TODO: add some unit tests for this. +# TODO: add a test case that checks for spurious spaces. +# TODO: add test cases for delimiters. +def parse(template, delimiters=None): + """ + Parse a unicode template string and return a ParsedTemplate instance. + + Arguments: + + template: a unicode template string. + + delimiters: a 2-tuple of delimiters. Defaults to the package default. + + Examples: + + >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}") + >>> print str(parsed).replace('u', '') # This is a hack to get the test to pass both in Python 2 and 3. + ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])] + + """ + if type(template) is not unicode: + raise Exception("Template is not unicode: %s" % type(template)) + parser = _Parser(delimiters) + return parser.parse(template) + + +def _compile_template_re(delimiters): + """ + Return a regular expression object (re.RegexObject) instance. + + """ + # The possible tag type characters following the opening tag, + # excluding "=" and "{". + tag_types = "!>&/#^" + + # TODO: are we following this in the spec? + # + # The tag's content MUST be a non-whitespace character sequence + # NOT containing the current closing delimiter. + # + tag = r""" + (?P<whitespace>[\ \t]*) + %(otag)s \s* + (?: + (?P<change>=) \s* (?P<delims>.+?) \s* = | + (?P<raw>{) \s* (?P<raw_name>.+?) \s* } | + (?P<tag>[%(tag_types)s]?) \s* (?P<tag_key>[\s\S]+?) + ) + \s* %(ctag)s + """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])} + + return re.compile(tag, re.VERBOSE) + + +class ParsingError(Exception): + + pass + + +## Node types + +def _format(obj, exclude=None): + if exclude is None: + exclude = [] + exclude.append('key') + attrs = obj.__dict__ + names = list(set(attrs.keys()) - set(exclude)) + names.sort() + names.insert(0, 'key') + args = ["%s=%s" % (name, repr(attrs[name])) for name in names] + return "%s(%s)" % (obj.__class__.__name__, ", ".join(args)) + + +class _CommentNode(object): + + def __repr__(self): + return _format(self) + + def render(self, engine, context): + return u'' + + +class _ChangeNode(object): + + def __init__(self, delimiters): + self.delimiters = delimiters + + def __repr__(self): + return _format(self) + + def render(self, engine, context): + return u'' + + +class _EscapeNode(object): + + def __init__(self, key): + self.key = key + + def __repr__(self): + return _format(self) + + def render(self, engine, context): + s = engine.fetch_string(context, self.key) + return engine.escape(s) + + +class _LiteralNode(object): + + def __init__(self, key): + self.key = key + + def __repr__(self): + return _format(self) + + def render(self, engine, context): + s = engine.fetch_string(context, self.key) + return engine.literal(s) + + +class _PartialNode(object): + + def __init__(self, key, indent): + self.key = key + self.indent = indent + + def __repr__(self): + return _format(self) + + def render(self, engine, context): + template = engine.resolve_partial(self.key) + # Indent before rendering. + template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template) + + return engine.render(template, context) + + +class _InvertedNode(object): + + def __init__(self, key, parsed_section): + self.key = key + self.parsed_section = parsed_section + + def __repr__(self): + return _format(self) + + def render(self, engine, context): + # TODO: is there a bug because we are not using the same + # logic as in fetch_string()? + data = engine.resolve_context(context, self.key) + # Note that lambdas are considered truthy for inverted sections + # per the spec. + if data: + return u'' + return self.parsed_section.render(engine, context) + + +class _SectionNode(object): + + # TODO: the template_ and parsed_template_ arguments don't both seem + # to be necessary. Can we remove one of them? For example, if + # callable(data) is True, then the initial parsed_template isn't used. + def __init__(self, key, parsed, delimiters, template, index_begin, index_end): + self.delimiters = delimiters + self.key = key + self.parsed = parsed + self.template = template + self.index_begin = index_begin + self.index_end = index_end + + def __repr__(self): + return _format(self, exclude=['delimiters', 'template']) + + def render(self, engine, context): + values = engine.fetch_section_data(context, self.key) + + parts = [] + for val in values: + if callable(val): + # Lambdas special case section rendering and bypass pushing + # the data value onto the context stack. From the spec-- + # + # When used as the data value for a Section tag, the + # lambda MUST be treatable as an arity 1 function, and + # invoked as such (passing a String containing the + # unprocessed section contents). The returned value + # MUST be rendered against the current delimiters, then + # interpolated in place of the section. + # + # Also see-- + # + # https://github.com/defunkt/pystache/issues/113 + # + # TODO: should we check the arity? + val = val(self.template[self.index_begin:self.index_end]) + val = engine._render_value(val, context, delimiters=self.delimiters) + parts.append(val) + continue + + context.push(val) + parts.append(self.parsed.render(engine, context)) + context.pop() + + return unicode(''.join(parts)) + + +class _Parser(object): + + _delimiters = None + _template_re = None + + def __init__(self, delimiters=None): + if delimiters is None: + delimiters = defaults.DELIMITERS + + self._delimiters = delimiters + + def _compile_delimiters(self): + self._template_re = _compile_template_re(self._delimiters) + + def _change_delimiters(self, delimiters): + self._delimiters = delimiters + self._compile_delimiters() + + def parse(self, template): + """ + Parse a template string starting at some index. + + This method uses the current tag delimiter. + + Arguments: + + template: a unicode string that is the template to parse. + + index: the index at which to start parsing. + + Returns: + + a ParsedTemplate instance. + + """ + self._compile_delimiters() + + start_index = 0 + content_end_index, parsed_section, section_key = None, None, None + parsed_template = ParsedTemplate() + + states = [] + + while True: + match = self._template_re.search(template, start_index) + + if match is None: + break + + match_index = match.start() + end_index = match.end() + + matches = match.groupdict() + + # Normalize the matches dictionary. + if matches['change'] is not None: + matches.update(tag='=', tag_key=matches['delims']) + elif matches['raw'] is not None: + matches.update(tag='&', tag_key=matches['raw_name']) + + tag_type = matches['tag'] + tag_key = matches['tag_key'] + leading_whitespace = matches['whitespace'] + + # Standalone (non-interpolation) tags consume the entire line, + # both leading whitespace and trailing newline. + did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS + did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS + is_tag_interpolating = tag_type in ['', '&'] + + if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: + if end_index < len(template): + end_index += template[end_index] == '\r' and 1 or 0 + if end_index < len(template): + end_index += template[end_index] == '\n' and 1 or 0 + elif leading_whitespace: + match_index += len(leading_whitespace) + leading_whitespace = '' + + # Avoid adding spurious empty strings to the parse tree. + if start_index != match_index: + parsed_template.add(template[start_index:match_index]) + + start_index = end_index + + if tag_type in ('#', '^'): + # Cache current state. + state = (tag_type, end_index, section_key, parsed_template) + states.append(state) + + # Initialize new state + section_key, parsed_template = tag_key, ParsedTemplate() + continue + + if tag_type == '/': + if tag_key != section_key: + raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key)) + + # Restore previous state with newly found section data. + parsed_section = parsed_template + + (tag_type, section_start_index, section_key, parsed_template) = states.pop() + node = self._make_section_node(template, tag_type, tag_key, parsed_section, + section_start_index, match_index) + + else: + node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace) + + parsed_template.add(node) + + # Avoid adding spurious empty strings to the parse tree. + if start_index != len(template): + parsed_template.add(template[start_index:]) + + return parsed_template + + def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace): + """ + Create and return a non-section node for the parse tree. + + """ + # TODO: switch to using a dictionary instead of a bunch of ifs and elifs. + if tag_type == '!': + return _CommentNode() + + if tag_type == '=': + delimiters = tag_key.split() + self._change_delimiters(delimiters) + return _ChangeNode(delimiters) + + if tag_type == '': + return _EscapeNode(tag_key) + + if tag_type == '&': + return _LiteralNode(tag_key) + + if tag_type == '>': + return _PartialNode(tag_key, leading_whitespace) + + raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type)) + + def _make_section_node(self, template, tag_type, tag_key, parsed_section, + section_start_index, section_end_index): + """ + Create and return a section node for the parse tree. + + """ + if tag_type == '#': + return _SectionNode(tag_key, parsed_section, self._delimiters, + template, section_start_index, section_end_index) + + if tag_type == '^': + return _InvertedNode(tag_key, parsed_section) + + raise Exception("Invalid symbol for section tag: %s" % repr(tag_type)) diff --git a/python/pystache/pystache/renderengine.py b/python/pystache/pystache/renderengine.py new file mode 100644 index 000000000..c797b1765 --- /dev/null +++ b/python/pystache/pystache/renderengine.py @@ -0,0 +1,181 @@ +# 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) diff --git a/python/pystache/pystache/renderer.py b/python/pystache/pystache/renderer.py new file mode 100644 index 000000000..ff6a90c64 --- /dev/null +++ b/python/pystache/pystache/renderer.py @@ -0,0 +1,460 @@ +# coding: utf-8 + +""" +This module provides a Renderer class to render templates. + +""" + +import sys + +from pystache import defaults +from pystache.common import TemplateNotFoundError, MissingTags, is_string +from pystache.context import ContextStack, KeyNotFoundError +from pystache.loader import Loader +from pystache.parsed import ParsedTemplate +from pystache.renderengine import context_get, RenderEngine +from pystache.specloader import SpecLoader +from pystache.template_spec import TemplateSpec + + +class Renderer(object): + + """ + A class for rendering mustache templates. + + This class supports several rendering options which are described in + the constructor's docstring. Other behavior can be customized by + subclassing this class. + + For example, one can pass a string-string dictionary to the constructor + to bypass loading partials from the file system: + + >>> partials = {'partial': 'Hello, {{thing}}!'} + >>> renderer = Renderer(partials=partials) + >>> # We apply print to make the test work in Python 3 after 2to3. + >>> print renderer.render('{{>partial}}', {'thing': 'world'}) + Hello, world! + + To customize string coercion (e.g. to render False values as ''), one can + subclass this class. For example: + + class MyRenderer(Renderer): + def str_coerce(self, val): + if not val: + return '' + else: + return str(val) + + """ + + def __init__(self, file_encoding=None, string_encoding=None, + decode_errors=None, search_dirs=None, file_extension=None, + escape=None, partials=None, missing_tags=None): + """ + Construct an instance. + + Arguments: + + file_encoding: the name of the encoding to use by default when + reading template files. All templates are converted to unicode + prior to parsing. Defaults to the package default. + + string_encoding: the name of the encoding to use when converting + to unicode any byte strings (type str in Python 2) encountered + during the rendering process. This name will be passed as the + encoding argument to the built-in function unicode(). + Defaults to the package default. + + decode_errors: the string to pass as the errors argument to the + built-in function unicode() when converting byte strings to + unicode. Defaults to the package default. + + search_dirs: the list of directories in which to search when + loading a template by name or file name. If given a string, + the method interprets the string as a single directory. + Defaults to the package default. + + file_extension: the template file extension. Pass False for no + extension (i.e. to use extensionless template files). + Defaults to the package default. + + partials: an object (e.g. a dictionary) for custom partial loading + during the rendering process. + The object should have a get() method that accepts a string + and returns the corresponding template as a string, preferably + as a unicode string. If there is no template with that name, + the get() method should either return None (as dict.get() does) + or raise an exception. + If this argument is None, the rendering process will use + the normal procedure of locating and reading templates from + the file system -- using relevant instance attributes like + search_dirs, file_encoding, etc. + + escape: the function used to escape variable tag values when + rendering a template. The function should accept a unicode + string (or subclass of unicode) and return an escaped string + that is again unicode (or a subclass of unicode). + This function need not handle strings of type `str` because + this class will only pass it unicode strings. The constructor + assigns this function to the constructed instance's escape() + method. + To disable escaping entirely, one can pass `lambda u: u` + as the escape function, for example. One may also wish to + consider using markupsafe's escape function: markupsafe.escape(). + This argument defaults to the package default. + + missing_tags: a string specifying how to handle missing tags. + If 'strict', an error is raised on a missing tag. If 'ignore', + the value of the tag is the empty string. Defaults to the + package default. + + """ + if decode_errors is None: + decode_errors = defaults.DECODE_ERRORS + + if escape is None: + escape = defaults.TAG_ESCAPE + + if file_encoding is None: + file_encoding = defaults.FILE_ENCODING + + if file_extension is None: + file_extension = defaults.TEMPLATE_EXTENSION + + if missing_tags is None: + missing_tags = defaults.MISSING_TAGS + + if search_dirs is None: + search_dirs = defaults.SEARCH_DIRS + + if string_encoding is None: + string_encoding = defaults.STRING_ENCODING + + if isinstance(search_dirs, basestring): + search_dirs = [search_dirs] + + self._context = None + self.decode_errors = decode_errors + self.escape = escape + self.file_encoding = file_encoding + self.file_extension = file_extension + self.missing_tags = missing_tags + self.partials = partials + self.search_dirs = search_dirs + self.string_encoding = string_encoding + + # This is an experimental way of giving views access to the current context. + # TODO: consider another approach of not giving access via a property, + # but instead letting the caller pass the initial context to the + # main render() method by reference. This approach would probably + # be less likely to be misused. + @property + def context(self): + """ + Return the current rendering context [experimental]. + + """ + return self._context + + # We could not choose str() as the name because 2to3 renames the unicode() + # method of this class to str(). + def str_coerce(self, val): + """ + Coerce a non-string value to a string. + + This method is called whenever a non-string is encountered during the + rendering process when a string is needed (e.g. if a context value + for string interpolation is not a string). To customize string + coercion, you can override this method. + + """ + return str(val) + + def _to_unicode_soft(self, s): + """ + Convert a basestring to unicode, preserving any unicode subclass. + + """ + # We type-check to avoid "TypeError: decoding Unicode is not supported". + # We avoid the Python ternary operator for Python 2.4 support. + if isinstance(s, unicode): + return s + return self.unicode(s) + + def _to_unicode_hard(self, s): + """ + Convert a basestring to a string with type unicode (not subclass). + + """ + return unicode(self._to_unicode_soft(s)) + + def _escape_to_unicode(self, s): + """ + Convert a basestring to unicode (preserving any unicode subclass), and escape it. + + Returns a unicode string (not subclass). + + """ + return unicode(self.escape(self._to_unicode_soft(s))) + + def unicode(self, b, encoding=None): + """ + Convert a byte string to unicode, using string_encoding and decode_errors. + + Arguments: + + b: a byte string. + + encoding: the name of an encoding. Defaults to the string_encoding + attribute for this instance. + + Raises: + + TypeError: Because this method calls Python's built-in unicode() + function, this method raises the following exception if the + given string is already unicode: + + TypeError: decoding Unicode is not supported + + """ + if encoding is None: + encoding = self.string_encoding + + # TODO: Wrap UnicodeDecodeErrors with a message about setting + # the string_encoding and decode_errors attributes. + return unicode(b, encoding, self.decode_errors) + + def _make_loader(self): + """ + Create a Loader instance using current attributes. + + """ + return Loader(file_encoding=self.file_encoding, extension=self.file_extension, + to_unicode=self.unicode, search_dirs=self.search_dirs) + + def _make_load_template(self): + """ + Return a function that loads a template by name. + + """ + loader = self._make_loader() + + def load_template(template_name): + return loader.load_name(template_name) + + return load_template + + def _make_load_partial(self): + """ + Return a function that loads a partial by name. + + """ + if self.partials is None: + return self._make_load_template() + + # Otherwise, create a function from the custom partial loader. + partials = self.partials + + def load_partial(name): + # TODO: consider using EAFP here instead. + # http://docs.python.org/glossary.html#term-eafp + # This would mean requiring that the custom partial loader + # raise a KeyError on name not found. + template = partials.get(name) + if template is None: + raise TemplateNotFoundError("Name %s not found in partials: %s" % + (repr(name), type(partials))) + + # RenderEngine requires that the return value be unicode. + return self._to_unicode_hard(template) + + return load_partial + + def _is_missing_tags_strict(self): + """ + Return whether missing_tags is set to strict. + + """ + val = self.missing_tags + + if val == MissingTags.strict: + return True + elif val == MissingTags.ignore: + return False + + raise Exception("Unsupported 'missing_tags' value: %s" % repr(val)) + + def _make_resolve_partial(self): + """ + Return the resolve_partial function to pass to RenderEngine.__init__(). + + """ + load_partial = self._make_load_partial() + + if self._is_missing_tags_strict(): + return load_partial + # Otherwise, ignore missing tags. + + def resolve_partial(name): + try: + return load_partial(name) + except TemplateNotFoundError: + return u'' + + return resolve_partial + + def _make_resolve_context(self): + """ + Return the resolve_context function to pass to RenderEngine.__init__(). + + """ + if self._is_missing_tags_strict(): + return context_get + # Otherwise, ignore missing tags. + + def resolve_context(stack, name): + try: + return context_get(stack, name) + except KeyNotFoundError: + return u'' + + return resolve_context + + def _make_render_engine(self): + """ + Return a RenderEngine instance for rendering. + + """ + resolve_context = self._make_resolve_context() + resolve_partial = self._make_resolve_partial() + + engine = RenderEngine(literal=self._to_unicode_hard, + escape=self._escape_to_unicode, + resolve_context=resolve_context, + resolve_partial=resolve_partial, + to_str=self.str_coerce) + return engine + + # TODO: add unit tests for this method. + def load_template(self, template_name): + """ + Load a template by name from the file system. + + """ + load_template = self._make_load_template() + return load_template(template_name) + + def _render_object(self, obj, *context, **kwargs): + """ + Render the template associated with the given object. + + """ + loader = self._make_loader() + + # TODO: consider an approach that does not require using an if + # block here. For example, perhaps this class's loader can be + # a SpecLoader in all cases, and the SpecLoader instance can + # check the object's type. Or perhaps Loader and SpecLoader + # can be refactored to implement the same interface. + if isinstance(obj, TemplateSpec): + loader = SpecLoader(loader) + template = loader.load(obj) + else: + template = loader.load_object(obj) + + context = [obj] + list(context) + + return self._render_string(template, *context, **kwargs) + + def render_name(self, template_name, *context, **kwargs): + """ + Render the template with the given name using the given context. + + See the render() docstring for more information. + + """ + loader = self._make_loader() + template = loader.load_name(template_name) + return self._render_string(template, *context, **kwargs) + + def render_path(self, template_path, *context, **kwargs): + """ + Render the template at the given path using the given context. + + Read the render() docstring for more information. + + """ + loader = self._make_loader() + template = loader.read(template_path) + + return self._render_string(template, *context, **kwargs) + + def _render_string(self, template, *context, **kwargs): + """ + Render the given template string using the given context. + + """ + # RenderEngine.render() requires that the template string be unicode. + template = self._to_unicode_hard(template) + + render_func = lambda engine, stack: engine.render(template, stack) + + return self._render_final(render_func, *context, **kwargs) + + # All calls to render() should end here because it prepares the + # context stack correctly. + def _render_final(self, render_func, *context, **kwargs): + """ + Arguments: + + render_func: a function that accepts a RenderEngine and ContextStack + instance and returns a template rendering as a unicode string. + + """ + stack = ContextStack.create(*context, **kwargs) + self._context = stack + + engine = self._make_render_engine() + + return render_func(engine, stack) + + def render(self, template, *context, **kwargs): + """ + Render the given template string, view template, or parsed template. + + Returns a unicode string. + + Prior to rendering, this method will convert a template that is a + byte string (type str in Python 2) to unicode using the string_encoding + and decode_errors attributes. See the constructor docstring for + more information. + + Arguments: + + template: a template string that is unicode or a byte string, + a ParsedTemplate instance, or another object instance. In the + final case, the function first looks for the template associated + to the object by calling this class's get_associated_template() + method. The rendering process also uses the passed object as + the first element of the context stack when rendering. + + *context: zero or more dictionaries, ContextStack instances, or objects + with which to populate the initial context stack. None + arguments are skipped. Items in the *context list are added to + the context stack in order so that later items in the argument + list take precedence over earlier items. + + **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. + + """ + if is_string(template): + return self._render_string(template, *context, **kwargs) + if isinstance(template, ParsedTemplate): + render_func = lambda engine, stack: template.render(engine, stack) + return self._render_final(render_func, *context, **kwargs) + # Otherwise, we assume the template is an object. + + return self._render_object(template, *context, **kwargs) diff --git a/python/pystache/pystache/specloader.py b/python/pystache/pystache/specloader.py new file mode 100644 index 000000000..3a77d4c52 --- /dev/null +++ b/python/pystache/pystache/specloader.py @@ -0,0 +1,90 @@ +# coding: utf-8 + +""" +This module supports customized (aka special or specified) template loading. + +""" + +import os.path + +from pystache.loader import Loader + + +# TODO: add test cases for this class. +class SpecLoader(object): + + """ + Supports loading custom-specified templates (from TemplateSpec instances). + + """ + + def __init__(self, loader=None): + if loader is None: + loader = Loader() + + self.loader = loader + + def _find_relative(self, spec): + """ + Return the path to the template as a relative (dir, file_name) pair. + + The directory returned is relative to the directory containing the + class definition of the given object. The method returns None for + this directory if the directory is unknown without first searching + the search directories. + + """ + if spec.template_rel_path is not None: + return os.path.split(spec.template_rel_path) + # Otherwise, determine the file name separately. + + locator = self.loader._make_locator() + + # We do not use the ternary operator for Python 2.4 support. + if spec.template_name is not None: + template_name = spec.template_name + else: + template_name = locator.make_template_name(spec) + + file_name = locator.make_file_name(template_name, spec.template_extension) + + return (spec.template_rel_directory, file_name) + + def _find(self, spec): + """ + Find and return the path to the template associated to the instance. + + """ + if spec.template_path is not None: + return spec.template_path + + dir_path, file_name = self._find_relative(spec) + + locator = self.loader._make_locator() + + if dir_path is None: + # Then we need to search for the path. + path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name) + else: + obj_dir = locator.get_object_directory(spec) + path = os.path.join(obj_dir, dir_path, file_name) + + return path + + def load(self, spec): + """ + Find and return the template associated to a TemplateSpec instance. + + Returns the template as a unicode string. + + Arguments: + + spec: a TemplateSpec instance. + + """ + if spec.template is not None: + return self.loader.unicode(spec.template, spec.template_encoding) + + path = self._find(spec) + + return self.loader.read(path, spec.template_encoding) diff --git a/python/pystache/pystache/template_spec.py b/python/pystache/pystache/template_spec.py new file mode 100644 index 000000000..9e9f454c1 --- /dev/null +++ b/python/pystache/pystache/template_spec.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +""" +Provides a class to customize template information on a per-view basis. + +To customize template properties for a particular view, create that view +from a class that subclasses TemplateSpec. The "spec" in TemplateSpec +stands for "special" or "specified" template information. + +""" + +class TemplateSpec(object): + + """ + A mixin or interface for specifying custom template information. + + The "spec" in TemplateSpec can be taken to mean that the template + information is either "specified" or "special." + + A view should subclass this class only if customized template loading + is needed. The following attributes allow one to customize/override + template information on a per view basis. A None value means to use + default behavior for that value and perform no customization. All + attributes are initialized to None. + + Attributes: + + template: the template as a string. + + template_encoding: the encoding used by the template. + + template_extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. extensionless template files). + + template_name: the name of the template. + + template_path: absolute path to the template. + + template_rel_directory: the directory containing the template file, + relative to the directory containing the module defining the class. + + template_rel_path: the path to the template file, relative to the + directory containing the module defining the class. + + """ + + template = None + template_encoding = None + template_extension = None + template_name = None + template_path = None + template_rel_directory = None + template_rel_path = None |