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
=======
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)
------------------
* Added support for Python 2.4. [wangtz, jvantuyl]
......@@ -44,3 +91,7 @@ History
------------------
* 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
Permission is hereby granted, free of charge, to any person obtaining
......
......@@ -4,27 +4,38 @@ Pystache
.. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png
Inspired by ctemplate_ and et_, Mustache_ is a
framework-agnostic way to render logic-free views.
Pystache_ is a Python implementation of Mustache_.
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:
it is impossible to embed application logic in this template language."
The `mustache(5)`_ man page provides a good introduction to Mustache's
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
* Python 2.5
* Python 2.6
* Python 2.7
Logo: `David Phillips`_
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
==========
......@@ -41,25 +52,64 @@ Use It
>>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
'Hi Mom!'
u'Hi Mom!'
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
class Simple(pystache.View):
def thing(self):
return "pizza"
def to(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::
>>> Simple().render()
'Hi pizza!'
>>> renderer = pystache.Renderer()
>>> 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
......@@ -76,27 +126,57 @@ to type, for example ::
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
==================
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/
Note: There's a bit of a delay in seeing the latest emails appear
in the archive.
Author
======
::
context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
pystache.render("{{author}} :: {{email}}", context)
>>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
>>> pystache.render("{{author}} :: {{email}}", context)
u'Chris Wanstrath :: chris@ozmm.org'
.. _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
.. _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
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
\ No newline at end of file
.. _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
<h1>{{title}}{{! just something interesting... #or not... }}</h1>
<h1>{{title}}{{! just something interesting... #or not... }}</h1>
\ No newline at end of file
import pystache
class Comments(pystache.View):
template_path = 'examples'
class Comments(object):
def title(self):
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 ComplexView(pystache.View):
template_path = 'examples'
class Complex(object):
def header(self):
return "Colors"
......@@ -18,6 +15,6 @@ class ComplexView(pystache.View):
def empty(self):
return len(self.item()) == 0
def empty_list(self):
return [];
<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(pystache.View):
template_path = 'examples'
class Delimiters(object):
def first(self):
return "It worked the first time."
......
import pystache
class DoubleSection(pystache.View):
template_path = 'examples'
class DoubleSection(object):
def t(self):
return True
......
import pystache
class Escaped(pystache.View):
template_path = 'examples'
class Escaped(object):
def title(self):
return "Bear > Shark"
No file extension: {{foo}}
\ No newline at end of file
## Again, {{title}}! ##
## Again, {{title}}! ##
\ No newline at end of file
import pystache
from pystache import TemplateSpec
class Inverted(pystache.View):
template_path = 'examples'
class Inverted(object):
def t(self):
return True
......@@ -14,11 +13,11 @@ class Inverted(pystache.View):
def empty_list(self):
return []
def populated_list(self):
return ['some_value']
class InvertedLists(Inverted):
class InvertedLists(Inverted, TemplateSpec):
template_name = 'inverted'
def t(self):
......
import pystache
from pystache import TemplateSpec
def rot(s, n=13):
r = ""
......@@ -17,8 +17,10 @@ def rot(s, n=13):
def replace(subject, this='foo', with_this='bar'):
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):
return replace
......
import pystache
from pystache import TemplateSpec
class NestedContext(pystache.View):
template_path = 'examples'
class NestedContext(TemplateSpec):
def __init__(self, renderer):
self.renderer = renderer
def _context_get(self, key):
return self.renderer.context.get(key)
def outer_thing(self):
return "two"
......@@ -16,6 +21,6 @@ class NestedContext(pystache.View):
return [{'outer': 'car'}]
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 ''
\ No newline at end of file
return ''
import pystache
from examples.lambdas import rot
class PartialsWithLambdas(pystache.View):
template_path = 'examples'
class PartialsWithLambdas(object):
def rot(self):
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):
template_path = 'examples'
class Simple(TemplateSpec):
def thing(self):
return "pizza"
......
No tags...
\ No newline at end of file
import pystache
from pystache import TemplateSpec
class TemplatePartial(pystache.View):
template_path = 'examples'
class TemplatePartial(TemplateSpec):
def __init__(self, renderer):
self.renderer = renderer
def _context_get(self, key):
return self.renderer.context.get(key)
def title(self):
return "Welcome"
......@@ -11,6 +16,6 @@ class TemplatePartial(pystache.View):
def looping(self):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
def thing(self):
return self['prop']
\ No newline at end of file
return self._context_get('prop')
\ No newline at end of file
import pystache
class Unescaped(pystache.View):
template_path = 'examples'
class Unescaped(object):
def title(self):
return "Bear > Shark"
<p>If alive today, Henri Poincaré would be {{age}} years old.</p>
\ No newline at end of file
abcdé
\ 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'
def age(self):
......
# encoding: utf-8
import pystache
class UnicodeOutput(pystache.View):
template_path = 'examples'
class UnicodeOutput(object):
def name(self):
return u'Henri Poincaré'
Subproject commit 48c933b0bb780875acbfd15816297e263c53d6f7
from pystache.template import Template
from pystache.view import View
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()
# We keep all initialization code in a separate module.
from init import *
# 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 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):
template_extension = 'mustache'
template_path = '.'
template_encoding = None
def load_template(self, template_name, template_dirs=None, encoding=None, extension=None):
'''Returns the template string from a file or throws IOError if it non existent'''
if None == template_dirs:
template_dirs = self.template_path
if encoding is not None:
self.template_encoding = encoding
if extension is not None:
self.template_extension = extension
file_name = template_name + '.' + self.template_extension
# Given a single directory we'll load from it
if isinstance(template_dirs, basestring):
file_path = os.path.join(template_dirs, file_name)
return self._load_template_file(file_path)
# Given a list of directories we'll check each for our file
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),))
def _load_template_file(self, file_path):
'''Loads and returns the template file from disk'''
f = open(file_path, 'r')
"""
Loads the template associated to a name or user-defined object.
"""
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
if search_dirs is None:
search_dirs = defaults.SEARCH_DIRS
if to_unicode is None:
to_unicode = _to_unicode
self.extension = extension
self.file_encoding = file_encoding
# TODO: unit test setting this attribute.
self.search_dirs = search_dirs
self.to_unicode = to_unicode
def _make_locator(self):
return Locator(extension=self.extension)
def unicode(self, s, encoding=None):
"""
Convert a string to unicode using the given encoding, and return it.
This function uses the underlying to_unicode attribute.
Arguments:
s: a basestring instance to convert to unicode. Unlike Python's
built-in unicode() function, it is okay to pass unicode strings
to this function. (Passing a unicode string to Python's unicode()
with the encoding argument throws the error, "TypeError: decoding
Unicode is not supported.")
encoding: the encoding to pass to the to_unicode attribute.
Defaults to None.
"""
if isinstance(s, unicode):
return unicode(s)
return self.to_unicode(s, encoding)
def read(self, path, encoding=None):
"""
Read the template at the given path, and return it as a unicode string.
"""
# We avoid use of the with keyword for Python 2.4 support.
f = open(path, 'r')
try:
template = f.read()
if self.template_encoding:
template = unicode(template, self.template_encoding)
text = f.read()
finally:
f.close()
return template
\ No newline at end of file
if encoding is None:
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 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
# 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 sys
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
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.
if sys.argv[-1] == "publish":
"""
long_description = open('README.rst').read() + '\n\n' + open('HISTORY.rst').read()
return long_description
if sys.argv[-1] == 'publish':
publish()
sys.exit()
long_description = make_long_description()
setup(name='pystache',
version='0.4.1',
version='0.5.0-rc',
description='Mustache for Python',
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
long_description=long_description,
author='Chris Wanstrath',
author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache',
packages=['pystache'],
license='MIT',
entry_points = {
'console_scripts': ['pystache=pystache.commands:main'],
},
classifiers = (
"Development Status :: 4 - Beta",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 2.4",
"Programming Language :: Python :: 2.5",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
)
)
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.4',
'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'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
# encoding: utf-8
import unittest
import pystache
from examples.comments import Comments
from examples.double_section import DoubleSection
......@@ -12,72 +11,91 @@ from examples.delimiters import Delimiters
from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput
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):
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):
self.assertEquals(DoubleSection().render(),"""* first\n* second\n* third""")
self._assert(DoubleSection(), u"* first\n* second\n* third")
def test_unicode_output(self):
self.assertEquals(UnicodeOutput().render(), u'<p>Name: Henri Poincaré</p>')
def test_encoded_output(self):
self.assertEquals(UnicodeOutput().render('utf8'), '<p>Name: Henri Poincar\xc3\xa9</p>')
renderer = Renderer()
actual = renderer.render(UnicodeOutput())
self.assertString(actual, u'<p>Name: Henri Poincaré</p>')
def test_unicode_input(self):
self.assertEquals(UnicodeInput().render(),
u'<p>If alive today, Henri Poincaré would be 156 years old.</p>')
renderer = Renderer()
actual = renderer.render(UnicodeInput())
self.assertString(actual, u'abcdé')
def test_escaped(self):
self.assertEquals(Escaped().render(), "<h1>Bear &gt; Shark</h1>")
def test_escaping(self):
self._assert(Escaped(), u"<h1>Bear &gt; Shark</h1>")
def test_unescaped(self):
self.assertEquals(Unescaped().render(), "<h1>Bear > Shark</h1>")
def test_unescaped_sigil(self):
view = Escaped(template="<h1>{{& thing}}</h1>", context={
'thing': 'Bear > Giraffe'
})
self.assertEquals(view.render(), "<h1>Bear > Giraffe</h1>")
def test_literal(self):
renderer = Renderer()
actual = renderer.render(Unescaped())
self.assertString(actual, u"<h1>Bear > Shark</h1>")
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!""")
def test_template_partial_extension(self):
view = TemplatePartial()
view.template_extension = 'txt'
self.assertEquals(view.render(), """Welcome
-------
renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
Again, Welcome!
""")
view = TemplatePartial(renderer=renderer)
actual = renderer.render(view)
self.assertString(actual, u"""Welcome
-------
## Again, Welcome! ##""")
def test_delimiters(self):
self.assertEquals(Delimiters().render(), """
renderer = Renderer()
actual = renderer.render(Delimiters())
self.assertString(actual, u"""\
* It worked the first time.
* And it worked the second time.
* Then, surprisingly, it worked the third time.
""")
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):
view = NestedContext()
renderer = Renderer()
view = NestedContext(renderer)
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):
view = TemplatePartial(context = {'prop': 'derp'})
renderer = Renderer(search_dirs=EXAMPLES_DIR)
view = TemplatePartial(renderer=renderer)
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__':
unittest.main()
# encoding: utf-8
"""
Unit tests of reader.py.
"""
import os
import sys
import unittest
import pystache
class TestLoader(unittest.TestCase):
def test_template_is_loaded(self):
loader = pystache.Loader()
template = loader.load_template('simple', 'examples')
self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
def test_using_list_of_paths(self):
loader = pystache.Loader()
template = loader.load_template('simple', ['doesnt_exist', 'examples'])
self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
def test_non_existent_template_fails(self):
loader = pystache.Loader()
self.assertRaises(IOError, loader.load_template, 'simple', 'doesnt_exist')
\ No newline at end of file
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)
actual = reader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é')
def test_reader__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
"""
reader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path)
#reader.decode_errors = 'ignore'
#actual = reader.read(path)
#self.assertString(actual, u'non-ascii: ')
# 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 @@
import unittest
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):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
self.assertEquals(ret, "Hi world!")
......@@ -14,66 +21,96 @@ class TestPystache(unittest.TestCase):
def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?"
ret = pystache.render(template, { 'beverage': 'soda', 'person': 'Bob' })
self.assertEquals(ret, "It's a nice day for soda, right Bob?")
context = { 'beverage': 'soda', 'person': 'Bob' }
self._assert_rendered("It's a nice day for soda, right Bob?", template, context)
def test_even_less_basic(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?"
ret = pystache.render(template, { 'name': 'Jon', 'thing': 'racecar' })
self.assertEquals(ret, "I think Jon wants a racecar, right Jon?")
context = { 'name': 'Jon', 'thing': 'racecar' }
self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context)
def test_ignores_misses(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?"
ret = pystache.render(template, { 'name': 'Jon' })
self.assertEquals(ret, "I think Jon wants a , right Jon?")
context = { 'name': 'Jon' }
self._assert_rendered("I think Jon wants a , right Jon?", template, context)
def test_render_zero(self):
template = 'My value is {{value}}.'
ret = pystache.render(template, { 'value': 0 })
self.assertEquals(ret, 'My value is 0.')
context = { 'value': 0 }
self._assert_rendered('My value is 0.', template, context)
def test_comments(self):
template = "What {{! the }} what?"
ret = pystache.render(template)
self.assertEquals(ret, "What what?")
actual = pystache.render(template)
self.assertEquals("What what?", actual)
def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!"
ret = pystache.render(template, { 'set': False })
self.assertEquals(ret, "Ready go!")
context = { 'set': False }
self._assert_rendered("Ready go!", template, context)
def test_true_sections_are_shown(self):
template = "Ready {{#set}}set{{/set}} go!"
ret = pystache.render(template, { 'set': True })
self.assertEquals(ret, "Ready set go!")
context = { 'set': True }
self._assert_rendered("Ready set go!", template, context)
non_strings_expected = """(123 & ['something'])(chris & 0.9)"""
def test_non_strings(self):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
stats = []
stats.append({'key': 123, 'value': ['something']})
stats.append({'key': u"chris", 'value': 0.900})
ret = pystache.render(template, { 'stats': stats })
self.assertEquals(ret, """(123 & ['something'])(chris & 0.9)""")
context = { 'stats': stats }
self._assert_rendered(self.non_strings_expected, template, context)
def test_unicode(self):
template = 'Name: {{name}}; Age: {{age}}'
ret = pystache.render(template, { 'name': u'Henri Poincaré',
'age': 156 })
self.assertEquals(ret, u'Name: Henri Poincaré; Age: 156')
context = {'name': u'Henri Poincaré', 'age': 156 }
self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context)
def test_sections(self):
template = """<ul>{{#users}}<li>{{name}}</li>{{/users}}</ul>"""
context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] }
ret = pystache.render(template, context)
self.assertEquals(ret, """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>""")
expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
self._assert_rendered(expected, template, context)
def test_implicit_iterator(self):
template = """<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"""
context = { 'users': [ 'Chris', 'Tom','PJ' ] }
ret = pystache.render(template, context)
self.assertEquals(ret, """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>""")
expected = """<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)
import unittest
import pystache
from pystache import Renderer
from examples.nested_context import NestedContext
from examples.complex_view import ComplexView
from examples.complex import Complex
from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial
from examples.simple import Simple
class TestSimple(unittest.TestCase):
def test_simple_render(self):
self.assertEqual('herp', pystache.Template('{{derp}}', {'derp': 'herp'}).render())
from tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin
class TestSimple(unittest.TestCase, AssertStringMixin):
def test_nested_context(self):
view = NestedContext()
self.assertEquals(pystache.Template('{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}', view).render(), "one and foo and two")
renderer = Renderer()
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):
view = ComplexView()
self.assertEquals(pystache.Template('{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}', view).render(), "Colors: red Colors: green Colors: blue ")
template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}'
context = Complex()
renderer = Renderer()
actual = renderer.render(template, context)
self.assertEquals(actual, "Colors: red Colors: green Colors: blue ")
def test_empty_context(self):
view = ComplexView()
self.assertEquals(pystache.Template('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me")
template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}'
self.assertEquals(pystache.Renderer().render(template), "Should see me")
def test_callables(self):
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):
view = TemplatePartial()
self.assertEquals(pystache.Template('{{>inner_partial}}', view).render(), 'Again, Welcome!')
self.assertEquals(pystache.Template('{{#looping}}{{>inner_partial}} {{/looping}}', view).render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''')
renderer = Renderer(search_dirs=EXAMPLES_DIR)
view = TemplatePartial(renderer=renderer)
view.template = '{{>inner_partial}}'
actual = renderer.render(view)
self.assertString(actual, u'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):
view = Simple()
self.assertEquals(pystache.Template('{{not_set}} {{blank}}', view).render(), ' ')
template = '{{not_set}} {{blank}}'
self.assertEquals(pystache.Renderer().render(template), ' ')
def test_template_partial_extension(self):
view = TemplatePartial()
view.template_extension = 'txt'
self.assertEquals(view.render(), """Welcome
"""
Side note:
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()
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