Added Mustache renderer for NewsML XML.

This commit is contained in:
sv-ncp-3 root 2014-11-10 16:40:41 +01:00
parent 46175bc659
commit a335f7e702
19 changed files with 2228 additions and 2 deletions

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pystache
import re
class IPTCMessage(object):
@ -12,9 +13,10 @@ class IPTCMessage(object):
(self.fullheader, leftover) = self.raw[1:-1].split(b'\x02', 2)
(self.text, self.posttext) = leftover.split(b'\x03', 2)
self.parseHeader()
self.parseText()
self.parsePostText()
print("Header: " + repr(self.header))
print("Main text: " + self.text.decode("latin1"))
print("Main text: " + self.text)
print("Post-text: " + repr(self.posttext))
print("Post-data: " + repr(self.postdata))
# print("Message: %s" % repr(self.raw))
@ -34,6 +36,10 @@ class IPTCMessage(object):
"keywords": parts.group(7)
}
def parseText(self):
self.text = self.text.decode("latin1")
pass
def parsePostText(self):
posttext = self.posttext.decode("latin1")
# posttext = "191552 MEZ sep 14blafasel" # test string according to spec
@ -52,7 +58,10 @@ class IPTCMessage(object):
return self.raw
def getNewsML(self):
return '<?xml encoding="utf-8"?><result>Not yet implemented.</result>'
renderer = pystache.Renderer()
tpl = renderer.load_template('newsml')
xml = renderer.render(tpl, self)
return xml
if __name__=='__main__':
print("Testmode!")

View File

@ -16,7 +16,9 @@ class IPTCMessageTestCase(unittest.TestCase):
def testMessageParsing(self):
iptc = iptcmessage.IPTCMessage(DPATEST)
print(iptc.getNewsML())
iptc = iptcmessage.IPTCMessage(TEST1)
print(iptc.getNewsML())
self.assertGreater(iptc.length, 0)
if __name__=='__main__':

35
newsml.mustache Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<newsMessage xmlns="http://iptc.org/std/nar/2006-10-01/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:x="http://www.w3.org/1999/xhtml">
<header>
<sent></sent>
<sender>{{ header.source_id }}</sender>
<priority>{{ header.priority }}</priority>
</header>
<itemSet>
<newsItem>
<itemMeta>
<itemClass qcode="icls:text" />
</itemMeta>
<contentMeta>
<altId type="idType:USN">{{ header.message_no }}</altId>
<subject qcode="subj:{{ header.category }}" />
<slugline separator="-">{{ header.keywords }}</slugline>
<headline></headline>
</contentMeta>
<contentSet>
<inlineXML contenttype="application/xhtml+html" wordcount="{{ header.word_count }}">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title />
</head>
<body>
<p>
{{ text }}
</p>
</body>
</html>
</inlineXML>
</contentSet>
</newsItem>
</itemSet>
</newsMessage>

13
pystache/__init__.py Normal file
View File

@ -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.

View File

@ -0,0 +1,4 @@
"""
TODO: add a docstring.
"""

View File

@ -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()

18
pystache/commands/test.py Normal file
View File

@ -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()

71
pystache/common.py Normal file
View File

@ -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 (str, 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

342
pystache/context.py Normal file
View File

@ -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)

65
pystache/defaults.py Normal file
View File

@ -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'

19
pystache/init.py Normal file
View File

@ -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)

170
pystache/loader.py Normal file
View File

@ -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 str(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, str):
return str(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)

171
pystache/locator.py Normal file
View File

@ -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

50
pystache/parsed.py Normal file
View File

@ -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 str:
return node
return node.render(engine, context)
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
return str(s)

378
pystache/parser.py Normal file
View File

@ -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(r'^(.)', 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 str:
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 + r'\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 str(''.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))

181
pystache/renderengine.py Normal file
View File

@ -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)

460
pystache/renderer.py Normal file
View File

@ -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, str):
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, str):
return s
return self.unicode(s)
def _to_unicode_hard(self, s):
"""
Convert a basestring to a string with type unicode (not subclass).
"""
return str(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 str(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 str(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)

90
pystache/specloader.py Normal file
View File

@ -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)

53
pystache/template_spec.py Normal file
View File

@ -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