Commit a4d763d5 by Chris Jerdonek

Merge branch 'development' to 'master': staging version 0.5.0-rc.

parents a06fd423 368f0dfd
[submodule "ext/spec"]
path = ext/spec
url = http://github.com/mustache/spec.git
History History
======= =======
0.5.0 (2012-04-03)
------------------
This version represents a major rewrite and refactoring of the code base
that also adds features and fixes many bugs. All functionality and nearly
all unit tests have been preserved. However, some backwards incompatible
changes to the API have been made.
Below is a selection of some of the changes (not exhaustive).
Highlights:
* Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande]
* Removed View class: it is no longer necessary to subclass from View or
from any other class to create a view.
* Replaced Template with Renderer class: template rendering behavior can be
modified via the Renderer constructor or by setting attributes on a Renderer instance.
* Added TemplateSpec class: template rendering can be specified on a per-view
basis by subclassing from TemplateSpec.
* Introduced separation of concerns and removed circular dependencies (e.g.
between Template and View classes, cf. `issue #13`_).
* Unicode now used consistently throughout the rendering process.
* Expanded test coverage: nosetests now runs doctests and ~105 test cases
from the Mustache spec (increasing the number of tests from 56 to ~315).
* Added a rudimentary benchmarking script to gauge performance while refactoring.
* Extensive documentation added (e.g. docstrings).
Other changes:
* Added a command-line interface. [vrde]
* The main rendering class now accepts a custom partial loader (e.g. a dictionary)
and a custom escape function.
* Non-ascii characters in str strings are now supported while rendering.
* Added string encoding, file encoding, and errors options for decoding to unicode.
* Removed the output encoding option.
* Removed the use of markupsafe.
Bug fixes:
* Context values no longer processed as template strings. [jakearchibald]
* Whitespace surrounding sections is no longer altered, per the spec. [heliodor]
* Zeroes now render correctly when using PyPy. [alex]
* Multline comments now permitted. [fczuardi]
* Extensionless template files are now supported.
* Passing ``**kwargs`` to ``Template()`` no longer modifies the context.
* Passing ``**kwargs`` to ``Template()`` with no context no longer raises an exception.
0.4.1 (2012-03-25) 0.4.1 (2012-03-25)
------------------ ------------------
* Added support for Python 2.4. [wangtz, jvantuyl] * Added support for Python 2.4. [wangtz, jvantuyl]
...@@ -44,3 +91,7 @@ History ...@@ -44,3 +91,7 @@ History
------------------ ------------------
* First release * First release
.. _issue #13: https://github.com/defunkt/pystache/issues/13
.. _Mustache spec: https://github.com/mustache/spec
Copyright (C) 2012 Chris Jerdonek. All rights reserved.
Copyright (c) 2009 Chris Wanstrath Copyright (c) 2009 Chris Wanstrath
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
......
...@@ -4,27 +4,38 @@ Pystache ...@@ -4,27 +4,38 @@ Pystache
.. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png .. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png
Inspired by ctemplate_ and et_, Mustache_ is a Pystache_ is a Python implementation of Mustache_.
framework-agnostic way to render logic-free views. Mustache is a framework-agnostic, logic-free templating system inspired
by ctemplate_ and et_. Like ctemplate, Mustache "emphasizes
separating logic from presentation: it is impossible to embed application
logic in this template language."
As ctemplates says, "It emphasizes separating logic from presentation: The `mustache(5)`_ man page provides a good introduction to Mustache's
it is impossible to embed application logic in this template language." syntax. For a more complete (and more current) description of Mustache's
behavior, see the official `Mustache spec`_.
Pystache is a Python implementation of Mustache. Pystache works on-- Pystache is `semantically versioned`_ and can be found on PyPI_. This
version of Pystache passes all tests in `version 1.0.3`_ of the spec.
* Python 2.4 Logo: `David Phillips`_
* Python 2.5
* Python 2.6
* Python 2.7
Pystache is semantically versioned: http://semver.org.
Logo: David Phillips - http://davidphillips.us/ Requirements
============
Documentation Pystache is tested with the following versions of Python:
=============
* Python 2.4 (requires simplejson version 2.0.9 or earlier)
* Python 2.5 (requires simplejson)
* Python 2.6
* Python 2.7
JSON support is needed only for the command-line interface and to run the
spec tests. Python's json_ module is new as of Python 2.6. Python's
simplejson_ package works with earlier versions of Python. Because
simplejson stopped officially supporting Python 2.4 as of version 2.1.0,
Python 2.4 requires an earlier version.
The different Mustache tags are documented at `mustache(5)`_.
Install It Install It
========== ==========
...@@ -41,25 +52,64 @@ Use It ...@@ -41,25 +52,64 @@ Use It
>>> import pystache >>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'}) >>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
'Hi Mom!' u'Hi Mom!'
You can also create dedicated view classes to hold your view logic. You can also create dedicated view classes to hold your view logic.
Here's your simple.py:: Here's your view class (in examples/readme.py)::
class SayHello(object):
import pystache def to(self):
class Simple(pystache.View): return "Pizza"
def thing(self):
return "pizza"
Then your template, simple.mustache:: Like so::
Hi {{thing}}! >>> from examples.readme import SayHello
>>> hello = SayHello()
Then your template, say_hello.mustache::
Hello, {{to}}!
Pull it together:: Pull it together::
>>> Simple().render() >>> renderer = pystache.Renderer()
'Hi pizza!' >>> renderer.render(hello)
u'Hello, Pizza!'
Unicode Handling
================
This section describes Pystache's handling of unicode (e.g. strings and
encodings).
Internally, Pystache uses `only unicode strings`_. For input, Pystache accepts
both ``unicode`` and ``str`` strings. For output, Pystache's template
rendering methods return only unicode.
Pystache's ``Renderer`` class supports a number of attributes that control how
Pystache converts ``str`` strings to unicode on input. These include the
``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes.
The ``file_encoding`` attribute is the encoding the renderer uses to convert
to unicode any files read from the file system. Similarly, ``string_encoding``
is the encoding the renderer uses to convert to unicode any other strings of
type ``str`` encountered during the rendering process (e.g. context values
of type ``str``).
The ``decode_errors`` attribute is what the renderer passes as the ``errors``
argument to Python's `built-in unicode function`_ ``unicode()`` when
converting. The valid values for this argument are ``strict``, ``ignore``,
and ``replace``.
Each of these attributes can be set via the ``Renderer`` class's constructor
using a keyword argument of the same name. See the Renderer class's
docstrings for further details. In addition, the ``file_encoding``
attribute can be controlled on a per-view basis by subclassing the
``TemplateSpec`` class. When not specified explicitly, these attributes
default to values set in Pystache's ``defaults`` module.
Test It Test It
...@@ -76,27 +126,57 @@ to type, for example :: ...@@ -76,27 +126,57 @@ to type, for example ::
nosetests-2.4 nosetests-2.4
To include tests from the Mustache spec in your test runs: ::
git submodule init
git submodule update
To run all available tests (including doctests)::
nosetests --with-doctest --doctest-extension=rst
or alternatively (using setup.cfg)::
python setup.py nosetests
To run a subset of the tests, you can use this pattern, for example: ::
nosetests --tests tests/test_context.py:GetValueTests.test_dictionary__key_present
Mailing List Mailing List
================== ============
As of Nov 26, 2011, there's a mailing list, pystache@librelist.com.
As of November 2011, there's a mailing list, pystache@librelist.com.
Archive: http://librelist.com/browser/pystache/ Archive: http://librelist.com/browser/pystache/
Note: There's a bit of a delay in seeing the latest emails appear Note: There's a bit of a delay in seeing the latest emails appear
in the archive. in the archive.
Author Author
====== ======
:: ::
context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' } >>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
pystache.render("{{author}} :: {{email}}", context) >>> pystache.render("{{author}} :: {{email}}", context)
u'Chris Wanstrath :: chris@ozmm.org'
.. _ctemplate: http://code.google.com/p/google-ctemplate/ .. _ctemplate: http://code.google.com/p/google-ctemplate/
.. _David Phillips: http://davidphillips.us/
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _Mustache: http://defunkt.github.com/mustache/ .. _json: http://docs.python.org/library/json.html
.. _Mustache: http://mustache.github.com/
.. _Mustache spec: https://github.com/mustache/spec
.. _mustache(5): http://mustache.github.com/mustache.5.html .. _mustache(5): http://mustache.github.com/mustache.5.html
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
.. _PyPI: http://pypi.python.org/pypi/pystache
.. _Pystache: https://github.com/defunkt/pystache
.. _semantically versioned: http://semver.org
.. _simplejson: http://pypi.python.org/pypi/simplejson/
.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode
.. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7
import pystache class Comments(object):
class Comments(pystache.View):
template_path = 'examples'
def title(self): def title(self):
return "A Comedy of Errors" return "A Comedy of Errors"
<h1>{{ header }}</h1>
{{#list}}
<ul>
{{#item}}{{# current }}<li><strong>{{name}}</strong></li>
{{/ current }}{{#link}}<li><a href="{{url}}">{{name}}</a></li>
{{/link}}{{/item}}</ul>{{/list}}{{#empty}}<p>The list is empty.</p>{{/empty}}
\ No newline at end of file
import pystache class Complex(object):
class ComplexView(pystache.View):
template_path = 'examples'
def header(self): def header(self):
return "Colors" return "Colors"
......
<h1>{{ header }}</h1>{{#list}}<ul>{{#item}}{{# current }}<li><strong>{{name}}</strong></li>{{/ current }}{{#link}}<li><a href="{{url}}">{{name}}</a></li>{{/link}}{{/item}}</ul>{{/list}}{{#empty}}<p>The list is empty.</p>{{/empty}}
\ No newline at end of file
import pystache class Delimiters(object):
class Delimiters(pystache.View):
template_path = 'examples'
def first(self): def first(self):
return "It worked the first time." return "It worked the first time."
......
import pystache class DoubleSection(object):
class DoubleSection(pystache.View):
template_path = 'examples'
def t(self): def t(self):
return True return True
......
import pystache class Escaped(object):
class Escaped(pystache.View):
template_path = 'examples'
def title(self): def title(self):
return "Bear > Shark" return "Bear > Shark"
No file extension: {{foo}}
\ No newline at end of file
import pystache from pystache import TemplateSpec
class Inverted(pystache.View): class Inverted(object):
template_path = 'examples'
def t(self): def t(self):
return True return True
...@@ -18,7 +17,7 @@ class Inverted(pystache.View): ...@@ -18,7 +17,7 @@ class Inverted(pystache.View):
def populated_list(self): def populated_list(self):
return ['some_value'] return ['some_value']
class InvertedLists(Inverted): class InvertedLists(Inverted, TemplateSpec):
template_name = 'inverted' template_name = 'inverted'
def t(self): def t(self):
......
import pystache from pystache import TemplateSpec
def rot(s, n=13): def rot(s, n=13):
r = "" r = ""
...@@ -17,8 +17,10 @@ def rot(s, n=13): ...@@ -17,8 +17,10 @@ def rot(s, n=13):
def replace(subject, this='foo', with_this='bar'): def replace(subject, this='foo', with_this='bar'):
return subject.replace(this, with_this) return subject.replace(this, with_this)
class Lambdas(pystache.View):
template_path = 'examples' # This class subclasses TemplateSpec because at least one unit test
# sets the template attribute.
class Lambdas(TemplateSpec):
def replace_foo_with_bar(self, text=None): def replace_foo_with_bar(self, text=None):
return replace return replace
......
import pystache from pystache import TemplateSpec
class NestedContext(pystache.View): class NestedContext(TemplateSpec):
template_path = 'examples'
def __init__(self, renderer):
self.renderer = renderer
def _context_get(self, key):
return self.renderer.context.get(key)
def outer_thing(self): def outer_thing(self):
return "two" return "two"
...@@ -16,6 +21,6 @@ class NestedContext(pystache.View): ...@@ -16,6 +21,6 @@ class NestedContext(pystache.View):
return [{'outer': 'car'}] return [{'outer': 'car'}]
def nested_context_in_view(self): def nested_context_in_view(self):
if self.get('outer') == self.get('inner'): if self._context_get('outer') == self._context_get('inner'):
return 'it works!' return 'it works!'
return '' return ''
import pystache
from examples.lambdas import rot from examples.lambdas import rot
class PartialsWithLambdas(pystache.View): class PartialsWithLambdas(object):
template_path = 'examples'
def rot(self): def rot(self):
return rot return rot
\ No newline at end of file
class SayHello(object):
def to(self):
return "Pizza"
Hello, {{to}}!
\ No newline at end of file
import pystache from pystache import TemplateSpec
class Simple(pystache.View): class Simple(TemplateSpec):
template_path = 'examples'
def thing(self): def thing(self):
return "pizza" return "pizza"
......
No tags...
\ No newline at end of file
import pystache from pystache import TemplateSpec
class TemplatePartial(pystache.View): class TemplatePartial(TemplateSpec):
template_path = 'examples'
def __init__(self, renderer):
self.renderer = renderer
def _context_get(self, key):
return self.renderer.context.get(key)
def title(self): def title(self):
return "Welcome" return "Welcome"
...@@ -13,4 +18,4 @@ class TemplatePartial(pystache.View): ...@@ -13,4 +18,4 @@ class TemplatePartial(pystache.View):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
def thing(self): def thing(self):
return self['prop'] return self._context_get('prop')
\ No newline at end of file \ No newline at end of file
import pystache class Unescaped(object):
class Unescaped(pystache.View):
template_path = 'examples'
def title(self): def title(self):
return "Bear > Shark" return "Bear > Shark"
<p>If alive today, Henri Poincaré would be {{age}} years old.</p> abcdé
\ No newline at end of file \ No newline at end of file
import pystache from pystache import TemplateSpec
class UnicodeInput(TemplateSpec):
class UnicodeInput(pystache.View):
template_path = 'examples'
template_encoding = 'utf8' template_encoding = 'utf8'
def age(self): def age(self):
......
# encoding: utf-8 # encoding: utf-8
import pystache class UnicodeOutput(object):
class UnicodeOutput(pystache.View):
template_path = 'examples'
def name(self): def name(self):
return u'Henri Poincaré' return u'Henri Poincaré'
Subproject commit 48c933b0bb780875acbfd15816297e263c53d6f7
from pystache.template import Template # We keep all initialization code in a separate module.
from pystache.view import View from init import *
from pystache.loader import Loader
def render(template, context=None, **kwargs):
context = context and context.copy() or {}
context.update(kwargs)
return Template(template, context).render()
# 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.
import simplejson as json
# 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.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
def main(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 IOError:
pass
try:
context = json.load(open(context))
except IOError:
context = json.loads(context)
rendered = renderer.render(template, context)
print rendered
if __name__=='__main__':
main(sys.argv)
# coding: utf-8
"""
Defines a Context class to represent mustache(5)'s notion of context.
"""
class NotFound(object): pass
# We use this private global variable as a return value to represent a key
# not being found on lookup. This lets us distinguish between the case
# of a key's value being None with the case of a key not being found --
# without having to rely on exceptions (e.g. KeyError) for flow control.
_NOT_FOUND = NotFound()
# TODO: share code with template.check_callable().
def _is_callable(obj):
return hasattr(obj, '__call__')
def _get_value(item, key):
"""
Retrieve a key's value from an item.
Returns _NOT_FOUND if the key does not exist.
The Context.get() docstring documents this function's intended behavior.
"""
if isinstance(item, dict):
# Then we consider the argument a "hash" for the purposes of the spec.
#
# We do a membership test to avoid using exceptions for flow control
# (e.g. catching KeyError).
if key in item:
return item[key]
elif type(item).__module__ != '__builtin__':
# Then we consider the argument an "object" for the purposes of
# the spec.
#
# The elif test above lets us avoid treating instances of built-in
# types like integers and strings as objects (cf. issue #81).
# Instances of user-defined classes on the other hand, for example,
# are considered objects by the test above.
if hasattr(item, key):
attr = getattr(item, key)
if _is_callable(attr):
return attr()
return attr
return _NOT_FOUND
class Context(object):
"""
Provides dictionary-like access to a stack of zero or more items.
Instances of this class are meant to act as the rendering context
when rendering Mustache templates in accordance with mustache(5)
and the Mustache spec.
Instances encapsulate a private stack of hashes, objects, and built-in
type instances. Querying the stack for the value of a key queries
the items in the stack in order from last-added objects to first
(last in, first out).
Caution: this class does not currently support recursive nesting in
that items in the stack cannot themselves be Context instances.
See the docstrings of the methods of this class for more details.
"""
# We reserve keyword arguments for future options (e.g. a "strict=True"
# option for enabling a strict mode).
def __init__(self, *items):
"""
Construct an instance, and initialize the private stack.
The *items arguments are the items with which to populate the
initial stack. Items in the argument list are added to the
stack in order so that, in particular, items at the end of
the argument list are queried first when querying the stack.
Caution: items should not themselves be Context instances, as
recursive nesting does not behave as one might expect.
"""
self._stack = list(items)
def __repr__(self):
"""
Return a string representation of the instance.
For example--
>>> context = Context({'alpha': 'abc'}, {'numeric': 123})
>>> repr(context)
"Context({'alpha': 'abc'}, {'numeric': 123})"
"""
return "%s%s" % (self.__class__.__name__, tuple(self._stack))
@staticmethod
def create(*context, **kwargs):
"""
Build a Context instance from a sequence of context-like items.
This factory-style method is more general than the Context class's
constructor in that, unlike the constructor, the argument list
can itself contain Context instances.
Here is an example illustrating various aspects of this method:
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
>>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'})
>>>
>>> context = Context.create(obj1, None, obj2, mineral='gold')
>>>
>>> context.get('animal')
'cat'
>>> context.get('vegetable')
'spinach'
>>> context.get('mineral')
'gold'
Arguments:
*context: zero or more dictionaries, Context instances, or objects
with which to populate the initial context stack. None
arguments will be skipped. Items in the *context list are
added to the stack in order so that later items in the argument
list take precedence over earlier items. This behavior is the
same as the constructor's.
**kwargs: additional key-value data to add to the context stack.
As these arguments appear after all items in the *context list,
in the case of key conflicts these values take precedence over
all items in the *context list. This behavior is the same as
the constructor's.
"""
items = context
context = Context()
for item in items:
if item is None:
continue
if isinstance(item, Context):
context._stack.extend(item._stack)
else:
context.push(item)
if kwargs:
context.push(kwargs)
return context
def get(self, key, default=None):
"""
Query the stack for the given key, and return the resulting value.
This method queries items in the stack in order from last-added
objects to first (last in, first out). The value returned is
the value of the key in the first item that contains the key.
If the key is not found in any item in the stack, then the default
value is returned. The default value defaults to None.
When speaking about returning values from a context, the Mustache
spec distinguishes between two types of context stack elements:
hashes and objects.
In accordance with the spec, this method queries items in the
stack for a key in the following way. For the purposes of querying,
each item is classified into one of the following three mutually
exclusive categories: a hash, an object, or neither:
(1) Hash: if the item's type is a subclass of dict, then the item
is considered a hash (in the terminology of the spec), and
the key's value is the dictionary value of the key. If the
dictionary doesn't contain the key, the key is not found.
(2) Object: if the item isn't a hash and isn't an instance of a
built-in type, then the item is considered an object (again
using the language of the spec). In this case, the method
looks for an attribute with the same name as the key. If an
attribute with that name exists, the value of the attribute is
returned. If the attribute is callable, however (i.e. if the
attribute is a method), then the attribute is called with no
arguments and instead that value returned. If there is no
attribute with the same name as the key, then the key is
considered not found.
(3) Neither: if the item is neither a hash nor an object, then
the key is considered not found.
*Caution*:
Callables are handled differently depending on whether they are
dictionary values, as in (1) above, or attributes, as in (2).
The former are returned as-is, while the latter are first
called and that value returned.
Here is an example to illustrate:
>>> def greet():
... return "Hi Bob!"
>>>
>>> class Greeter(object):
... greet = None
>>>
>>> dct = {'greet': greet}
>>> obj = Greeter()
>>> obj.greet = greet
>>>
>>> dct['greet'] is obj.greet
True
>>> Context(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
>>> Context(obj).get('greet')
'Hi Bob!'
TODO: explain the rationale for this difference in treatment.
"""
for obj in reversed(self._stack):
val = _get_value(obj, key)
if val is _NOT_FOUND:
continue
# Otherwise, the key was found.
return val
# Otherwise, no item in the stack contained the key.
return default
def push(self, item):
"""
Push an item onto the stack.
"""
self._stack.append(item)
def pop(self):
"""
Pop an item off of the stack, and return it.
"""
return self._stack.pop()
def top(self):
"""
Return the item last added to the stack.
"""
return self._stack[-1]
def copy(self):
"""
Return a copy of this instance.
"""
return Context(*self._stack)
# 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.
"""
import cgi
import os
import sys
# 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 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 quotes to be escaped,
# but not single quotes:
#
# http://docs.python.org/library/cgi.html#cgi.escape
#
TAG_ESCAPE = lambda u: cgi.escape(u, quote=True)
# The default template extension.
TEMPLATE_EXTENSION = 'mustache'
# encoding: utf-8
"""
This module contains the initialization logic called by __init__.py.
"""
from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
__all__ = ['render', 'Renderer', '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)
# coding: utf-8
"""
This module provides a Loader class for locating and reading templates.
"""
import os import os
import sys
from pystache import defaults
from pystache.locator import Locator
def _to_unicode(s, encoding=None):
"""
Raises a TypeError exception if the given string is already unicode.
"""
if encoding is None:
encoding = defaults.STRING_ENCODING
return unicode(s, encoding, defaults.DECODE_ERRORS)
class Loader(object): class Loader(object):
template_extension = 'mustache' """
template_path = '.' Loads the template associated to a name or user-defined object.
template_encoding = None
"""
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. Pass False for no
extension (i.e. 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
def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): if search_dirs is None:
'''Returns the template string from a file or throws IOError if it non existent''' search_dirs = defaults.SEARCH_DIRS
if None == template_dirs:
template_dirs = self.template_path
if encoding is not None: if to_unicode is None:
self.template_encoding = encoding to_unicode = _to_unicode
if extension is not None: self.extension = extension
self.template_extension = extension self.file_encoding = file_encoding
# TODO: unit test setting this attribute.
self.search_dirs = search_dirs
self.to_unicode = to_unicode
file_name = template_name + '.' + self.template_extension def _make_locator(self):
return Locator(extension=self.extension)
# Given a single directory we'll load from it def unicode(self, s, encoding=None):
if isinstance(template_dirs, basestring): """
file_path = os.path.join(template_dirs, file_name) Convert a string to unicode using the given encoding, and return it.
return self._load_template_file(file_path) This function uses the underlying to_unicode attribute.
# Given a list of directories we'll check each for our file Arguments:
for path in template_dirs:
file_path = os.path.join(path, file_name)
if os.path.exists(file_path):
return self._load_template_file(file_path)
raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),)) 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.")
def _load_template_file(self, file_path): encoding: the encoding to pass to the to_unicode attribute.
'''Loads and returns the template file from disk''' Defaults to None.
f = open(file_path, 'r')
"""
if isinstance(s, unicode):
return unicode(s)
return self.to_unicode(s, encoding)
def read(self, path, encoding=None):
"""
Read the template at the given path, and return it as a unicode string.
"""
# We avoid use of the with keyword for Python 2.4 support.
f = open(path, 'r')
try: try:
template = f.read() text = f.read()
if self.template_encoding:
template = unicode(template, self.template_encoding)
finally: finally:
f.close() f.close()
return template if encoding is None:
\ No newline at end of file encoding = self.file_encoding
return self.unicode(text, encoding)
# TODO: unit-test this method.
def load_name(self, name):
"""
Find and return the template with the given name.
Arguments:
name: the name of the template.
search_dirs: the list of directories in which to search.
"""
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)
# coding: utf-8
"""
This module provides a Locator class for finding template files.
"""
import os
import re
import sys
from pystache import defaults
class Locator(object):
def __init__(self, extension=None):
"""
Construct a template locator.
Arguments:
extension: the template file extension. Pass False for no
extension (i.e. 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:
# TODO: we should probably raise an exception of our own type.
raise IOError('Template file %s not found in directories: %s' %
(repr(file_name), repr(search_dirs)))
return path
def find_name(self, template_name, search_dirs):
"""
Return the path to a template with the given name.
"""
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
# coding: utf-8
"""
Exposes a class that represents a parsed (or compiled) template.
This module is meant only for internal use.
"""
class ParsedTemplate(object):
def __init__(self, parse_tree):
"""
Arguments:
parse_tree: a list, each element of which is either--
(1) a unicode string, or
(2) a "rendering" callable that accepts a Context instance
and returns a unicode string.
The possible rendering callables are the return values of the
following functions:
* RenderEngine._make_get_escaped()
* RenderEngine._make_get_inverse()
* RenderEngine._make_get_literal()
* RenderEngine._make_get_partial()
* RenderEngine._make_get_section()
"""
self._parse_tree = parse_tree
def render(self, context):
"""
Returns: a string of type unicode.
"""
# We avoid use of the ternary operator for Python 2.4 support.
def get_unicode(val):
if callable(val):
return val(context)
return val
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
return unicode(s)
# coding: utf-8
"""
Provides a class for parsing template strings.
This module is only meant for internal use by the renderengine module.
"""
import re
from parsed import ParsedTemplate
DEFAULT_DELIMITERS = ('{{', '}}')
END_OF_LINE_CHARACTERS = ['\r', '\n']
NON_BLANK_RE = re.compile(r'^(.)', re.M)
def _compile_template_re(delimiters):
# 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
class Parser(object):
_delimiters = None
_template_re = None
def __init__(self, engine, delimiters=None):
"""
Construct an instance.
Arguments:
engine: a RenderEngine instance.
"""
if delimiters is None:
delimiters = DEFAULT_DELIMITERS
self._delimiters = delimiters
self.engine = engine
def compile_template_re(self):
self._template_re = _compile_template_re(self._delimiters)
def _change_delimiters(self, delimiters):
self._delimiters = delimiters
self.compile_template_re()
def parse(self, template, index=0, section_key=None):
"""
Parse a template string into a ParsedTemplate instance.
This method uses the current tag delimiter.
Arguments:
template: a template string of type unicode.
"""
parse_tree = []
start_index = index
while True:
match = self._template_re.search(template, index)
if match is None:
break
match_index = match.start()
end_index = match.end()
before_tag = template[index : match_index]
parse_tree.append(before_tag)
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:
parse_tree.append(leading_whitespace)
match_index += len(leading_whitespace)
leading_whitespace = ''
if tag_type == '/':
if tag_key != section_key:
raise ParsingError("Section end tag mismatch: %s != %s" % (repr(tag_key), repr(section_key)))
return ParsedTemplate(parse_tree), template[start_index:match_index], end_index
index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
# Save the rest of the template.
parse_tree.append(template[index:])
return ParsedTemplate(parse_tree)
def _parse_section(self, template, index_start, section_key):
parsed_template, template, index_end = self.parse(template=template, index=index_start, section_key=section_key)
return parsed_template, template, index_end
def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!':
return end_index
if tag_type == '=':
delimiters = tag_key.split()
self._change_delimiters(delimiters)
return end_index
engine = self.engine
if tag_type == '':
func = engine._make_get_escaped(tag_key)
elif tag_type == '&':
func = engine._make_get_literal(tag_key)
elif tag_type == '#':
parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_section(tag_key, parsed_section, template, self._delimiters)
elif tag_type == '^':
parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_inverse(tag_key, parsed_section)
elif tag_type == '>':
template = engine.load_partial(tag_key)
# Indent before rendering.
template = re.sub(NON_BLANK_RE, leading_whitespace + r'\1', template)
func = engine._make_get_partial(template)
else:
raise Exception("Unrecognized tag type: %s" % repr(tag_type))
parse_tree.append(func)
return end_index
# coding: utf-8
"""
Defines a class responsible for rendering logic.
"""
import re
from parser import Parser
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).
"""
def __init__(self, load_partial=None, literal=None, escape=None):
"""
Arguments:
load_partial: the function to call when loading a partial. The
function should accept a string template name and return a
template string of type unicode (not a subclass).
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 markupssafe.Markup differently
from plain unicode strings.
"""
self.escape = escape
self.literal = literal
self.load_partial = load_partial
def _get_string_value(self, context, tag_name):
"""
Get a value from the given context as a basestring instance.
"""
val = context.get(tag_name)
# We use "==" rather than "is" to compare integers, as using "is"
# relies on an implementation detail of CPython. The test about
# rendering zeroes failed while using PyPy when using "is".
# See issue #34: https://github.com/defunkt/pystache/issues/34
if not val and val != 0:
if tag_name != '.':
return ''
val = context.top()
if callable(val):
# According to 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.
template = val()
if not isinstance(template, basestring):
# In case the template is an integer, for example.
template = str(template)
if type(template) is not unicode:
template = self.literal(template)
val = self._render(template, context)
if not isinstance(val, basestring):
val = str(val)
return val
def _make_get_literal(self, name):
def get_literal(context):
"""
Returns: a string of type unicode.
"""
s = self._get_string_value(context, name)
s = self.literal(s)
return s
return get_literal
def _make_get_escaped(self, name):
get_literal = self._make_get_literal(name)
def get_escaped(context):
"""
Returns: a string of type unicode.
"""
s = self._get_string_value(context, name)
s = self.escape(s)
return s
return get_escaped
def _make_get_partial(self, template):
def get_partial(context):
"""
Returns: a string of type unicode.
"""
return self._render(template, context)
return get_partial
def _make_get_inverse(self, name, parsed_template):
def get_inverse(context):
"""
Returns a string with type unicode.
"""
data = context.get(name)
if data:
return u''
return parsed_template.render(context)
return get_inverse
# 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 _make_get_section(self, name, parsed_template_, template_, delims):
def get_section(context):
"""
Returns: a string of type unicode.
"""
template = template_
parsed_template = parsed_template_
data = context.get(name)
if not data:
data = []
elif callable(data):
# TODO: should we check the arity?
template = data(template)
parsed_template = self._parse(template, delimiters=delims)
data = [ data ]
elif not hasattr(data, '__iter__') or isinstance(data, dict):
data = [ data ]
parts = []
for element in data:
context.push(element)
parts.append(parsed_template.render(context))
context.pop()
return unicode(''.join(parts))
return get_section
def _parse(self, template, delimiters=None):
"""
Parse the given template, and return a ParsedTemplate instance.
Arguments:
template: a template string of type unicode.
"""
parser = Parser(self, delimiters=delimiters)
parser.compile_template_re()
return parser.parse(template=template)
def _render(self, template, context):
"""
Returns: a string of type unicode.
Arguments:
template: a template string of type unicode.
context: a Context instance.
"""
# We keep this type-check as an added check because this method is
# called with template strings coming from potentially externally-
# supplied functions like self.literal, self.load_partial, etc.
# Beyond this point, we have much better control over the type.
if type(template) is not unicode:
raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template)))
parsed_template = self._parse(template)
return parsed_template.render(context)
def render(self, template, context):
"""
Return a template rendered as a string with type unicode.
Arguments:
template: a template string of type unicode (but not a proper
subclass of unicode).
context: a Context instance.
"""
# Be strict but not too strict. In other words, accept str instead
# of unicode, but don't assume anything about the encoding (e.g.
# don't use self.literal).
template = unicode(template)
return self._render(template, context)
# coding: utf-8
"""
This module provides a Renderer class to render templates.
"""
from pystache import defaults
from pystache.context import Context
from pystache.loader import Loader
from pystache.renderengine import RenderEngine
from pystache.spec_loader 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. Among these, the constructor supports
passing a custom partial loader.
Here is an example of rendering a template using a custom partial loader
that loads partials from a string-string dictionary.
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials)
>>> renderer.render('{{>partial}}', {'thing': 'world'})
u'Hello, world!'
"""
def __init__(self, file_encoding=None, string_encoding=None,
decode_errors=None, search_dirs=None, file_extension=None,
escape=None, partials=None):
"""
Construct an instance.
Arguments:
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.
decode_errors: the string to pass as the errors argument to the
built-in function unicode() when converting str strings to
unicode. Defaults to the package default.
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.
The argument defaults to `cgi.escape(s, quote=True)`. 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().
file_encoding: the name of the default encoding to use when reading
template files. All templates are converted to unicode prior
to parsing. This encoding is used when reading template files
and converting them to unicode. 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.
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.
string_encoding: the name of the encoding to use when converting
to unicode any strings of type str encountered during the
rendering process. The name will be passed as the encoding
argument to the built-in function unicode(). 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 search_dirs is None:
search_dirs = defaults.SEARCH_DIRS
if string_encoding is None:
string_encoding = defaults.STRING_ENCODING
if isinstance(search_dirs, basestring):
search_dirs = [search_dirs]
self._context = None
self.decode_errors = decode_errors
self.escape = escape
self.file_encoding = file_encoding
self.file_extension = file_extension
self.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
def _to_unicode_soft(self, s):
"""
Convert a basestring to unicode, preserving any unicode subclass.
"""
# We type-check to avoid "TypeError: decoding Unicode is not supported".
# We avoid the Python ternary operator for Python 2.4 support.
if isinstance(s, unicode):
return s
return self.unicode(s)
def _to_unicode_hard(self, s):
"""
Convert a basestring to a string with type unicode (not subclass).
"""
return unicode(self._to_unicode_soft(s))
def _escape_to_unicode(self, s):
"""
Convert a basestring to unicode (preserving any unicode subclass), and escape it.
Returns a unicode string (not subclass).
"""
return unicode(self.escape(self._to_unicode_soft(s)))
def unicode(self, s, encoding=None):
"""
Convert a string to unicode, using string_encoding and decode_errors.
Raises:
TypeError: Because this method calls Python's built-in unicode()
function, this method raises the following exception if the
given string is already unicode:
TypeError: decoding Unicode is not supported
"""
if encoding is None:
encoding = self.string_encoding
# TODO: Wrap UnicodeDecodeErrors with a message about setting
# the string_encoding and decode_errors attributes.
return unicode(s, 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 the load_partial function to pass to RenderEngine.__init__().
"""
if self.partials is None:
load_template = self._make_load_template()
return load_template
# Otherwise, create a load_partial function from the custom partial
# loader that satisfies RenderEngine requirements (and that provides
# a nicer exception, etc).
partials = self.partials
def load_partial(name):
template = partials.get(name)
if template is None:
# TODO: make a TemplateNotFoundException type that provides
# the original partials as an attribute.
raise Exception("Partial not found with name: %s" % repr(name))
# RenderEngine requires that the return value be unicode.
return self._to_unicode_hard(template)
return load_partial
def _make_render_engine(self):
"""
Return a RenderEngine instance for rendering.
"""
load_partial = self._make_load_partial()
engine = RenderEngine(load_partial=load_partial,
literal=self._to_unicode_hard,
escape=self._escape_to_unicode)
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_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)
context = Context.create(*context, **kwargs)
self._context = context
engine = self._make_render_engine()
rendered = engine.render(template, context)
return unicode(rendered)
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_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(self, template, *context, **kwargs):
"""
Render the given template (or template object) using the given context.
Returns the rendering as a unicode string.
Prior to rendering, templates of type str are converted to unicode
using the string_encoding and decode_errors attributes. See the
constructor docstring for more information.
Arguments:
template: a template string of type unicode or str, or an object
instance. If the argument is an object, 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, Context 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 isinstance(template, basestring):
return self._render_string(template, *context, **kwargs)
# Otherwise, we assume the template is an object.
return self._render_object(template, *context, **kwargs)
# 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.
"""
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)
import re
import cgi
import collections
import os
import copy
try:
import markupsafe
escape = markupsafe.escape
literal = markupsafe.Markup
except ImportError:
escape = lambda x: cgi.escape(unicode(x))
literal = unicode
class Modifiers(dict):
"""Dictionary with a decorator for assigning functions to keys."""
def set(self, key):
"""
Decorator function to set the given key to the decorated function.
>>> modifiers = {}
>>> @modifiers.set('P')
... def render_tongue(self, tag_name=None, context=None):
... return ":P %s" % tag_name
>>> modifiers
{'P': <function render_tongue at 0x...>}
"""
def setter(func):
self[key] = func
return func
return setter
class Template(object):
tag_re = None
otag = '{{'
ctag = '}}'
modifiers = Modifiers()
def __init__(self, template=None, context=None, **kwargs):
from view import View
self.template = template
if kwargs:
context.update(kwargs)
if isinstance(context, View):
self.view = context
else:
self.view = View(context=context)
self._compile_regexps()
def _compile_regexps(self):
tags = {
'otag': re.escape(self.otag),
'ctag': re.escape(self.ctag)
}
section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?\s*)%(otag)s/\1%(ctag)s"
self.section_re = re.compile(section % tags, re.M|re.S)
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
self.tag_re = re.compile(tag % tags)
def _render_sections(self, template, view):
while True:
match = self.section_re.search(template)
if match is None:
break
section, section_name, inner = match.group(0, 1, 2)
section_name = section_name.strip()
it = self.view.get(section_name, None)
replacer = ''
# Callable
if it and callable(it):
replacer = it(inner)
# Dictionary
elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
if section[2] != '^':
replacer = self._render_dictionary(inner, it)
# Lists
elif it and hasattr(it, '__iter__'):
if section[2] != '^':
replacer = self._render_list(inner, it)
# Other objects
elif it and isinstance(it, object):
if section[2] != '^':
replacer = self._render_dictionary(inner, it)
# Falsey and Negated or Truthy and Not Negated
elif (not it and section[2] == '^') or (it and section[2] != '^'):
replacer = self._render_dictionary(inner, it)
template = literal(template.replace(section, replacer))
return template
def _render_tags(self, template):
while True:
match = self.tag_re.search(template)
if match is None:
break
tag, tag_type, tag_name = match.group(0, 1, 2)
tag_name = tag_name.strip()
func = self.modifiers[tag_type]
replacement = func(self, tag_name)
template = template.replace(tag, replacement)
return template
def _render_dictionary(self, template, context):
self.view.context_list.insert(0, context)
template = Template(template, self.view)
out = template.render()
self.view.context_list.pop(0)
return out
def _render_list(self, template, listing):
insides = []
for item in listing:
insides.append(self._render_dictionary(template, item))
return ''.join(insides)
@modifiers.set(None)
def _render_tag(self, tag_name):
raw = self.view.get(tag_name, '')
# For methods with no return value
if not raw and raw is not 0:
if tag_name == '.':
raw = self.view.context_list[0]
else:
return ''
return escape(raw)
@modifiers.set('!')
def _render_comment(self, tag_name):
return ''
@modifiers.set('>')
def _render_partial(self, template_name):
from pystache import Loader
markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding)
template = Template(markup, self.view)
return template.render()
@modifiers.set('=')
def _change_delimiter(self, tag_name):
"""Changes the Mustache delimiter."""
self.otag, self.ctag = tag_name.split(' ')
self._compile_regexps()
return ''
@modifiers.set('{')
@modifiers.set('&')
def render_unescaped(self, tag_name):
"""Render a tag without escaping it."""
return literal(self.view.get(tag_name, ''))
def render(self, encoding=None):
template = self._render_sections(self.template, self.view)
result = self._render_tags(template)
if encoding is not None:
result = result.encode(encoding)
return result
# coding: utf-8
"""
This module supports customized (aka special or specified) template loading.
"""
# TODO: finish the class docstring.
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_rel_path: the path to the template file, relative to the
directory containing the module defining the class.
template_rel_directory: the directory containing the template file, relative
to the directory containing the module defining the class.
template_extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. extensionless template files).
"""
template = None
template_rel_path = None
template_rel_directory = None
template_name = None
template_extension = None
template_encoding = None
from pystache import Template
import os.path
import re
from types import *
def get_or_attr(context_list, name, default=None):
if not context_list:
return default
for obj in context_list:
try:
return obj[name]
except KeyError:
pass
except:
try:
return getattr(obj, name)
except AttributeError:
pass
return default
class View(object):
template_name = None
template_path = None
template = None
template_encoding = None
template_extension = 'mustache'
def __init__(self, template=None, context=None, **kwargs):
self.template = template
context = context or {}
context.update(**kwargs)
self.context_list = [context]
def get(self, attr, default=None):
attr = get_or_attr(self.context_list, attr, getattr(self, attr, default))
if hasattr(attr, '__call__') and type(attr) is UnboundMethodType:
return attr()
else:
return attr
def get_template(self, template_name):
if not self.template:
from pystache import Loader
template_name = self._get_template_name(template_name)
self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension)
return self.template
def _get_template_name(self, template_name=None):
"""TemplatePartial => template_partial
Takes a string but defaults to using the current class' name or
the `template_name` attribute
"""
if template_name:
return template_name
template_name = self.__class__.__name__
def repl(match):
return '_' + match.group(0).lower()
return re.sub('[A-Z]', repl, template_name)[1:]
def _get_context(self):
context = {}
for item in self.context_list:
if hasattr(item, 'keys') and hasattr(item, '__getitem__'):
context.update(item)
return context
def render(self, encoding=None):
return Template(self.get_template(self.template_name), self).render(encoding=encoding)
def __contains__(self, needle):
return needle in self.context or hasattr(self, needle)
def __getitem__(self, attr):
val = self.get(attr, None)
if not val and val is not 0:
raise KeyError("Key '%s' does not exist in View" % attr)
return val
def __getattr__(self, attr):
if attr == 'context':
return self._get_context()
raise AttributeError("Attribute '%s' does not exist in View" % attr)
def __str__(self):
return self.render()
\ No newline at end of file
[nosetests]
with-doctest=1
doctest-extension=rst
#!/usr/bin/env python #!/usr/bin/env python
# coding: utf-8
""" """
Run the following to publish to PyPI: This script supports installing and distributing pystache.
Below are instructions to pystache maintainers on how to push a new
version of pystache to PyPI--
http://pypi.python.org/pypi/pystache
Create a PyPI user account. The user account will need permissions to push
to PyPI. A current "Package Index Owner" of pystache can grant you those
permissions.
When you have permissions, run the following (after preparing the release,
bumping the version number in setup.py, etc):
> python setup.py publish
If you get an error like the following--
Upload failed (401): You must be identified to edit package information
then add a file called .pyirc to your home directory with the following
contents:
[server-login]
username: <PyPI username>
password: <PyPI password>
as described here, for example:
> python setup.py publish http://docs.python.org/release/2.5.2/dist/pypirc.html
""" """
import os import os
import sys import sys
try: try:
from setuptools import setup from setuptools import setup
except ImportError: except ImportError:
from distutils.core import setup from distutils.core import setup
def publish(): def publish():
"""Publish to Pypi""" """
os.system("python setup.py sdist upload") Publish this package to PyPI (aka "the Cheeseshop").
"""
os.system('python setup.py sdist upload')
def make_long_description():
"""
Return the long description for the package.
"""
long_description = open('README.rst').read() + '\n\n' + open('HISTORY.rst').read()
if sys.argv[-1] == "publish": return long_description
if sys.argv[-1] == 'publish':
publish() publish()
sys.exit() sys.exit()
long_description = make_long_description()
setup(name='pystache', setup(name='pystache',
version='0.4.1', version='0.5.0-rc',
description='Mustache for Python', description='Mustache for Python',
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), long_description=long_description,
author='Chris Wanstrath', author='Chris Wanstrath',
author_email='chris@ozmm.org', author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache', url='http://github.com/defunkt/pystache',
packages=['pystache'], packages=['pystache'],
license='MIT', license='MIT',
entry_points = {
'console_scripts': ['pystache=pystache.commands:main'],
},
classifiers = ( classifiers = (
"Development Status :: 4 - Beta", 'Development Status :: 4 - Beta',
"License :: OSI Approved :: MIT License", 'License :: OSI Approved :: MIT License',
"Programming Language :: Python", 'Programming Language :: Python',
"Programming Language :: Python :: 2.4", 'Programming Language :: Python :: 2.4',
"Programming Language :: Python :: 2.5", 'Programming Language :: Python :: 2.5',
"Programming Language :: Python :: 2.6", 'Programming Language :: Python :: 2.6',
"Programming Language :: Python :: 2.7", 'Programming Language :: Python :: 2.7',
) )
) )
#!/usr/bin/env python
# coding: utf-8
"""
A rudimentary backward- and forward-compatible script to benchmark pystache.
Usage:
tests/benchmark.py 10000
"""
import sys
from timeit import Timer
import pystache
# TODO: make the example realistic.
examples = [
# Test case: 1
("""{{#person}}Hi {{name}}{{/person}}""",
{"person": {"name": "Jon"}},
"Hi Jon"),
# Test case: 2
("""\
<div class="comments">
<h3>{{header}}</h3>
<ul>
{{#comments}}<li class="comment">
<h5>{{name}}</h5><p>{{body}}</p>
</li>{{/comments}}
</ul>
</div>""",
{'header': "My Post Comments",
'comments': [
{'name': "Joe", 'body': "Thanks for this post!"},
{'name': "Sam", 'body': "Thanks for this post!"},
{'name': "Heather", 'body': "Thanks for this post!"},
{'name': "Kathy", 'body': "Thanks for this post!"},
{'name': "George", 'body': "Thanks for this post!"}]},
"""\
<div class="comments">
<h3>My Post Comments</h3>
<ul>
<li class="comment">
<h5>Joe</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>Sam</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>Heather</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>Kathy</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>George</h5><p>Thanks for this post!</p>
</li>
</ul>
</div>"""),
]
def make_test_function(example):
template, context, expected = example
def test():
actual = pystache.render(template, context)
if actual != expected:
raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual))
return test
def main(sys_argv):
args = sys_argv[1:]
count = int(args[0])
print "Benchmarking: %sx" % count
print
for example in examples:
test = make_test_function(example)
t = Timer(test,)
print min(t.repeat(repeat=3, number=count))
print "Done"
if __name__ == '__main__':
main(sys.argv)
# coding: utf-8
"""
Provides test-related code that can be used by all tests.
"""
import os
import examples
DATA_DIR = 'tests/data'
EXAMPLES_DIR = os.path.dirname(examples.__file__)
def get_data_path(file_name):
return os.path.join(DATA_DIR, file_name)
class AssertStringMixin:
"""A unittest.TestCase mixin to check string equality."""
def assertString(self, actual, expected):
"""
Assert that the given strings are equal and have the same type.
"""
# Show both friendly and literal versions.
message = """String mismatch: %%s\
Expected: \"""%s\"""
Actual: \"""%s\"""
Expected: %s
Actual: %s""" % (expected, actual, repr(expected), repr(actual))
self.assertEquals(actual, expected, message % "different characters")
details = "types different: %s != %s" % (repr(type(expected)), repr(type(actual)))
self.assertEquals(type(expected), type(actual), message % details)
class AssertIsMixin:
"""A unittest.TestCase mixin adding assertIs()."""
# unittest.assertIs() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
def assertIs(self, first, second):
self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
ascii: abc
\ No newline at end of file
This file is used to test locate_path()'s search order.
\ No newline at end of file
This file is used to test locate_path()'s search order.
\ No newline at end of file
non-ascii: é
\ No newline at end of file
ascii: abc
\ No newline at end of file
Hello, {{to}}
\ No newline at end of file
# coding: utf-8
from pystache import TemplateSpec
class SayHello(object):
def to(self):
return "World"
class SampleView(TemplateSpec):
pass
class NonAscii(TemplateSpec):
pass
# coding: utf-8
"""
Unit tests of commands.py.
"""
import sys
import unittest
from pystache.commands import main
ORIGINAL_STDOUT = sys.stdout
class MockStdout(object):
def __init__(self):
self.output = ""
def write(self, str):
self.output += str
class CommandsTestCase(unittest.TestCase):
def setUp(self):
sys.stdout = MockStdout()
def callScript(self, template, context):
argv = ['pystache', template, context]
main(argv)
return sys.stdout.output
def testMainSimple(self):
"""
Test a simple command-line case.
"""
actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
self.assertEquals(actual, u"Hi world\n")
def tearDown(self):
sys.stdout = ORIGINAL_STDOUT
# coding: utf-8
"""
Unit tests of context.py.
"""
from datetime import datetime
import unittest
from pystache.context import _NOT_FOUND
from pystache.context import _get_value
from pystache.context import Context
from tests.common import AssertIsMixin
class SimpleObject(object):
"""A sample class that does not define __getitem__()."""
def __init__(self):
self.foo = "bar"
def foo_callable(self):
return "called..."
class DictLike(object):
"""A sample class that implements __getitem__() and __contains__()."""
def __init__(self):
self._dict = {'foo': 'bar'}
self.fuzz = 'buzz'
def __contains__(self, key):
return key in self._dict
def __getitem__(self, key):
return self._dict[key]
class GetValueTests(unittest.TestCase, AssertIsMixin):
"""Test context._get_value()."""
def assertNotFound(self, item, key):
"""
Assert that a call to _get_value() returns _NOT_FOUND.
"""
self.assertIs(_get_value(item, key), _NOT_FOUND)
### Case: the item is a dictionary.
def test_dictionary__key_present(self):
"""
Test getting a key from a dictionary.
"""
item = {"foo": "bar"}
self.assertEquals(_get_value(item, "foo"), "bar")
def test_dictionary__callable_not_called(self):
"""
Test that callable values are returned as-is (and in particular not called).
"""
def foo_callable(self):
return "bar"
item = {"foo": foo_callable}
self.assertNotEquals(_get_value(item, "foo"), "bar")
self.assertTrue(_get_value(item, "foo") is foo_callable)
def test_dictionary__key_missing(self):
"""
Test getting a missing key from a dictionary.
"""
item = {}
self.assertNotFound(item, "missing")
def test_dictionary__attributes_not_checked(self):
"""
Test that dictionary attributes are not checked.
"""
item = {}
attr_name = "keys"
self.assertEquals(getattr(item, attr_name)(), [])
self.assertNotFound(item, attr_name)
def test_dictionary__dict_subclass(self):
"""
Test that subclasses of dict are treated as dictionaries.
"""
class DictSubclass(dict): pass
item = DictSubclass()
item["foo"] = "bar"
self.assertEquals(_get_value(item, "foo"), "bar")
### Case: the item is an object.
def test_object__attribute_present(self):
"""
Test getting an attribute from an object.
"""
item = SimpleObject()
self.assertEquals(_get_value(item, "foo"), "bar")
def test_object__attribute_missing(self):
"""
Test getting a missing attribute from an object.
"""
item = SimpleObject()
self.assertNotFound(item, "missing")
def test_object__attribute_is_callable(self):
"""
Test getting a callable attribute from an object.
"""
item = SimpleObject()
self.assertEquals(_get_value(item, "foo_callable"), "called...")
def test_object__non_built_in_type(self):
"""
Test getting an attribute from an instance of a type that isn't built-in.
"""
item = datetime(2012, 1, 2)
self.assertEquals(_get_value(item, "day"), 2)
def test_object__dict_like(self):
"""
Test getting a key from a dict-like object (an object that implements '__getitem__').
"""
item = DictLike()
self.assertEquals(item["foo"], "bar")
self.assertNotFound(item, "foo")
### Case: the item is an instance of a built-in type.
def test_built_in_type__integer(self):
"""
Test getting from an integer.
"""
class MyInt(int): pass
item1 = MyInt(10)
item2 = 10
try:
item2.real
except AttributeError:
# Then skip this unit test. The numeric type hierarchy was
# added only in Python 2.6, in which case integers inherit
# from complex numbers the "real" attribute, etc:
#
# http://docs.python.org/library/numbers.html
#
return
self.assertEquals(item1.real, 10)
self.assertEquals(item2.real, 10)
self.assertEquals(_get_value(item1, 'real'), 10)
self.assertNotFound(item2, 'real')
def test_built_in_type__string(self):
"""
Test getting from a string.
"""
class MyStr(str): pass
item1 = MyStr('abc')
item2 = 'abc'
self.assertEquals(item1.upper(), 'ABC')
self.assertEquals(item2.upper(), 'ABC')
self.assertEquals(_get_value(item1, 'upper'), 'ABC')
self.assertNotFound(item2, 'upper')
def test_built_in_type__list(self):
"""
Test getting from a list.
"""
class MyList(list): pass
item1 = MyList([1, 2, 3])
item2 = [1, 2, 3]
self.assertEquals(item1.pop(), 3)
self.assertEquals(item2.pop(), 3)
self.assertEquals(_get_value(item1, 'pop'), 2)
self.assertNotFound(item2, 'pop')
class ContextTests(unittest.TestCase, AssertIsMixin):
"""
Test the Context class.
"""
def test_init__no_elements(self):
"""
Check that passing nothing to __init__() raises no exception.
"""
context = Context()
def test_init__many_elements(self):
"""
Check that passing more than two items to __init__() raises no exception.
"""
context = Context({}, {}, {})
def test__repr(self):
context = Context()
self.assertEquals(repr(context), 'Context()')
context = Context({'foo': 'bar'})
self.assertEquals(repr(context), "Context({'foo': 'bar'},)")
context = Context({'foo': 'bar'}, {'abc': 123})
self.assertEquals(repr(context), "Context({'foo': 'bar'}, {'abc': 123})")
def test__str(self):
context = Context()
self.assertEquals(str(context), 'Context()')
context = Context({'foo': 'bar'})
self.assertEquals(str(context), "Context({'foo': 'bar'},)")
context = Context({'foo': 'bar'}, {'abc': 123})
self.assertEquals(str(context), "Context({'foo': 'bar'}, {'abc': 123})")
## Test the static create() method.
def test_create__dictionary(self):
"""
Test passing a dictionary.
"""
context = Context.create({'foo': 'bar'})
self.assertEquals(context.get('foo'), 'bar')
def test_create__none(self):
"""
Test passing None.
"""
context = Context.create({'foo': 'bar'}, None)
self.assertEquals(context.get('foo'), 'bar')
def test_create__object(self):
"""
Test passing an object.
"""
class Foo(object):
foo = 'bar'
context = Context.create(Foo())
self.assertEquals(context.get('foo'), 'bar')
def test_create__context(self):
"""
Test passing a Context instance.
"""
obj = Context({'foo': 'bar'})
context = Context.create(obj)
self.assertEquals(context.get('foo'), 'bar')
def test_create__kwarg(self):
"""
Test passing a keyword argument.
"""
context = Context.create(foo='bar')
self.assertEquals(context.get('foo'), 'bar')
def test_create__precedence_positional(self):
"""
Test precedence of positional arguments.
"""
context = Context.create({'foo': 'bar'}, {'foo': 'buzz'})
self.assertEquals(context.get('foo'), 'buzz')
def test_create__precedence_keyword(self):
"""
Test precedence of keyword arguments.
"""
context = Context.create({'foo': 'bar'}, foo='buzz')
self.assertEquals(context.get('foo'), 'buzz')
def test_get__key_present(self):
"""
Test getting a key.
"""
context = Context({"foo": "bar"})
self.assertEquals(context.get("foo"), "bar")
def test_get__key_missing(self):
"""
Test getting a missing key.
"""
context = Context()
self.assertTrue(context.get("foo") is None)
def test_get__default(self):
"""
Test that get() respects the default value.
"""
context = Context()
self.assertEquals(context.get("foo", "bar"), "bar")
def test_get__precedence(self):
"""
Test that get() respects the order of precedence (later items first).
"""
context = Context({"foo": "bar"}, {"foo": "buzz"})
self.assertEquals(context.get("foo"), "buzz")
def test_get__fallback(self):
"""
Check that first-added stack items are queried on context misses.
"""
context = Context({"fuzz": "buzz"}, {"foo": "bar"})
self.assertEquals(context.get("fuzz"), "buzz")
def test_push(self):
"""
Test push().
"""
key = "foo"
context = Context({key: "bar"})
self.assertEquals(context.get(key), "bar")
context.push({key: "buzz"})
self.assertEquals(context.get(key), "buzz")
def test_pop(self):
"""
Test pop().
"""
key = "foo"
context = Context({key: "bar"}, {key: "buzz"})
self.assertEquals(context.get(key), "buzz")
item = context.pop()
self.assertEquals(item, {"foo": "buzz"})
self.assertEquals(context.get(key), "bar")
def test_top(self):
key = "foo"
context = Context({key: "bar"}, {key: "buzz"})
self.assertEquals(context.get(key), "buzz")
top = context.top()
self.assertEquals(top, {"foo": "buzz"})
# Make sure calling top() didn't remove the item from the stack.
self.assertEquals(context.get(key), "buzz")
def test_copy(self):
key = "foo"
original = Context({key: "bar"}, {key: "buzz"})
self.assertEquals(original.get(key), "buzz")
new = original.copy()
# Confirm that the copy behaves the same.
self.assertEquals(new.get(key), "buzz")
# Change the copy, and confirm it is changed.
new.pop()
self.assertEquals(new.get(key), "bar")
# Confirm the original is unchanged.
self.assertEquals(original.get(key), "buzz")
# encoding: utf-8 # encoding: utf-8
import unittest import unittest
import pystache
from examples.comments import Comments from examples.comments import Comments
from examples.double_section import DoubleSection from examples.double_section import DoubleSection
...@@ -12,72 +11,91 @@ from examples.delimiters import Delimiters ...@@ -12,72 +11,91 @@ from examples.delimiters import Delimiters
from examples.unicode_output import UnicodeOutput from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput from examples.unicode_input import UnicodeInput
from examples.nested_context import NestedContext from examples.nested_context import NestedContext
from pystache import Renderer
from tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin
class TestView(unittest.TestCase, AssertStringMixin):
def _assert(self, obj, expected):
renderer = Renderer()
actual = renderer.render(obj)
self.assertString(actual, expected)
class TestView(unittest.TestCase):
def test_comments(self): def test_comments(self):
self.assertEquals(Comments().render(), """<h1>A Comedy of Errors</h1> self._assert(Comments(), u"<h1>A Comedy of Errors</h1>")
""")
def test_double_section(self): def test_double_section(self):
self.assertEquals(DoubleSection().render(),"""* first\n* second\n* third""") self._assert(DoubleSection(), u"* first\n* second\n* third")
def test_unicode_output(self): def test_unicode_output(self):
self.assertEquals(UnicodeOutput().render(), u'<p>Name: Henri Poincaré</p>') renderer = Renderer()
actual = renderer.render(UnicodeOutput())
def test_encoded_output(self): self.assertString(actual, u'<p>Name: Henri Poincaré</p>')
self.assertEquals(UnicodeOutput().render('utf8'), '<p>Name: Henri Poincar\xc3\xa9</p>')
def test_unicode_input(self): def test_unicode_input(self):
self.assertEquals(UnicodeInput().render(), renderer = Renderer()
u'<p>If alive today, Henri Poincaré would be 156 years old.</p>') actual = renderer.render(UnicodeInput())
self.assertString(actual, u'abcdé')
def test_escaped(self): def test_escaping(self):
self.assertEquals(Escaped().render(), "<h1>Bear &gt; Shark</h1>") self._assert(Escaped(), u"<h1>Bear &gt; Shark</h1>")
def test_unescaped(self): def test_literal(self):
self.assertEquals(Unescaped().render(), "<h1>Bear > Shark</h1>") renderer = Renderer()
actual = renderer.render(Unescaped())
def test_unescaped_sigil(self): self.assertString(actual, u"<h1>Bear > Shark</h1>")
view = Escaped(template="<h1>{{& thing}}</h1>", context={
'thing': 'Bear > Giraffe'
})
self.assertEquals(view.render(), "<h1>Bear > Giraffe</h1>")
def test_template_partial(self): def test_template_partial(self):
self.assertEquals(TemplatePartial().render(), """<h1>Welcome</h1> renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(TemplatePartial(renderer=renderer))
self.assertString(actual, u"""<h1>Welcome</h1>
Again, Welcome!""") Again, Welcome!""")
def test_template_partial_extension(self): def test_template_partial_extension(self):
view = TemplatePartial() renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
view.template_extension = 'txt'
self.assertEquals(view.render(), """Welcome
-------
Again, Welcome! view = TemplatePartial(renderer=renderer)
""")
actual = renderer.render(view)
self.assertString(actual, u"""Welcome
-------
## Again, Welcome! ##""")
def test_delimiters(self): def test_delimiters(self):
self.assertEquals(Delimiters().render(), """ renderer = Renderer()
actual = renderer.render(Delimiters())
self.assertString(actual, u"""\
* It worked the first time. * It worked the first time.
* And it worked the second time. * And it worked the second time.
* Then, surprisingly, it worked the third time. * Then, surprisingly, it worked the third time.
""") """)
def test_nested_context(self): def test_nested_context(self):
self.assertEquals(NestedContext().render(), "one and foo and two") renderer = Renderer()
actual = renderer.render(NestedContext(renderer))
self.assertString(actual, u"one and foo and two")
def test_nested_context_is_available_in_view(self): def test_nested_context_is_available_in_view(self):
view = NestedContext() renderer = Renderer()
view = NestedContext(renderer)
view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}' view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}'
self.assertEquals(view.render(), 'it works!')
actual = renderer.render(view)
self.assertString(actual, u'it works!')
def test_partial_in_partial_has_access_to_grand_parent_context(self): def test_partial_in_partial_has_access_to_grand_parent_context(self):
view = TemplatePartial(context = {'prop': 'derp'}) renderer = Renderer(search_dirs=EXAMPLES_DIR)
view = TemplatePartial(renderer=renderer)
view.template = '''{{>partial_in_partial}}''' view.template = '''{{>partial_in_partial}}'''
self.assertEquals(view.render(), 'Hi derp!')
actual = renderer.render(view, {'prop': 'derp'})
self.assertEquals(actual, 'Hi derp!')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
# encoding: utf-8
"""
Unit tests of reader.py.
"""
import os
import sys
import unittest import unittest
import pystache
class TestLoader(unittest.TestCase): from tests.common import AssertStringMixin
from pystache import defaults
from pystache.loader import Loader
DATA_DIR = 'tests/data'
class LoaderTests(unittest.TestCase, AssertStringMixin):
def test_init__extension(self):
loader = Loader(extension='foo')
self.assertEquals(loader.extension, 'foo')
def test_init__extension__default(self):
# Test the default value.
loader = Loader()
self.assertEquals(loader.extension, 'mustache')
def test_init__file_encoding(self):
loader = Loader(file_encoding='bar')
self.assertEquals(loader.file_encoding, 'bar')
def test_init__file_encoding__default(self):
file_encoding = defaults.FILE_ENCODING
try:
defaults.FILE_ENCODING = 'foo'
loader = Loader()
self.assertEquals(loader.file_encoding, 'foo')
finally:
defaults.FILE_ENCODING = file_encoding
def test_init__to_unicode(self):
to_unicode = lambda x: x
loader = Loader(to_unicode=to_unicode)
self.assertEquals(loader.to_unicode, to_unicode)
def test_init__to_unicode__default(self):
loader = Loader()
self.assertRaises(TypeError, loader.to_unicode, u"abc")
decode_errors = defaults.DECODE_ERRORS
string_encoding = defaults.STRING_ENCODING
nonascii = 'abcdé'
try:
defaults.DECODE_ERRORS = 'strict'
defaults.STRING_ENCODING = 'ascii'
loader = Loader()
self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
defaults.DECODE_ERRORS = 'ignore'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcd')
defaults.STRING_ENCODING = 'utf-8'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcdé')
finally:
defaults.DECODE_ERRORS = decode_errors
defaults.STRING_ENCODING = string_encoding
def _get_path(self, filename):
return os.path.join(DATA_DIR, filename)
def test_unicode__basic__input_str(self):
"""
Test unicode(): default arguments with str input.
"""
reader = Loader()
actual = reader.unicode("foo")
self.assertString(actual, u"foo")
def test_unicode__basic__input_unicode(self):
"""
Test unicode(): default arguments with unicode input.
"""
reader = Loader()
actual = reader.unicode(u"foo")
self.assertString(actual, u"foo")
def test_unicode__basic__input_unicode_subclass(self):
"""
Test unicode(): default arguments with unicode-subclass input.
"""
class UnicodeSubclass(unicode):
pass
s = UnicodeSubclass(u"foo")
reader = Loader()
actual = reader.unicode(s)
self.assertString(actual, u"foo")
def test_unicode__to_unicode__attribute(self):
"""
Test unicode(): encoding attribute.
"""
reader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
def to_unicode(s, encoding=None):
if encoding is None:
encoding = 'utf-8'
return unicode(s, encoding)
reader.to_unicode = to_unicode
self.assertString(reader.unicode(non_ascii), u"abcdé")
def test_unicode__encoding_argument(self):
"""
Test unicode(): encoding argument.
"""
reader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
actual = reader.unicode(non_ascii, encoding='utf-8')
self.assertString(actual, u'abcdé')
# TODO: check the read() unit tests.
def test_read(self):
"""
Test read().
"""
reader = Loader()
path = self._get_path('ascii.mustache')
actual = reader.read(path)
self.assertString(actual, u'ascii: abc')
def test_read__file_encoding__attribute(self):
"""
Test read(): file_encoding attribute respected.
"""
loader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, loader.read, path)
loader.file_encoding = 'utf-8'
actual = loader.read(path)
self.assertString(actual, u'non-ascii: é')
def test_read__encoding__argument(self):
"""
Test read(): encoding argument respected.
"""
reader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path)
def test_template_is_loaded(self): actual = reader.read(path, encoding='utf-8')
loader = pystache.Loader() self.assertString(actual, u'non-ascii: é')
template = loader.load_template('simple', 'examples')
self.assertEqual(template, 'Hi {{thing}}!{{blank}}') def test_reader__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
def test_using_list_of_paths(self): """
loader = pystache.Loader() reader = Loader()
template = loader.load_template('simple', ['doesnt_exist', 'examples']) path = self._get_path('non_ascii.mustache')
self.assertEqual(template, 'Hi {{thing}}!{{blank}}') self.assertRaises(UnicodeDecodeError, reader.read, path)
def test_non_existent_template_fails(self): #reader.decode_errors = 'ignore'
loader = pystache.Loader() #actual = reader.read(path)
#self.assertString(actual, u'non-ascii: ')
self.assertRaises(IOError, loader.load_template, 'simple', 'doesnt_exist')
\ No newline at end of file
# encoding: utf-8
"""
Contains locator.py unit tests.
"""
from datetime import datetime
import os
import sys
import unittest
# TODO: remove this alias.
from pystache.loader import Loader as Reader
from pystache.locator import Locator
from tests.common import DATA_DIR
from data.views import SayHello
class LocatorTests(unittest.TestCase):
def _locator(self):
return Locator(search_dirs=DATA_DIR)
def test_init__extension(self):
# Test the default value.
locator = Locator()
self.assertEquals(locator.template_extension, 'mustache')
locator = Locator(extension='txt')
self.assertEquals(locator.template_extension, 'txt')
locator = Locator(extension=False)
self.assertTrue(locator.template_extension is False)
def test_get_object_directory(self):
locator = Locator()
obj = SayHello()
actual = locator.get_object_directory(obj)
self.assertEquals(actual, os.path.abspath(DATA_DIR))
def test_get_object_directory__not_hasattr_module(self):
locator = Locator()
obj = datetime(2000, 1, 1)
self.assertFalse(hasattr(obj, '__module__'))
self.assertEquals(locator.get_object_directory(obj), None)
self.assertFalse(hasattr(None, '__module__'))
self.assertEquals(locator.get_object_directory(None), None)
def test_make_file_name(self):
locator = Locator()
locator.template_extension = 'bar'
self.assertEquals(locator.make_file_name('foo'), 'foo.bar')
locator.template_extension = False
self.assertEquals(locator.make_file_name('foo'), 'foo')
locator.template_extension = ''
self.assertEquals(locator.make_file_name('foo'), 'foo.')
def test_make_file_name__template_extension_argument(self):
locator = Locator()
self.assertEquals(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
def test_find_name(self):
locator = Locator()
path = locator.find_name(search_dirs=['examples'], template_name='simple')
self.assertEquals(os.path.basename(path), 'simple.mustache')
def test_find_name__using_list_of_paths(self):
locator = Locator()
path = locator.find_name(search_dirs=['doesnt_exist', 'examples'], template_name='simple')
self.assertTrue(path)
def test_find_name__precedence(self):
"""
Test the order in which find_name() searches directories.
"""
locator = Locator()
dir1 = DATA_DIR
dir2 = os.path.join(DATA_DIR, 'locator')
self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate'))
self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate'))
path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate')
dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1]
self.assertEquals(dirname, 'locator')
def test_find_name__non_existent_template_fails(self):
locator = Locator()
self.assertRaises(IOError, locator.find_name, search_dirs=[], template_name='doesnt_exist')
def test_find_object(self):
locator = Locator()
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache')
expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
self.assertEquals(actual, expected)
def test_find_object__none_file_name(self):
locator = Locator()
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj)
expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache'))
self.assertEquals(actual, expected)
def test_find_object__none_object_directory(self):
locator = Locator()
obj = None
self.assertEquals(None, locator.get_object_directory(obj))
actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache')
expected = os.path.join(DATA_DIR, 'say_hello.mustache')
self.assertEquals(actual, expected)
def test_make_template_name(self):
"""
Test make_template_name().
"""
locator = Locator()
class FooBar(object):
pass
foo = FooBar()
self.assertEquals(locator.make_template_name(foo), 'foo_bar')
...@@ -2,8 +2,15 @@ ...@@ -2,8 +2,15 @@
import unittest import unittest
import pystache import pystache
from pystache import renderer
class PystacheTests(unittest.TestCase):
def _assert_rendered(self, expected, template, context):
actual = pystache.render(template, context)
self.assertEquals(actual, expected)
class TestPystache(unittest.TestCase):
def test_basic(self): def test_basic(self):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' }) ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
self.assertEquals(ret, "Hi world!") self.assertEquals(ret, "Hi world!")
...@@ -14,66 +21,96 @@ class TestPystache(unittest.TestCase): ...@@ -14,66 +21,96 @@ class TestPystache(unittest.TestCase):
def test_less_basic(self): def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?" template = "It's a nice day for {{beverage}}, right {{person}}?"
ret = pystache.render(template, { 'beverage': 'soda', 'person': 'Bob' }) context = { 'beverage': 'soda', 'person': 'Bob' }
self.assertEquals(ret, "It's a nice day for soda, right Bob?") self._assert_rendered("It's a nice day for soda, right Bob?", template, context)
def test_even_less_basic(self): def test_even_less_basic(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?" template = "I think {{name}} wants a {{thing}}, right {{name}}?"
ret = pystache.render(template, { 'name': 'Jon', 'thing': 'racecar' }) context = { 'name': 'Jon', 'thing': 'racecar' }
self.assertEquals(ret, "I think Jon wants a racecar, right Jon?") self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context)
def test_ignores_misses(self): def test_ignores_misses(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?" template = "I think {{name}} wants a {{thing}}, right {{name}}?"
ret = pystache.render(template, { 'name': 'Jon' }) context = { 'name': 'Jon' }
self.assertEquals(ret, "I think Jon wants a , right Jon?") self._assert_rendered("I think Jon wants a , right Jon?", template, context)
def test_render_zero(self): def test_render_zero(self):
template = 'My value is {{value}}.' template = 'My value is {{value}}.'
ret = pystache.render(template, { 'value': 0 }) context = { 'value': 0 }
self.assertEquals(ret, 'My value is 0.') self._assert_rendered('My value is 0.', template, context)
def test_comments(self): def test_comments(self):
template = "What {{! the }} what?" template = "What {{! the }} what?"
ret = pystache.render(template) actual = pystache.render(template)
self.assertEquals(ret, "What what?") self.assertEquals("What what?", actual)
def test_false_sections_are_hidden(self): def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!" template = "Ready {{#set}}set {{/set}}go!"
ret = pystache.render(template, { 'set': False }) context = { 'set': False }
self.assertEquals(ret, "Ready go!") self._assert_rendered("Ready go!", template, context)
def test_true_sections_are_shown(self): def test_true_sections_are_shown(self):
template = "Ready {{#set}}set{{/set}} go!" template = "Ready {{#set}}set{{/set}} go!"
ret = pystache.render(template, { 'set': True }) context = { 'set': True }
self.assertEquals(ret, "Ready set go!") self._assert_rendered("Ready set go!", template, context)
non_strings_expected = """(123 & ['something'])(chris & 0.9)"""
def test_non_strings(self): def test_non_strings(self):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}" template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
stats = [] stats = []
stats.append({'key': 123, 'value': ['something']}) stats.append({'key': 123, 'value': ['something']})
stats.append({'key': u"chris", 'value': 0.900}) stats.append({'key': u"chris", 'value': 0.900})
context = { 'stats': stats }
ret = pystache.render(template, { 'stats': stats }) self._assert_rendered(self.non_strings_expected, template, context)
self.assertEquals(ret, """(123 & ['something'])(chris & 0.9)""")
def test_unicode(self): def test_unicode(self):
template = 'Name: {{name}}; Age: {{age}}' template = 'Name: {{name}}; Age: {{age}}'
ret = pystache.render(template, { 'name': u'Henri Poincaré', context = {'name': u'Henri Poincaré', 'age': 156 }
'age': 156 }) self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context)
self.assertEquals(ret, u'Name: Henri Poincaré; Age: 156')
def test_sections(self): def test_sections(self):
template = """<ul>{{#users}}<li>{{name}}</li>{{/users}}</ul>""" template = """<ul>{{#users}}<li>{{name}}</li>{{/users}}</ul>"""
context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] } context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] }
ret = pystache.render(template, context) expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
self.assertEquals(ret, """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>""") self._assert_rendered(expected, template, context)
def test_implicit_iterator(self): def test_implicit_iterator(self):
template = """<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>""" template = """<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"""
context = { 'users': [ 'Chris', 'Tom','PJ' ] } context = { 'users': [ 'Chris', 'Tom','PJ' ] }
ret = pystache.render(template, context) expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
self.assertEquals(ret, """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>""") self._assert_rendered(expected, template, context)
# The spec says that sections should not alter surrounding whitespace.
def test_surrounding_whitepace_not_altered(self):
template = "first{{#spacing}} second {{/spacing}}third"
context = {"spacing": True}
self._assert_rendered("first second third", template, context)
def test__section__non_false_value(self):
"""
Test when a section value is a (non-list) "non-false value".
From mustache(5):
When the value [of a section key] is non-false but not a list, it
will be used as the context for a single rendering of the block.
"""
template = """{{#person}}Hi {{name}}{{/person}}"""
context = {"person": {"name": "Jon"}}
self._assert_rendered("Hi Jon", template, context)
def test_later_list_section_with_escapable_character(self):
"""
This is a simple test case intended to cover issue #53.
The test case failed with markupsafe enabled, as follows:
AssertionError: Markup(u'foo &lt;') != 'foo <'
if __name__ == '__main__': """
unittest.main() template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}"""
context = {'s1': True, 's2': [True]}
self._assert_rendered("foo <", template, context)
# coding: utf-8
"""
Unit tests of renderengine.py.
"""
import cgi
import unittest
from pystache.context import Context
from pystache.parser import ParsingError
from pystache.renderengine import RenderEngine
from tests.common import AssertStringMixin
class RenderEngineTestCase(unittest.TestCase):
"""Test the RenderEngine class."""
def test_init(self):
"""
Test that __init__() stores all of the arguments correctly.
"""
# In real-life, these arguments would be functions
engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
self.assertEquals(engine.escape, "escape")
self.assertEquals(engine.literal, "literal")
self.assertEquals(engine.load_partial, "foo")
class RenderTests(unittest.TestCase, AssertStringMixin):
"""
Tests RenderEngine.render().
Explicit spec-test-like tests best go in this class since the
RenderEngine class contains all parsing logic. This way, the unit tests
will be more focused and fail "closer to the code".
"""
def _engine(self):
"""
Create and return a default RenderEngine for testing.
"""
escape = lambda s: unicode(cgi.escape(s))
engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
return engine
def _assert_render(self, expected, template, *context, **kwargs):
"""
Test rendering the given template using the given context.
"""
partials = kwargs.get('partials')
engine = kwargs.get('engine', self._engine())
if partials is not None:
engine.load_partial = lambda key: unicode(partials[key])
context = Context(*context)
actual = engine.render(template, context)
self.assertString(actual=actual, expected=expected)
def test_render(self):
self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'})
def test__load_partial(self):
"""
Test that render() uses the load_template attribute.
"""
engine = self._engine()
partials = {'partial': u"{{person}}"}
engine.load_partial = lambda key: partials[key]
self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
def test__literal(self):
"""
Test that render() uses the literal attribute.
"""
engine = self._engine()
engine.literal = lambda s: s.upper()
self._assert_render(u'BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine)
def test_literal__sigil(self):
template = "<h1>{{& thing}}</h1>"
context = {'thing': 'Bear > Giraffe'}
expected = u"<h1>Bear > Giraffe</h1>"
self._assert_render(expected, template, context)
def test__escape(self):
"""
Test that render() uses the escape attribute.
"""
engine = self._engine()
engine.escape = lambda s: "**" + s
self._assert_render(u'**bar', '{{foo}}', {'foo': 'bar'}, engine=engine)
def test__escape_does_not_call_literal(self):
"""
Test that render() does not call literal before or after calling escape.
"""
engine = self._engine()
engine.literal = lambda s: s.upper() # a test version
engine.escape = lambda s: "**" + s
template = 'literal: {{{foo}}} escaped: {{foo}}'
context = {'foo': 'bar'}
self._assert_render(u'literal: BAR escaped: **bar', template, context, engine=engine)
def test__escape_preserves_unicode_subclasses(self):
"""
Test that render() preserves unicode subclasses when passing to escape.
This is useful, for example, if one wants to respect whether a
variable value is markupsafe.Markup when escaping.
"""
class MyUnicode(unicode):
pass
def escape(s):
if type(s) is MyUnicode:
return "**" + s
else:
return s + "**"
engine = self._engine()
engine.escape = escape
template = '{{foo1}} {{foo2}}'
context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'}
self._assert_render(u'**bar bar**', template, context, engine=engine)
def test__non_basestring__literal_and_escaped(self):
"""
Test a context value that is not a basestring instance.
"""
# We use include upper() to make sure we are actually using
# our custom function in the tests
to_unicode = lambda s: unicode(s, encoding='ascii').upper()
engine = self._engine()
engine.escape = to_unicode
engine.literal = to_unicode
self.assertRaises(TypeError, engine.literal, 100)
template = '{{text}} {{int}} {{{int}}}'
context = {'int': 100, 'text': 'foo'}
self._assert_render(u'FOO 100 100', template, context, engine=engine)
def test_tag__output_not_interpolated(self):
"""
Context values should not be treated as templates (issue #44).
"""
template = '{{template}}: {{planet}}'
context = {'template': '{{planet}}', 'planet': 'Earth'}
self._assert_render(u'{{planet}}: Earth', template, context)
def test_tag__output_not_interpolated__section(self):
"""
Context values should not be treated as templates (issue #44).
"""
template = '{{test}}'
context = {'test': '{{#hello}}'}
self._assert_render(u'{{#hello}}', template, context)
def test_interpolation__built_in_type__string(self):
"""
Check tag interpolation with a string on the top of the context stack.
"""
item = 'abc'
# item.upper() == 'ABC'
template = '{{#section}}{{upper}}{{/section}}'
context = {'section': item, 'upper': 'XYZ'}
self._assert_render(u'XYZ', template, context)
def test_interpolation__built_in_type__integer(self):
"""
Check tag interpolation with an integer on the top of the context stack.
"""
item = 10
# item.real == 10
template = '{{#section}}{{real}}{{/section}}'
context = {'section': item, 'real': 1000}
self._assert_render(u'1000', template, context)
def test_interpolation__built_in_type__list(self):
"""
Check tag interpolation with a list on the top of the context stack.
"""
item = [[1, 2, 3]]
# item[0].pop() == 3
template = '{{#section}}{{pop}}{{/section}}'
context = {'section': item, 'pop': 7}
self._assert_render(u'7', template, context)
def test_implicit_iterator__literal(self):
"""
Test an implicit iterator in a literal tag.
"""
template = """{{#test}}{{{.}}}{{/test}}"""
context = {'test': ['<', '>']}
self._assert_render(u'<>', template, context)
def test_implicit_iterator__escaped(self):
"""
Test an implicit iterator in a normal tag.
"""
template = """{{#test}}{{.}}{{/test}}"""
context = {'test': ['<', '>']}
self._assert_render(u'&lt;&gt;', template, context)
def test_literal__in_section(self):
"""
Check that literals work in sections.
"""
template = '{{#test}}1 {{{less_than}}} 2{{/test}}'
context = {'test': {'less_than': '<'}}
self._assert_render(u'1 < 2', template, context)
def test_literal__in_partial(self):
"""
Check that literals work in partials.
"""
template = '{{>partial}}'
partials = {'partial': '1 {{{less_than}}} 2'}
context = {'less_than': '<'}
self._assert_render(u'1 < 2', template, context, partials=partials)
def test_partial(self):
partials = {'partial': "{{person}}"}
self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials)
def test_partial__context_values(self):
"""
Test that escape and literal work on context values in partials.
"""
engine = self._engine()
template = '{{>partial}}'
partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'}
context = {'foo': '<'}
self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
## Test cases related specifically to sections.
def test_section__end_tag_with_no_start_tag(self):
"""
Check what happens if there is an end tag with no start tag.
"""
template = '{{/section}}'
try:
self._assert_render(None, template)
except ParsingError, err:
self.assertEquals(str(err), "Section end tag mismatch: u'section' != None")
def test_section__end_tag_mismatch(self):
"""
Check what happens if the end tag doesn't match.
"""
template = '{{#section_start}}{{/section_end}}'
try:
self._assert_render(None, template)
except ParsingError, err:
self.assertEquals(str(err), "Section end tag mismatch: u'section_end' != u'section_start'")
def test_section__context_values(self):
"""
Test that escape and literal work on context values in sections.
"""
engine = self._engine()
template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}'
context = {'test': {'foo': '<'}}
self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine)
def test_section__context_precedence(self):
"""
Check that items higher in the context stack take precedence.
"""
template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}'
context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}}
self._assert_render(u'chicken : beans and rice', template, context)
def test_section__list_referencing_outer_context(self):
"""
Check that list items can access the parent context.
For sections whose value is a list, check that items in the list
have access to the values inherited from the parent context
when rendering.
"""
context = {
"greeting": "Hi",
"list": [{"name": "Al"}, {"name": "Bob"}],
}
template = "{{#list}}{{greeting}} {{name}}, {{/list}}"
self._assert_render(u"Hi Al, Hi Bob, ", template, context)
def test_section__output_not_interpolated(self):
"""
Check that rendered section output is not interpolated.
"""
template = '{{#section}}{{template}}{{/section}}: {{planet}}'
context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
self._assert_render(u'{{planet}}: Earth', template, context)
def test_section__nested_truthy(self):
"""
Check that "nested truthy" sections get rendered.
Test case for issue #24: https://github.com/defunkt/pystache/issues/24
This test is copied from the spec. We explicitly include it to
prevent regressions for those who don't pull down the spec tests.
"""
template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |'
context = {'bool': True}
self._assert_render(u'| A B C D E |', template, context)
def test_section__nested_with_same_keys(self):
"""
Check a doubly-nested section with the same context key.
Test case for issue #36: https://github.com/defunkt/pystache/issues/36
"""
# Start with an easier, working case.
template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}'
context = {'x': {'z': {'y': 1}}}
self._assert_render(u'1', template, context)
template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}'
context = {'x': {'x': {'y': 1}}}
self._assert_render(u'1', template, context)
def test_section__lambda(self):
template = '{{#test}}Mom{{/test}}'
context = {'test': (lambda text: 'Hi %s' % text)}
self._assert_render(u'Hi Mom', template, context)
def test_section__iterable(self):
"""
Check that objects supporting iteration (aside from dicts) behave like lists.
"""
template = '{{#iterable}}{{.}}{{/iterable}}'
context = {'iterable': (i for i in range(3))} # type 'generator'
self._assert_render(u'012', template, context)
context = {'iterable': xrange(4)} # type 'xrange'
self._assert_render(u'0123', template, context)
d = {'foo': 0, 'bar': 0}
# We don't know what order of keys we'll be given, but from the
# Python documentation:
# "If items(), keys(), values(), iteritems(), iterkeys(), and
# itervalues() are called with no intervening modifications to
# the dictionary, the lists will directly correspond."
expected = u''.join(d.keys())
context = {'iterable': d.iterkeys()} # type 'dictionary-keyiterator'
self._assert_render(expected, template, context)
def test_section__lambda__tag_in_output(self):
"""
Check that callable output is treated as a template string (issue #46).
The spec says--
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.
"""
template = '{{#test}}Hi {{person}}{{/test}}'
context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
self._assert_render(u'Hi Mom :)', template, context)
def test_comment__multiline(self):
"""
Check that multiline comments are permitted.
"""
self._assert_render(u'foobar', 'foo{{! baz }}bar')
self._assert_render(u'foobar', 'foo{{! \nbaz }}bar')
def test_custom_delimiters__sections(self):
"""
Check that custom delimiters can be used to start a section.
Test case for issue #20: https://github.com/defunkt/pystache/issues/20
"""
template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]'
context = {'foo': True}
self._assert_render(u'bar', template, context)
def test_custom_delimiters__not_retroactive(self):
"""
Check that changing custom delimiters back is not "retroactive."
Test case for issue #35: https://github.com/defunkt/pystache/issues/35
"""
expected = u' {{foo}} '
self._assert_render(expected, '{{=$ $=}} {{foo}} ')
self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '.
# coding: utf-8
"""
Unit tests of template.py.
"""
import codecs
import os
import sys
import unittest
from examples.simple import Simple
from pystache import Renderer
from pystache import TemplateSpec
from pystache.loader import Loader
from tests.common import get_data_path
from tests.common import AssertStringMixin
from tests.data.views import SayHello
class RendererInitTestCase(unittest.TestCase):
"""
Tests the Renderer.__init__() method.
"""
def test_partials__default(self):
"""
Test the default value.
"""
renderer = Renderer()
self.assertTrue(renderer.partials is None)
def test_partials(self):
"""
Test that the attribute is set correctly.
"""
renderer = Renderer(partials={'foo': 'bar'})
self.assertEquals(renderer.partials, {'foo': 'bar'})
def test_escape__default(self):
escape = Renderer().escape
self.assertEquals(escape(">"), "&gt;")
self.assertEquals(escape('"'), "&quot;")
# Single quotes are not escaped.
self.assertEquals(escape("'"), "'")
def test_escape(self):
escape = lambda s: "**" + s
renderer = Renderer(escape=escape)
self.assertEquals(renderer.escape("bar"), "**bar")
def test_decode_errors__default(self):
"""
Check the default value.
"""
renderer = Renderer()
self.assertEquals(renderer.decode_errors, 'strict')
def test_decode_errors(self):
"""
Check that the constructor sets the attribute correctly.
"""
renderer = Renderer(decode_errors="foo")
self.assertEquals(renderer.decode_errors, "foo")
def test_file_encoding__default(self):
"""
Check the file_encoding default.
"""
renderer = Renderer()
self.assertEquals(renderer.file_encoding, renderer.string_encoding)
def test_file_encoding(self):
"""
Check that the file_encoding attribute is set correctly.
"""
renderer = Renderer(file_encoding='foo')
self.assertEquals(renderer.file_encoding, 'foo')
def test_file_extension__default(self):
"""
Check the file_extension default.
"""
renderer = Renderer()
self.assertEquals(renderer.file_extension, 'mustache')
def test_file_extension(self):
"""
Check that the file_encoding attribute is set correctly.
"""
renderer = Renderer(file_extension='foo')
self.assertEquals(renderer.file_extension, 'foo')
def test_search_dirs__default(self):
"""
Check the search_dirs default.
"""
renderer = Renderer()
self.assertEquals(renderer.search_dirs, [os.curdir])
def test_search_dirs__string(self):
"""
Check that the search_dirs attribute is set correctly when a string.
"""
renderer = Renderer(search_dirs='foo')
self.assertEquals(renderer.search_dirs, ['foo'])
def test_search_dirs__list(self):
"""
Check that the search_dirs attribute is set correctly when a list.
"""
renderer = Renderer(search_dirs=['foo'])
self.assertEquals(renderer.search_dirs, ['foo'])
def test_string_encoding__default(self):
"""
Check the default value.
"""
renderer = Renderer()
self.assertEquals(renderer.string_encoding, sys.getdefaultencoding())
def test_string_encoding(self):
"""
Check that the constructor sets the attribute correctly.
"""
renderer = Renderer(string_encoding="foo")
self.assertEquals(renderer.string_encoding, "foo")
class RendererTests(unittest.TestCase, AssertStringMixin):
"""Test the Renderer class."""
def _renderer(self):
return Renderer()
## Test Renderer.unicode().
def test_unicode__string_encoding(self):
"""
Test that the string_encoding attribute is respected.
"""
renderer = Renderer()
s = "é"
renderer.string_encoding = "ascii"
self.assertRaises(UnicodeDecodeError, renderer.unicode, s)
renderer.string_encoding = "utf-8"
self.assertEquals(renderer.unicode(s), u"é")
def test_unicode__decode_errors(self):
"""
Test that the decode_errors attribute is respected.
"""
renderer = Renderer()
renderer.string_encoding = "ascii"
s = "déf"
renderer.decode_errors = "ignore"
self.assertEquals(renderer.unicode(s), "df")
renderer.decode_errors = "replace"
# U+FFFD is the official Unicode replacement character.
self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf')
## Test the _make_loader() method.
def test__make_loader__return_type(self):
"""
Test that _make_loader() returns a Loader.
"""
renderer = Renderer()
loader = renderer._make_loader()
self.assertEquals(type(loader), Loader)
def test__make_loader__attributes(self):
"""
Test that _make_loader() sets all attributes correctly..
"""
unicode_ = lambda x: x
renderer = Renderer()
renderer.file_encoding = 'enc'
renderer.file_extension = 'ext'
renderer.unicode = unicode_
loader = renderer._make_loader()
self.assertEquals(loader.extension, 'ext')
self.assertEquals(loader.file_encoding, 'enc')
self.assertEquals(loader.to_unicode, unicode_)
## Test the render() method.
def test_render__return_type(self):
"""
Check that render() returns a string of type unicode.
"""
renderer = Renderer()
rendered = renderer.render('foo')
self.assertEquals(type(rendered), unicode)
def test_render__unicode(self):
renderer = Renderer()
actual = renderer.render(u'foo')
self.assertEquals(actual, u'foo')
def test_render__str(self):
renderer = Renderer()
actual = renderer.render('foo')
self.assertEquals(actual, 'foo')
def test_render__non_ascii_character(self):
renderer = Renderer()
actual = renderer.render(u'Poincaré')
self.assertEquals(actual, u'Poincaré')
def test_render__context(self):
"""
Test render(): passing a context.
"""
renderer = Renderer()
self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom')
def test_render__context_and_kwargs(self):
"""
Test render(): passing a context and **kwargs.
"""
renderer = Renderer()
template = 'Hi {{person1}} and {{person2}}'
self.assertEquals(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad')
def test_render__kwargs_and_no_context(self):
"""
Test render(): passing **kwargs and no context.
"""
renderer = Renderer()
self.assertEquals(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom')
def test_render__context_and_kwargs__precedence(self):
"""
Test render(): **kwargs takes precedence over context.
"""
renderer = Renderer()
self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad')
def test_render__kwargs_does_not_modify_context(self):
"""
Test render(): passing **kwargs does not modify the passed context.
"""
context = {}
renderer = Renderer()
renderer.render('Hi {{person}}', context=context, foo="bar")
self.assertEquals(context, {})
def test_render__nonascii_template(self):
"""
Test passing a non-unicode template with non-ascii characters.
"""
renderer = Renderer()
template = "déf"
# Check that decode_errors and string_encoding are both respected.
renderer.decode_errors = 'ignore'
renderer.string_encoding = 'ascii'
self.assertEquals(renderer.render(template), "df")
renderer.string_encoding = 'utf_8'
self.assertEquals(renderer.render(template), u"déf")
def test_make_load_partial(self):
"""
Test the _make_load_partial() method.
"""
renderer = Renderer()
renderer.partials = {'foo': 'bar'}
load_partial = renderer._make_load_partial()
actual = load_partial('foo')
self.assertEquals(actual, 'bar')
self.assertEquals(type(actual), unicode, "RenderEngine requires that "
"load_partial return unicode strings.")
def test_make_load_partial__unicode(self):
"""
Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode.
"""
renderer = Renderer()
renderer.partials = {'partial': 'foo'}
load_partial = renderer._make_load_partial()
self.assertEquals(load_partial("partial"), "foo")
# Now with a value that is already unicode.
renderer.partials = {'partial': u'foo'}
load_partial = renderer._make_load_partial()
# If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported
self.assertEquals(load_partial("partial"), "foo")
def test_render_path(self):
"""
Test the render_path() method.
"""
renderer = Renderer()
path = get_data_path('say_hello.mustache')
actual = renderer.render_path(path, to='foo')
self.assertEquals(actual, "Hello, foo")
def test_render__object(self):
"""
Test rendering an object instance.
"""
renderer = Renderer()
say_hello = SayHello()
actual = renderer.render(say_hello)
self.assertEquals('Hello, World', actual)
actual = renderer.render(say_hello, to='Mars')
self.assertEquals('Hello, Mars', actual)
def test_render__template_spec(self):
"""
Test rendering a TemplateSpec instance.
"""
renderer = Renderer()
class Spec(TemplateSpec):
template = "hello, {{to}}"
to = 'world'
spec = Spec()
actual = renderer.render(spec)
self.assertString(actual, u'hello, world')
def test_render__view(self):
"""
Test rendering a View instance.
"""
renderer = Renderer()
view = Simple()
actual = renderer.render(view)
self.assertEquals('Hi pizza!', actual)
# By testing that Renderer.render() constructs the right RenderEngine,
# we no longer need to exercise all rendering code paths through
# the Renderer. It suffices to test rendering paths through the
# RenderEngine for the same amount of code coverage.
class Renderer_MakeRenderEngineTests(unittest.TestCase):
"""
Check the RenderEngine returned by Renderer._make_render_engine().
"""
## Test the engine's load_partial attribute.
def test__load_partial__returns_unicode(self):
"""
Check that load_partial returns unicode (and not a subclass).
"""
class MyUnicode(unicode):
pass
renderer = Renderer()
renderer.string_encoding = 'ascii'
renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')}
engine = renderer._make_render_engine()
actual = engine.load_partial('str')
self.assertEquals(actual, "foo")
self.assertEquals(type(actual), unicode)
# Check that unicode subclasses are not preserved.
actual = engine.load_partial('subclass')
self.assertEquals(actual, "abc")
self.assertEquals(type(actual), unicode)
def test__load_partial__not_found(self):
"""
Check that load_partial provides a nice message when a template is not found.
"""
renderer = Renderer()
renderer.partials = {}
engine = renderer._make_render_engine()
load_partial = engine.load_partial
try:
load_partial("foo")
raise Exception("Shouldn't get here")
except Exception, err:
self.assertEquals(str(err), "Partial not found with name: 'foo'")
## Test the engine's literal attribute.
def test__literal__uses_renderer_unicode(self):
"""
Test that literal uses the renderer's unicode function.
"""
renderer = Renderer()
renderer.unicode = lambda s: s.upper()
engine = renderer._make_render_engine()
literal = engine.literal
self.assertEquals(literal("foo"), "FOO")
def test__literal__handles_unicode(self):
"""
Test that literal doesn't try to "double decode" unicode.
"""
renderer = Renderer()
renderer.string_encoding = 'ascii'
engine = renderer._make_render_engine()
literal = engine.literal
self.assertEquals(literal(u"foo"), "foo")
def test__literal__returns_unicode(self):
"""
Test that literal returns unicode (and not a subclass).
"""
renderer = Renderer()
renderer.string_encoding = 'ascii'
engine = renderer._make_render_engine()
literal = engine.literal
self.assertEquals(type(literal("foo")), unicode)
class MyUnicode(unicode):
pass
s = MyUnicode("abc")
self.assertEquals(type(s), MyUnicode)
self.assertTrue(isinstance(s, unicode))
self.assertEquals(type(literal(s)), unicode)
## Test the engine's escape attribute.
def test__escape__uses_renderer_escape(self):
"""
Test that escape uses the renderer's escape function.
"""
renderer = Renderer()
renderer.escape = lambda s: "**" + s
engine = renderer._make_render_engine()
escape = engine.escape
self.assertEquals(escape("foo"), "**foo")
def test__escape__uses_renderer_unicode(self):
"""
Test that escape uses the renderer's unicode function.
"""
renderer = Renderer()
renderer.unicode = lambda s: s.upper()
engine = renderer._make_render_engine()
escape = engine.escape
self.assertEquals(escape("foo"), "FOO")
def test__escape__has_access_to_original_unicode_subclass(self):
"""
Test that escape receives strings with the unicode subclass intact.
"""
renderer = Renderer()
renderer.escape = lambda s: type(s).__name__
engine = renderer._make_render_engine()
escape = engine.escape
class MyUnicode(unicode):
pass
self.assertEquals(escape("foo"), "unicode")
self.assertEquals(escape(u"foo"), "unicode")
self.assertEquals(escape(MyUnicode("foo")), "MyUnicode")
def test__escape__returns_unicode(self):
"""
Test that literal returns unicode (and not a subclass).
"""
renderer = Renderer()
renderer.string_encoding = 'ascii'
engine = renderer._make_render_engine()
escape = engine.escape
self.assertEquals(type(escape("foo")), unicode)
# Check that literal doesn't preserve unicode subclasses.
class MyUnicode(unicode):
pass
s = MyUnicode("abc")
self.assertEquals(type(s), MyUnicode)
self.assertTrue(isinstance(s, unicode))
self.assertEquals(type(escape(s)), unicode)
import unittest import unittest
import pystache import pystache
from pystache import Renderer
from examples.nested_context import NestedContext from examples.nested_context import NestedContext
from examples.complex_view import ComplexView from examples.complex import Complex
from examples.lambdas import Lambdas from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial from examples.template_partial import TemplatePartial
from examples.simple import Simple from examples.simple import Simple
class TestSimple(unittest.TestCase): from tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin
def test_simple_render(self): class TestSimple(unittest.TestCase, AssertStringMixin):
self.assertEqual('herp', pystache.Template('{{derp}}', {'derp': 'herp'}).render())
def test_nested_context(self): def test_nested_context(self):
view = NestedContext() renderer = Renderer()
self.assertEquals(pystache.Template('{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}', view).render(), "one and foo and two") view = NestedContext(renderer)
view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}'
actual = renderer.render(view)
self.assertString(actual, u"one and foo and two")
def test_looping_and_negation_context(self): def test_looping_and_negation_context(self):
view = ComplexView() template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}'
self.assertEquals(pystache.Template('{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}', view).render(), "Colors: red Colors: green Colors: blue ") context = Complex()
renderer = Renderer()
actual = renderer.render(template, context)
self.assertEquals(actual, "Colors: red Colors: green Colors: blue ")
def test_empty_context(self): def test_empty_context(self):
view = ComplexView() template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}'
self.assertEquals(pystache.Template('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me") self.assertEquals(pystache.Renderer().render(template), "Should see me")
def test_callables(self): def test_callables(self):
view = Lambdas() view = Lambdas()
self.assertEquals(pystache.Template('{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}', view).render(), 'bar != bar. oh, it does!') view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}'
renderer = Renderer()
actual = renderer.render(view)
self.assertString(actual, u'bar != bar. oh, it does!')
def test_rendering_partial(self): def test_rendering_partial(self):
view = TemplatePartial() renderer = Renderer(search_dirs=EXAMPLES_DIR)
self.assertEquals(pystache.Template('{{>inner_partial}}', view).render(), 'Again, Welcome!')
view = TemplatePartial(renderer=renderer)
view.template = '{{>inner_partial}}'
actual = renderer.render(view)
self.assertString(actual, u'Again, Welcome!')
self.assertEquals(pystache.Template('{{#looping}}{{>inner_partial}} {{/looping}}', view).render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''') view.template = '{{#looping}}{{>inner_partial}} {{/looping}}'
actual = renderer.render(view)
self.assertString(actual, u"Again, Welcome! Again, Welcome! Again, Welcome! ")
def test_non_existent_value_renders_blank(self): def test_non_existent_value_renders_blank(self):
view = Simple() view = Simple()
template = '{{not_set}} {{blank}}'
self.assertEquals(pystache.Template('{{not_set}} {{blank}}', view).render(), ' ') self.assertEquals(pystache.Renderer().render(template), ' ')
def test_template_partial_extension(self): def test_template_partial_extension(self):
view = TemplatePartial() """
view.template_extension = 'txt' Side note:
self.assertEquals(view.render(), """Welcome
From the spec--
Partial tags SHOULD be treated as standalone when appropriate.
In particular, this means that trailing newlines should be removed.
"""
renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
view = TemplatePartial(renderer=renderer)
actual = renderer.render(view)
self.assertString(actual, u"""Welcome
------- -------
Again, Welcome! ## Again, Welcome! ##""")
""")
# coding: utf-8
"""
Creates a unittest.TestCase for the tests defined in the mustache spec.
"""
# TODO: this module can be cleaned up somewhat.
try:
# We deserialize the json form rather than the yaml form because
# json libraries are available for Python 2.4.
import json
except:
# The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions.
import simplejson as json
import glob
import os.path
import unittest
from pystache.renderer import Renderer
root_path = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs')
spec_paths = glob.glob(os.path.join(root_path, '*.json'))
class MustacheSpec(unittest.TestCase):
pass
def buildTest(testData, spec_filename):
name = testData['name']
description = testData['desc']
test_name = "%s (%s)" % (name, spec_filename)
def test(self):
template = testData['template']
partials = testData.has_key('partials') and testData['partials'] or {}
expected = testData['expected']
data = testData['data']
# Convert code strings to functions.
# TODO: make this section of code easier to understand.
new_data = {}
for key, val in data.iteritems():
if isinstance(val, dict) and val.get('__tag__') == 'code':
val = eval(val['python'])
new_data[key] = val
renderer = Renderer(partials=partials)
actual = renderer.render(template, new_data)
actual = actual.encode('utf-8')
message = """%s
Template: \"""%s\"""
Expected: %s
Actual: %s
Expected: \"""%s\"""
Actual: \"""%s\"""
""" % (description, template, repr(expected), repr(actual), expected, actual)
self.assertEquals(actual, expected, message)
# The name must begin with "test" for nosetests test discovery to work.
name = 'test: "%s"' % test_name
# If we don't convert unicode to str, we get the following error:
# "TypeError: __name__ must be set to a string object"
test.__name__ = str(name)
return test
for spec_path in spec_paths:
file_name = os.path.basename(spec_path)
# We avoid use of the with keyword for Python 2.4 support.
f = open(spec_path, 'r')
try:
spec_data = json.load(f)
finally:
f.close()
tests = spec_data['tests']
for test in tests:
test = buildTest(test, file_name)
setattr(MustacheSpec, test.__name__, test)
# Prevent this variable from being interpreted as another test.
del(test)
if __name__ == '__main__':
unittest.main()
# coding: utf-8
"""
Unit tests for template_spec.py.
"""
import os.path
import sys
import unittest
import examples
from examples.simple import Simple
from examples.complex import Complex
from examples.lambdas import Lambdas
from examples.inverted import Inverted, InvertedLists
from pystache import Renderer
from pystache import TemplateSpec
from pystache.locator import Locator
from pystache.loader import Loader
from pystache.spec_loader import SpecLoader
from tests.common import DATA_DIR
from tests.common import EXAMPLES_DIR
from tests.common import AssertIsMixin
from tests.common import AssertStringMixin
from tests.data.views import SampleView
from tests.data.views import NonAscii
class Thing(object):
pass
class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_template_rel_directory(self):
"""
Test that View.template_rel_directory is respected.
"""
class Tagless(TemplateSpec):
pass
view = Tagless()
renderer = Renderer()
self.assertRaises(IOError, renderer.render, view)
view.template_rel_directory = "../examples"
actual = renderer.render(view)
self.assertEquals(actual, "No tags...")
def test_template_path_for_partials(self):
"""
Test that View.template_rel_path is respected for partials.
"""
spec = TemplateSpec()
spec.template = "Partial: {{>tagless}}"
renderer1 = Renderer()
renderer2 = Renderer(search_dirs=EXAMPLES_DIR)
self.assertRaises(IOError, renderer1.render, spec)
actual = renderer2.render(spec)
self.assertEquals(actual, "Partial: No tags...")
def test_basic_method_calls(self):
renderer = Renderer()
actual = renderer.render(Simple())
self.assertString(actual, u"Hi pizza!")
def test_non_callable_attributes(self):
view = Simple()
view.thing = 'Chris'
renderer = Renderer()
actual = renderer.render(view)
self.assertEquals(actual, "Hi Chris!")
def test_complex(self):
renderer = Renderer()
actual = renderer.render(Complex())
self.assertString(actual, u"""\
<h1>Colors</h1>
<ul>
<li><strong>red</strong></li>
<li><a href="#Green">green</a></li>
<li><a href="#Blue">blue</a></li>
</ul>""")
def test_higher_order_replace(self):
renderer = Renderer()
actual = renderer.render(Lambdas())
self.assertEquals(actual, 'bar != bar. oh, it does!')
def test_higher_order_rot13(self):
view = Lambdas()
view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'
renderer = Renderer()
actual = renderer.render(view)
self.assertString(actual, u'nopqrstuvwxyz')
def test_higher_order_lambda(self):
view = Lambdas()
view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'
renderer = Renderer()
actual = renderer.render(view)
self.assertString(actual, u'abcdefghijklmnopqrstuvwxyz')
def test_partials_with_lambda(self):
view = Lambdas()
view.template = '{{>partial_with_lambda}}'
renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(view)
self.assertEquals(actual, u'nopqrstuvwxyz')
def test_hierarchical_partials_with_lambdas(self):
view = Lambdas()
view.template = '{{>partial_with_partial_and_lambda}}'
renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(view)
self.assertString(actual, u'nopqrstuvwxyznopqrstuvwxyz')
def test_inverted(self):
renderer = Renderer()
actual = renderer.render(Inverted())
self.assertString(actual, u"""one, two, three, empty list""")
def test_accessing_properties_on_parent_object_from_child_objects(self):
parent = Thing()
parent.this = 'derp'
parent.children = [Thing()]
view = Simple()
view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}"
renderer = Renderer()
actual = renderer.render(view, {'parent': parent})
self.assertString(actual, u'derp')
def test_inverted_lists(self):
renderer = Renderer()
actual = renderer.render(InvertedLists())
self.assertString(actual, u"""one, two, three, empty list""")
class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
Tests template_spec.SpecLoader.
"""
def test_init__defaults(self):
custom = SpecLoader()
# Check the loader attribute.
loader = custom.loader
self.assertEquals(loader.extension, 'mustache')
self.assertEquals(loader.file_encoding, sys.getdefaultencoding())
# TODO: finish testing the other Loader attributes.
to_unicode = loader.to_unicode
def test_init__loader(self):
loader = Loader()
custom = SpecLoader(loader=loader)
self.assertIs(custom.loader, loader)
# TODO: rename to something like _assert_load().
def _assert_template(self, loader, custom, expected):
self.assertString(loader.load(custom), expected)
def test_load__template__type_str(self):
"""
Test the template attribute: str string.
"""
custom = TemplateSpec()
custom.template = "abc"
self._assert_template(SpecLoader(), custom, u"abc")
def test_load__template__type_unicode(self):
"""
Test the template attribute: unicode string.
"""
custom = TemplateSpec()
custom.template = u"abc"
self._assert_template(SpecLoader(), custom, u"abc")
def test_load__template__unicode_non_ascii(self):
"""
Test the template attribute: non-ascii unicode string.
"""
custom = TemplateSpec()
custom.template = u"é"
self._assert_template(SpecLoader(), custom, u"é")
def test_load__template__with_template_encoding(self):
"""
Test the template attribute: with template encoding attribute.
"""
custom = TemplateSpec()
custom.template = u'é'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, self._assert_template, SpecLoader(), custom, u'é')
custom.template_encoding = 'utf-8'
self._assert_template(SpecLoader(), custom, u'é')
# TODO: make this test complete.
def test_load__template__correct_loader(self):
"""
Test that reader.unicode() is called correctly.
This test tests that the correct reader is called with the correct
arguments. This is a catch-all test to supplement the other
test cases. It tests SpecLoader.load() independent of reader.unicode()
being implemented correctly (and tested).
"""
class MockLoader(Loader):
def __init__(self):
self.s = None
self.encoding = None
# Overrides the existing method.
def unicode(self, s, encoding=None):
self.s = s
self.encoding = encoding
return u"foo"
loader = MockLoader()
custom_loader = SpecLoader()
custom_loader.loader = loader
view = TemplateSpec()
view.template = "template-foo"
view.template_encoding = "encoding-foo"
# Check that our unicode() above was called.
self._assert_template(custom_loader, view, u'foo')
self.assertEquals(loader.s, "template-foo")
self.assertEquals(loader.encoding, "encoding-foo")
# TODO: migrate these tests into the SpecLoaderTests class.
# TODO: rename the get_template() tests to test load().
# TODO: condense, reorganize, and rename the tests so that it is
# clear whether we have full test coverage (e.g. organized by
# TemplateSpec attributes or something).
class TemplateSpecTests(unittest.TestCase):
# TODO: rename this method to _make_loader().
def _make_locator(self):
return SpecLoader()
def _assert_template_location(self, view, expected):
locator = self._make_locator()
actual = locator._find_relative(view)
self.assertEquals(actual, expected)
def test_find_relative(self):
"""
Test _find_relative(): default behavior (no attributes set).
"""
view = SampleView()
self._assert_template_location(view, (None, 'sample_view.mustache'))
def test_find_relative__template_rel_path__file_name_only(self):
"""
Test _find_relative(): template_rel_path attribute.
"""
view = SampleView()
view.template_rel_path = 'template.txt'
self._assert_template_location(view, ('', 'template.txt'))
def test_find_relative__template_rel_path__file_name_with_directory(self):
"""
Test _find_relative(): template_rel_path attribute.
"""
view = SampleView()
view.template_rel_path = 'foo/bar/template.txt'
self._assert_template_location(view, ('foo/bar', 'template.txt'))
def test_find_relative__template_rel_directory(self):
"""
Test _find_relative(): template_rel_directory attribute.
"""
view = SampleView()
view.template_rel_directory = 'foo'
self._assert_template_location(view, ('foo', 'sample_view.mustache'))
def test_find_relative__template_name(self):
"""
Test _find_relative(): template_name attribute.
"""
view = SampleView()
view.template_name = 'new_name'
self._assert_template_location(view, (None, 'new_name.mustache'))
def test_find_relative__template_extension(self):
"""
Test _find_relative(): template_extension attribute.
"""
view = SampleView()
view.template_extension = 'txt'
self._assert_template_location(view, (None, 'sample_view.txt'))
def test_find__with_directory(self):
"""
Test _find() with a view that has a directory specified.
"""
locator = self._make_locator()
view = SampleView()
view.template_rel_path = 'foo/bar.txt'
self.assertTrue(locator._find_relative(view)[0] is not None)
actual = locator._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt'))
self.assertEquals(actual, expected)
def test_find__without_directory(self):
"""
Test _find() with a view that doesn't have a directory specified.
"""
locator = self._make_locator()
view = SampleView()
self.assertTrue(locator._find_relative(view)[0] is None)
actual = locator._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
self.assertEquals(actual, expected)
def _assert_get_template(self, custom, expected):
locator = self._make_locator()
actual = locator.load(custom)
self.assertEquals(type(actual), unicode)
self.assertEquals(actual, expected)
def test_get_template(self):
"""
Test get_template(): default behavior (no attributes set).
"""
view = SampleView()
self._assert_get_template(view, u"ascii: abc")
def test_get_template__template_encoding(self):
"""
Test get_template(): template_encoding attribute.
"""
view = NonAscii()
self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo')
view.template_encoding = 'utf-8'
self._assert_get_template(view, u"non-ascii: é")
import unittest
import pystache
from examples.simple import Simple
from examples.complex_view import ComplexView
from examples.lambdas import Lambdas
from examples.inverted import Inverted, InvertedLists
class Thing(object):
pass
class TestView(unittest.TestCase):
def test_basic(self):
view = Simple("Hi {{thing}}!", { 'thing': 'world' })
self.assertEquals(view.render(), "Hi world!")
def test_kwargs(self):
view = Simple("Hi {{thing}}!", thing='world')
self.assertEquals(view.render(), "Hi world!")
def test_template_load(self):
view = Simple(thing='world')
self.assertEquals(view.render(), "Hi world!")
def test_template_load_from_multiple_path(self):
path = Simple.template_path
Simple.template_path = ('examples/nowhere','examples',)
try:
view = Simple(thing='world')
self.assertEquals(view.render(), "Hi world!")
finally:
Simple.template_path = path
def test_template_load_from_multiple_path_fail(self):
path = Simple.template_path
Simple.template_path = ('examples/nowhere',)
try:
view = Simple(thing='world')
self.assertRaises(IOError, view.render)
finally:
Simple.template_path = path
def test_basic_method_calls(self):
view = Simple()
self.assertEquals(view.render(), "Hi pizza!")
def test_non_callable_attributes(self):
view = Simple()
view.thing = 'Chris'
self.assertEquals(view.render(), "Hi Chris!")
def test_view_instances_as_attributes(self):
other = Simple(name='chris')
other.template = '{{name}}'
view = Simple()
view.thing = other
self.assertEquals(view.render(), "Hi chris!")
def test_complex(self):
self.assertEquals(ComplexView().render(),
"""<h1>Colors</h1><ul><li><strong>red</strong></li><li><a href="#Green">green</a></li><li><a href="#Blue">blue</a></li></ul>""")
def test_higher_order_replace(self):
view = Lambdas()
self.assertEquals(view.render(),
'bar != bar. oh, it does!')
def test_higher_order_rot13(self):
view = Lambdas()
view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'
self.assertEquals(view.render(), 'nopqrstuvwxyz')
def test_higher_order_lambda(self):
view = Lambdas()
view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'
self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz')
def test_partials_with_lambda(self):
view = Lambdas()
view.template = '{{>partial_with_lambda}}'
self.assertEquals(view.render(), 'nopqrstuvwxyz')
def test_hierarchical_partials_with_lambdas(self):
view = Lambdas()
view.template = '{{>partial_with_partial_and_lambda}}'
self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz')
def test_inverted(self):
view = Inverted()
self.assertEquals(view.render(), """one, two, three, empty list""")
def test_accessing_properties_on_parent_object_from_child_objects(self):
parent = Thing()
parent.this = 'derp'
parent.children = [Thing()]
view = Simple(context={'parent': parent})
view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}"
self.assertEquals(view.render(), 'derp')
def test_context_returns_a_flattened_dict(self):
view = Simple()
view.context_list = [{'one':'1'}, {'two':'2'}, object()]
self.assertEqual(view.context, {'one': '1', 'two': '2'})
def test_inverted_lists(self):
view = InvertedLists()
self.assertEquals(view.render(), """one, two, three, empty list""")
if __name__ == '__main__':
unittest.main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment