Added Mustache renderer for NewsML XML.
This commit is contained in:
parent
46175bc659
commit
a335f7e702
@ -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!")
|
||||
|
@ -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
35
newsml.mustache
Normal 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
13
pystache/__init__.py
Normal 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.
|
4
pystache/commands/__init__.py
Normal file
4
pystache/commands/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""
|
||||
TODO: add a docstring.
|
||||
|
||||
"""
|
95
pystache/commands/render.py
Normal file
95
pystache/commands/render.py
Normal 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
18
pystache/commands/test.py
Normal 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
71
pystache/common.py
Normal 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
342
pystache/context.py
Normal 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
65
pystache/defaults.py
Normal 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
19
pystache/init.py
Normal 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
170
pystache/loader.py
Normal 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
171
pystache/locator.py
Normal 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
50
pystache/parsed.py
Normal 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
378
pystache/parser.py
Normal 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
181
pystache/renderengine.py
Normal 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
460
pystache/renderer.py
Normal 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
90
pystache/specloader.py
Normal 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
53
pystache/template_spec.py
Normal 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
|
Reference in New Issue
Block a user