Commit 8bc8baf3 by Chris Jerdonek

Merge branch 'development' into 'master': staging v0.5.2-rc

parents cc262abf 3c839348
History
=======
0.5.2 (2012-05-03)
------------------
* Added support for dot notation and version 1.1.2 of the spec (issue #99). [rbp]
* Missing partials now render as empty string per latest version of spec (issue #115).
* Bugfix: falsey values now coerced to strings using str().
* Bugfix: lambda return values for sections no longer pushed onto context stack (issue #113).
* Bugfix: lists of lambdas for sections were not rendered (issue #114).
0.5.1 (2012-04-24)
------------------
......
......@@ -15,7 +15,7 @@ syntax. For a more complete (and more current) description of Mustache's
behavior, see the official `Mustache spec`_.
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.
version of Pystache passes all tests in `version 1.1.2`_ of the spec.
Logo: `David Phillips`_
......@@ -23,7 +23,7 @@ Logo: `David Phillips`_
Requirements
============
Pystache is tested with the following versions of Python:
Pystache is tested with--
* Python 2.4 (requires simplejson `version 2.0.9`_ or earlier)
* Python 2.5 (requires simplejson_)
......@@ -49,6 +49,9 @@ Install It
::
pip install pystache
pystache-test
To install and test from source (e.g. from GitHub), see the Develop section.
Use It
......@@ -74,7 +77,8 @@ Like so::
>>> from pystache.tests.examples.readme import SayHello
>>> hello = SayHello()
Then your template, say_hello.mustache (by default in the same directory)::
Then your template, say_hello.mustache (in the same directory by default
as your class definition)::
Hello, {{to}}!
......@@ -95,9 +99,8 @@ more information.
Python 3
========
As of version 0.5.1, Pystache fully supports Python 3. There are slight
differences in behavior between Pystache running under Python 2 and 3,
as follows:
Pystache has supported Python 3 since version 0.5.1. Pystache behaves
slightly differently between Python 2 and 3, as follows:
* In Python 2, the default html-escape function ``cgi.escape()`` does not
escape single quotes; whereas in Python 3, the default escape function
......@@ -109,14 +112,13 @@ as follows:
defaults by passing in the encodings explicitly (e.g. to the ``Renderer`` class).
Unicode Handling
================
Unicode
=======
This section describes Pystache's handling of unicode (e.g. strings and
encodings).
This section describes how Pystache handles unicode, strings, and encodings.
Internally, Pystache uses `only unicode strings`_ (type ``str`` in Python 3 and
type ``unicode`` in Python 2). For input, Pystache accepts both unicode strings
Internally, Pystache uses `only unicode strings`_ (``str`` in Python 3 and
``unicode`` in Python 2). For input, Pystache accepts both unicode strings
and byte strings (``bytes`` in Python 3 and ``str`` in Python 2). For output,
Pystache's template rendering methods return only unicode.
......@@ -143,26 +145,22 @@ attribute can be controlled on a per-view basis by subclassing the
default to values set in Pystache's ``defaults`` module.
Test It
Develop
=======
From an install-- ::
pystache-test
From a source distribution-- ::
To test from a source distribution (without installing)-- ::
python test_pystache.py
To test Pystache source under multiple versions of Python all at once, you
can use tox_: ::
To test Pystache with multiple versions of Python (with a single command!),
you can use tox_: ::
pip install tox
tox
If you do not have all Python versions listed in ``tox.ini``-- ::
tox -e py26,py27 # for example
tox -e py26,py32 # for example
The source distribution tests also include doctests and tests from the
Mustache spec. To include tests from the Mustache spec in your test runs: ::
......@@ -175,23 +173,32 @@ is present. Otherwise, it parses the json files. To install PyYAML-- ::
pip install pyyaml
To test Pystache from a source distribution with Python 3.x, you must use tox.
This is because the source code must first be run through 2to3_.
To run a subset of the tests, you can use nose_: ::
pip install nose
nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
Mailing List
============
**Running Pystache from source with Python 3.** Pystache is written in
Python 2 and must be converted with 2to3_ prior to running under Python 3.
The installation process (and tox) do this conversion automatically.
To ``import pystache`` from a source distribution while using Python 3,
be sure that you are importing from a directory containing a converted
version (e.g. from your site-packages directory after manually installing)
and not from the original source directory. Otherwise, you will get a
syntax error. You can help ensure this by not running the Python IDE
from the project directory when importing Pystache.
As of November 2011, there's a mailing list, pystache@librelist.com.
Archive: http://librelist.com/browser/pystache/
Mailing List
============
Note: There's a bit of a delay in seeing the latest emails appear
in the archive.
There is a `mailing list`_. Note that there is a bit of a delay between
posting a message and seeing it appear in the mailing list archive.
Author
======
Authors
=======
::
......@@ -208,9 +215,11 @@ Author
.. _Distribute: http://pypi.python.org/pypi/distribute
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _json: http://docs.python.org/library/json.html
.. _mailing list: http://librelist.com/browser/pystache/
.. _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
.. _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
......@@ -221,5 +230,5 @@ Author
.. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py
.. _test: http://packages.python.org/distribute/setuptools.html#test
.. _tox: http://pypi.python.org/pypi/tox
.. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7
.. _version 1.1.2: https://github.com/mustache/spec/tree/v1.1.2
.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
......@@ -4,6 +4,5 @@ TODO
* Turn the benchmarking script at pystache/tests/benchmark.py into a command in pystache/commands, or
make it a subcommand of one of the existing commands (i.e. using a command argument).
* Provide support for logging in at least one of the commands.
* Make sure doctest text files can be converted for Python 3 when using tox.
* Make sure command parsing to pystache-test doesn't break with Python 2.4 and earlier.
* Combine pystache-test with the main command.
Subproject commit 48c933b0bb780875acbfd15816297e263c53d6f7
Subproject commit 9b1bc7ad19247e9671304af02078f2ce30132665
......@@ -10,4 +10,4 @@ from pystache.init import render, Renderer, TemplateSpec
__all__ = ['render', 'Renderer', 'TemplateSpec']
__version__ = '0.5.1' # Also change in setup.py.
__version__ = '0.5.2-rc' # Also change in setup.py.
......@@ -35,6 +35,7 @@ import sys
#
# ValueError: Attempted relative import in non-package
#
from pystache.common import TemplateNotFoundError
from pystache.renderer import Renderer
......@@ -78,7 +79,7 @@ def main(sys_argv=sys.argv):
try:
template = renderer.load_template(template)
except IOError:
except TemplateNotFoundError:
pass
try:
......
......@@ -7,7 +7,7 @@ This module provides a command to test pystache (unit tests, doctests, etc).
import sys
from pystache.tests.main import run_tests
from pystache.tests.main import main as run_tests
def main(sys_argv=sys.argv):
......
# coding: utf-8
"""
Exposes common functions.
Exposes functionality needed throughout the project.
"""
......@@ -24,3 +24,13 @@ def read(path):
return f.read()
finally:
f.close()
class PystacheError(Exception):
"""Base class for Pystache exceptions."""
pass
class TemplateNotFoundError(PystacheError):
"""An exception raised when a template is not found."""
pass
......@@ -9,6 +9,7 @@ import os
import re
import sys
from pystache.common import TemplateNotFoundError
from pystache import defaults
......@@ -117,9 +118,8 @@ class Locator(object):
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)))
raise TemplateNotFoundError('File %s not found in dirs: %s' %
(repr(file_name), repr(search_dirs)))
return path
......
......@@ -17,7 +17,7 @@ class ParsedTemplate(object):
parse_tree: a list, each element of which is either--
(1) a unicode string, or
(2) a "rendering" callable that accepts a Context instance
(2) a "rendering" callable that accepts a ContextStack instance
and returns a unicode string.
The possible rendering callables are the return values of the
......@@ -32,6 +32,9 @@ class ParsedTemplate(object):
"""
self._parse_tree = parse_tree
def __repr__(self):
return "[%s]" % (", ".join([repr(part) for part in self._parse_tree]))
def render(self, context):
"""
Returns: a string of type unicode.
......
......@@ -9,15 +9,22 @@ This module is only meant for internal use by the renderengine module.
import re
from pystache.common import TemplateNotFoundError
from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = ('{{', '}}')
END_OF_LINE_CHARACTERS = ['\r', '\n']
NON_BLANK_RE = re.compile(r'^(.)', re.M)
DEFAULT_DELIMITERS = (u'{{', u'}}')
END_OF_LINE_CHARACTERS = [u'\r', u'\n']
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
def _compile_template_re(delimiters):
def _compile_template_re(delimiters=None):
"""
Return a regular expresssion object (re.RegexObject) instance.
"""
if delimiters is None:
delimiters = DEFAULT_DELIMITERS
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
......@@ -74,19 +81,25 @@ class Parser(object):
self._delimiters = delimiters
self.compile_template_re()
def parse(self, template, index=0, section_key=None):
def parse(self, template, start_index=0, section_key=None):
"""
Parse a template string into a ParsedTemplate instance.
Parse a template string starting at some index.
This method uses the current tag delimiter.
Arguments:
template: a template string of type unicode.
template: a unicode string that is the template to parse.
index: the index at which to start parsing.
Returns:
a ParsedTemplate instance.
"""
parse_tree = []
start_index = index
index = start_index
while True:
match = self._template_re.search(template, index)
......@@ -133,7 +146,7 @@ class Parser(object):
if tag_key != section_key:
raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
return ParsedTemplate(parse_tree), template[start_index:match_index], end_index
return ParsedTemplate(parse_tree), match_index, end_index
index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
......@@ -142,10 +155,33 @@ class Parser(object):
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)
def _parse_section(self, template, start_index, section_key):
"""
Parse the contents of a template section.
Arguments:
template: a unicode template string.
start_index: the string index at which the section contents begin.
section_key: the tag key of the section.
Returns: a 3-tuple:
parsed_section: the section contents parsed as a ParsedTemplate
instance.
content_end_index: the string index after the section contents.
end_index: the string index after the closing section tag (and
including any trailing newlines).
"""
parsed_section, content_end_index, end_index = \
self.parse(template=template, start_index=start_index, section_key=section_key)
return parsed_template, template, index_end
return parsed_section, template[start_index:content_end_index], end_index
def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
......@@ -170,20 +206,24 @@ class Parser(object):
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)
parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_section(tag_key, parsed_section, section_contents, self._delimiters)
elif tag_type == '^':
parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
parsed_section, section_contents, 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)
try:
# TODO: make engine.load() and test it separately.
template = engine.load_partial(tag_key)
except TemplateNotFoundError:
template = u''
# Indent before rendering.
template = re.sub(NON_BLANK_RE, leading_whitespace + r'\1', template)
template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
func = engine._make_get_partial(template)
......
......@@ -35,7 +35,8 @@ class RenderEngine(object):
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).
template string of type unicode (not a subclass). If the
template is not found, it should raise a TemplateNotFoundError.
literal: the function used to convert unescaped variable tag
values to unicode, e.g. the value corresponding to a tag
......@@ -63,6 +64,7 @@ class RenderEngine(object):
self.literal = literal
self.load_partial = load_partial
# TODO: rename context to stack throughout this module.
def _get_string_value(self, context, tag_name):
"""
Get a value from the given context as a basestring instance.
......@@ -70,15 +72,6 @@ class RenderEngine(object):
"""
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:
#
......@@ -132,6 +125,7 @@ class RenderEngine(object):
Returns: a string of type unicode.
"""
# TODO: the parsing should be done before calling this function.
return self._render(template, context)
return get_partial
......@@ -142,7 +136,10 @@ class RenderEngine(object):
Returns a string with type unicode.
"""
# TODO: is there a bug because we are not using the same
# logic as in _get_string_value()?
data = context.get(name)
# Per the spec, lambdas in inverted sections are considered truthy.
if data:
return u''
return parsed_template.render(context)
......@@ -161,16 +158,19 @@ class RenderEngine(object):
template = template_
parsed_template = parsed_template_
data = context.get(name)
# From the spec:
#
# If the data is not of a list type, it is coerced into a list
# as follows: if the data is truthy (e.g. `!!data == true`),
# use a single-element list containing the data, otherwise use
# an empty list.
#
if not data:
data = []
elif callable(data):
# TODO: should we check the arity?
template = data(template)
parsed_template = self._parse(template, delimiters=delims)
data = [data]
else:
# The cleanest, least brittle way of determining whether
# something supports iteration is by trying to call iter() on it:
# The least brittle way to determine whether something
# supports iteration is by trying to call iter() on it:
#
# http://docs.python.org/library/functions.html#iter
#
......@@ -184,14 +184,34 @@ class RenderEngine(object):
# Then the value does not support iteration.
data = [data]
else:
# We treat the value as a list (but do not treat strings
# and dicts as lists).
if isinstance(data, (basestring, dict)):
# Do not treat strings and dicts (which are iterable) as lists.
data = [data]
# Otherwise, leave it alone.
# Otherwise, treat the value as a list.
parts = []
for element in data:
if callable(element):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
new_template = element(template)
parsed_template = self._parse(new_template, delimiters=delims)
parts.append(parsed_template.render(context))
continue
context.push(element)
parts.append(parsed_template.render(context))
context.pop()
......@@ -221,7 +241,7 @@ class RenderEngine(object):
Arguments:
template: a template string of type unicode.
context: a Context instance.
context: a ContextStack instance.
"""
# We keep this type-check as an added check because this method is
......@@ -244,7 +264,7 @@ class RenderEngine(object):
template: a template string of type unicode (but not a proper
subclass of unicode).
context: a Context instance.
context: a ContextStack instance.
"""
# Be strict but not too strict. In other words, accept str instead
......
......@@ -8,7 +8,8 @@ This module provides a Renderer class to render templates.
import sys
from pystache import defaults
from pystache.context import Context
from pystache.common import TemplateNotFoundError
from pystache.context import ContextStack
from pystache.loader import Loader
from pystache.renderengine import RenderEngine
from pystache.specloader import SpecLoader
......@@ -239,9 +240,8 @@ class Renderer(object):
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))
raise TemplateNotFoundError("Name %s not found in partials: %s" %
(repr(name), type(partials)))
# RenderEngine requires that the return value be unicode.
return self._to_unicode_hard(template)
......@@ -277,7 +277,7 @@ class Renderer(object):
# RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template)
context = Context.create(*context, **kwargs)
context = ContextStack.create(*context, **kwargs)
self._context = context
engine = self._make_render_engine()
......@@ -338,7 +338,7 @@ class Renderer(object):
uses the passed object as the first element of the context stack
when rendering.
*context: zero or more dictionaries, Context instances, or objects
*context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
arguments are skipped. Items in the *context list are added to
the context stack in order so that later items in the argument
......
......@@ -168,6 +168,20 @@ class AssertIsMixin:
self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
class AssertExceptionMixin:
"""A unittest.TestCase mixin adding assertException()."""
# unittest.assertRaisesRegexp() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertRaisesRegexp
def assertException(self, exception_type, msg, callable, *args, **kwds):
try:
callable(*args, **kwds)
raise Exception("Expected exception: %s: %s" % (exception_type, repr(msg)))
except exception_type, err:
self.assertEqual(str(err), msg)
class SetupDefaults(object):
"""
......@@ -191,3 +205,28 @@ class SetupDefaults(object):
defaults.FILE_ENCODING = self.original_file_encoding
defaults.STRING_ENCODING = self.original_string_encoding
class Attachable(object):
"""
A class that attaches all constructor named parameters as attributes.
For example--
>>> obj = Attachable(foo=42, size="of the universe")
>>> repr(obj)
"Attachable(foo=42, size='of the universe')"
>>> obj.foo
42
>>> obj.size
'of the universe'
"""
def __init__(self, **kwargs):
self.__args__ = kwargs
for arg, value in kwargs.iteritems():
setattr(self, arg, value)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__,
", ".join("%s=%s" % (k, repr(v))
for k, v in self.__args__.iteritems()))
......@@ -12,4 +12,4 @@ class Simple(TemplateSpec):
return "pizza"
def blank(self):
pass
return ''
......@@ -24,7 +24,9 @@ from pystache.tests.spectesting import get_spec_tests
FROM_SOURCE_OPTION = "--from-source"
def run_tests(sys_argv):
# Do not include "test" in this function's name to avoid it getting
# picked up by nosetests.
def main(sys_argv):
"""
Run all tests in the project.
......
......@@ -115,6 +115,37 @@ def _read_spec_tests(path):
return cases
# TODO: simplify the implementation of this function.
def _convert_children(node):
"""
Recursively convert to functions all "code strings" below the node.
This function is needed only for the json format.
"""
if not isinstance(node, (list, dict)):
# Then there is nothing to iterate over and recurse.
return
if isinstance(node, list):
for child in node:
_convert_children(child)
return
# Otherwise, node is a dict, so attempt the conversion.
for key in node.keys():
val = node[key]
if not isinstance(val, dict) or val.get('__tag__') != 'code':
_convert_children(val)
continue
# Otherwise, we are at a "leaf" node.
val = eval(val['python'])
node[key] = val
continue
def _deserialize_spec_test(data, file_path):
"""
Return a unittest.TestCase instance representing a spec test.
......@@ -124,7 +155,7 @@ def _deserialize_spec_test(data, file_path):
data: the dictionary of attributes for a single test.
"""
unconverted_context = data['data']
context = data['data']
description = data['desc']
# PyYAML seems to leave ASCII strings as byte strings.
expected = unicode(data['expected'])
......@@ -133,13 +164,7 @@ def _deserialize_spec_test(data, file_path):
template = data['template']
test_name = data['name']
# Convert code strings to functions.
# TODO: make this section of code easier to understand.
context = {}
for key, val in unconverted_context.iteritems():
if isinstance(val, dict) and val.get('__tag__') == 'code':
val = eval(val['python'])
context[key] = val
_convert_children(context)
test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path)
......
......@@ -11,14 +11,15 @@ import sys
import unittest
# TODO: remove this alias.
from pystache.common import TemplateNotFoundError
from pystache.loader import Loader as Reader
from pystache.locator import Locator
from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from pystache.tests.common import DATA_DIR, EXAMPLES_DIR, AssertExceptionMixin
from pystache.tests.data.views import SayHello
class LocatorTests(unittest.TestCase):
class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def _locator(self):
return Locator(search_dirs=DATA_DIR)
......@@ -110,7 +111,8 @@ class LocatorTests(unittest.TestCase):
def test_find_name__non_existent_template_fails(self):
locator = Locator()
self.assertRaises(IOError, locator.find_name, search_dirs=[], template_name='doesnt_exist')
self.assertException(TemplateNotFoundError, "File 'doesnt_exist.mustache' not found in dirs: []",
locator.find_name, search_dirs=[], template_name='doesnt_exist')
def test_find_object(self):
locator = Locator()
......
# coding: utf-8
"""
Unit tests of parser.py.
"""
import unittest
from pystache.parser import _compile_template_re as make_re
class RegularExpressionTestCase(unittest.TestCase):
"""Tests the regular expression returned by _compile_template_re()."""
def test_re(self):
"""
Test getting a key from a dictionary.
"""
re = make_re()
match = re.search("b {{test}}")
self.assertEqual(match.start(), 1)
......@@ -7,11 +7,11 @@ Unit tests of renderengine.py.
import unittest
from pystache.context import Context
from pystache.context import ContextStack
from pystache import defaults
from pystache.parser import ParsingError
from pystache.renderengine import RenderEngine
from pystache.tests.common import AssertStringMixin
from pystache.tests.common import AssertStringMixin, Attachable
def mock_literal(s):
......@@ -83,7 +83,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
if partials is not None:
engine.load_partial = lambda key: unicode(partials[key])
context = Context(*context)
context = ContextStack(*context)
actual = engine.render(template, context)
......@@ -204,6 +204,27 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': '{{#hello}}'}
self._assert_render(u'{{#hello}}', template, context)
## Test interpolation with "falsey" values
#
# In these test cases, we test the part of the spec that says that
# "data should be coerced into a string (and escaped, if appropriate)
# before interpolation." We test this for data that is "falsey."
def test_interpolation__falsey__zero(self):
template = '{{.}}'
context = 0
self._assert_render(u'0', template, context)
def test_interpolation__falsey__none(self):
template = '{{.}}'
context = None
self._assert_render(u'None', template, context)
def test_interpolation__falsey__zero(self):
template = '{{.}}'
context = False
self._assert_render(u'False', template, context)
# Built-in types:
#
# Confirm that we not treat instances of built-in types as objects,
......@@ -480,6 +501,56 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
self._assert_render(u'Hi Mom :)', template, context)
def test_section__lambda__list(self):
"""
Check that lists of lambdas are processed correctly for sections.
This test case is equivalent to a test submitted to the Mustache spec here:
https://github.com/mustache/spec/pull/47 .
"""
template = '<{{#lambdas}}foo{{/lambdas}}>'
context = {'foo': 'bar',
'lambdas': [lambda text: "~{{%s}}~" % text,
lambda text: "#{{%s}}#" % text]}
self._assert_render(u'<~bar~#bar#>', template, context)
def test_section__lambda__not_on_context_stack(self):
"""
Check that section lambdas are not pushed onto the context stack.
Even though the sections spec says that section data values should be
pushed onto the context stack prior to rendering, this does not apply
to lambdas. Lambdas obey their own special case.
This test case is equivalent to a test submitted to the Mustache spec here:
https://github.com/mustache/spec/pull/47 .
"""
context = {'foo': 'bar', 'lambda': (lambda text: "{{.}}")}
template = '{{#foo}}{{#lambda}}blah{{/lambda}}{{/foo}}'
self._assert_render(u'bar', template, context)
def test_section__lambda__no_reinterpolation(self):
"""
Check that section lambda return values are not re-interpolated.
This test is a sanity check that the rendered lambda return value
is not re-interpolated as could be construed by reading the
section part of the Mustache spec.
This test case is equivalent to a test submitted to the Mustache spec here:
https://github.com/mustache/spec/pull/47 .
"""
template = '{{#planet}}{{#lambda}}dot{{/lambda}}{{/planet}}'
context = {'planet': 'Earth', 'dot': '~{{.}}~', 'lambda': (lambda text: "#{{%s}}#" % text)}
self._assert_render(u'#~{{.}}~#', template, context)
def test_comment__multiline(self):
"""
Check that multiline comments are permitted.
......@@ -509,3 +580,78 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
expected = u' {{foo}} '
self._assert_render(expected, '{{=$ $=}} {{foo}} ')
self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '.
def test_dot_notation(self):
"""
Test simple dot notation cases.
Check that we can use dot notation when the variable is a dict,
user-defined object, or combination of both.
"""
template = 'Hello, {{person.name}}. I see you are {{person.details.age}}.'
person = Attachable(name='Biggles', details={'age': 42})
context = {'person': person}
self._assert_render(u'Hello, Biggles. I see you are 42.', template, context)
def test_dot_notation__missing_attributes_or_keys(self):
"""
Test dot notation with missing keys or attributes.
Check that if a key or attribute in a dotted name does not exist, then
the tag renders as the empty string.
"""
template = """I cannot see {{person.name}}'s age: {{person.age}}.
Nor {{other_person.name}}'s: ."""
expected = u"""I cannot see Biggles's age: .
Nor Mr. Bradshaw's: ."""
context = {'person': {'name': 'Biggles'},
'other_person': Attachable(name='Mr. Bradshaw')}
self._assert_render(expected, template, context)
def test_dot_notation__multiple_levels(self):
"""
Test dot notation with multiple levels.
"""
template = """Hello, Mr. {{person.name.lastname}}.
I see you're back from {{person.travels.last.country.city}}.
I'm missing some of your details: {{person.details.private.editor}}."""
expected = u"""Hello, Mr. Pither.
I see you're back from Cornwall.
I'm missing some of your details: ."""
context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'},
'travels': {'last': {'country': {'city': 'Cornwall'}}},
'details': {'public': 'likes cycling'}}}
self._assert_render(expected, template, context)
# It should also work with user-defined objects
context = {'person': Attachable(name={'firstname': 'unknown', 'lastname': 'Pither'},
travels=Attachable(last=Attachable(country=Attachable(city='Cornwall'))),
details=Attachable())}
self._assert_render(expected, template, context)
def test_dot_notation__missing_part_terminates_search(self):
"""
Test that dotted name resolution terminates on a later part not found.
Check that if a later dotted name part is not found in the result from
the former resolution, then name resolution terminates rather than
starting the search over with the next element of the context stack.
From the spec (interpolation section)--
5) If any name parts were retained in step 1, each should be resolved
against a context stack containing only the result from the former
resolution. If any part fails resolution, the result should be considered
falsey, and should interpolate as the empty string.
This test case is equivalent to the test case in the following pull
request:
https://github.com/mustache/spec/pull/48
"""
template = '{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})'
context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} }
self._assert_render(u'A.B :: (A :: )', template, context)
......@@ -13,9 +13,10 @@ import unittest
from examples.simple import Simple
from pystache import Renderer
from pystache import TemplateSpec
from pystache.common import TemplateNotFoundError
from pystache.loader import Loader
from pystache.tests.common import get_data_path, AssertStringMixin
from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin
from pystache.tests.data.views import SayHello
......@@ -405,7 +406,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
# 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):
class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
"""
Check the RenderEngine returned by Renderer._make_render_engine().
......@@ -444,7 +445,20 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
self.assertEqual(actual, "abc")
self.assertEqual(type(actual), unicode)
def test__load_partial__not_found(self):
def test__load_partial__not_found__default(self):
"""
Check that load_partial provides a nice message when a template is not found.
"""
renderer = Renderer()
engine = renderer._make_render_engine()
load_partial = engine.load_partial
self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']",
load_partial, "foo")
def test__load_partial__not_found__dict(self):
"""
Check that load_partial provides a nice message when a template is not found.
......@@ -455,11 +469,10 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine()
load_partial = engine.load_partial
try:
load_partial("foo")
raise Exception("Shouldn't get here")
except Exception, err:
self.assertEqual(str(err), "Partial not found with name: 'foo'")
# Include dict directly since str(dict) is different in Python 2 and 3:
# <type 'dict'> versus <class 'dict'>, respectively.
self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict,
load_partial, "foo")
## Test the engine's literal attribute.
......
......@@ -16,6 +16,7 @@ from examples.lambdas import Lambdas
from examples.inverted import Inverted, InvertedLists
from pystache import Renderer
from pystache import TemplateSpec
from pystache.common import TemplateNotFoundError
from pystache.locator import Locator
from pystache.loader import Loader
from pystache.specloader import SpecLoader
......@@ -42,7 +43,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
view = Tagless()
renderer = Renderer()
self.assertRaises(IOError, renderer.render, view)
self.assertRaises(TemplateNotFoundError, renderer.render, view)
# TODO: change this test to remove the following brittle line.
view.template_rel_directory = "examples"
......@@ -60,7 +61,8 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer1 = Renderer()
renderer2 = Renderer(search_dirs=EXAMPLES_DIR)
self.assertRaises(IOError, renderer1.render, spec)
actual = renderer1.render(spec)
self.assertString(actual, u"Partial: ")
actual = renderer2.render(spec)
self.assertEqual(actual, "Partial: No tags...")
......
......@@ -4,19 +4,21 @@
"""
This script supports publishing Pystache to PyPI.
Below are instructions to pystache maintainers on how to push a new
version of pystache to PyPI--
This docstring contains instructions to Pystache maintainers on how
to release a new version of Pystache.
(1) Push to PyPI. To release 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.
create a PyPI user account if you do not already have one. 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):
merging to master, bumping the version number in setup.py, etc):
> python setup.py publish
python setup.py publish
If you get an error like the following--
......@@ -33,6 +35,20 @@ as described here, for example:
http://docs.python.org/release/2.5.2/dist/pypirc.html
(2) Tag the release on GitHub. Here are some commands for tagging.
List current tags:
git tag -l -n3
Create an annotated tag:
git tag -a -m "Version 0.5.1" "v0.5.1"
Push a tag to GitHub:
git push --tags defunkt v0.5.1
"""
import os
......@@ -56,7 +72,7 @@ else:
# print("Using: version %s of %s" % (repr(dist.__version__), repr(dist)))
VERSION = '0.5.1' # Also change in pystache/__init__.py.
VERSION = '0.5.2-rc' # Also change in pystache/__init__.py.
HISTORY_PATH = 'HISTORY.rst'
LICENSE_PATH = 'LICENSE'
......
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