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
# coding: utf-8
"""
Defines a Context class to represent mustache(5)'s notion of context.
Exposes a ContextStack class.
The Mustache spec makes a special distinction between two types of context
stack elements: hashes and objects. For the purposes of interpreting the
spec, we define these categories mutually exclusively as follows:
(1) Hash: an item whose type is a subclass of dict.
(2) Object: an item that is neither a hash nor an instance of a
built-in type.
"""
......@@ -22,28 +31,23 @@ class NotFound(object):
_NOT_FOUND = NotFound()
# TODO: share code with template.check_callable().
def _is_callable(obj):
return hasattr(obj, '__call__')
def _get_value(item, key):
def _get_value(context, key):
"""
Retrieve a key's value from an item.
Retrieve a key's value from a context item.
Returns _NOT_FOUND if the key does not exist.
The Context.get() docstring documents this function's intended behavior.
The ContextStack.get() docstring documents this function's intended behavior.
"""
if isinstance(item, dict):
if isinstance(context, dict):
# Then we consider the argument a "hash" for the purposes of the spec.
#
# We do a membership test to avoid using exceptions for flow control
# (e.g. catching KeyError).
if key in item:
return item[key]
elif type(item).__module__ != _BUILTIN_MODULE:
if key in context:
return context[key]
elif type(context).__module__ != _BUILTIN_MODULE:
# Then we consider the argument an "object" for the purposes of
# the spec.
#
......@@ -51,16 +55,18 @@ def _get_value(item, key):
# 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):
if hasattr(context, key):
attr = getattr(context, key)
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if callable(attr):
return attr()
return attr
return _NOT_FOUND
class Context(object):
class ContextStack(object):
"""
Provides dictionary-like access to a stack of zero or more items.
......@@ -75,7 +81,7 @@ class Context(object):
(last in, first out).
Caution: this class does not currently support recursive nesting in
that items in the stack cannot themselves be Context instances.
that items in the stack cannot themselves be ContextStack instances.
See the docstrings of the methods of this class for more details.
......@@ -92,7 +98,7 @@ class Context(object):
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
Caution: items should not themselves be ContextStack instances, as
recursive nesting does not behave as one might expect.
"""
......@@ -104,9 +110,9 @@ class Context(object):
For example--
>>> context = Context({'alpha': 'abc'}, {'numeric': 123})
>>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
>>> repr(context)
"Context({'alpha': 'abc'}, {'numeric': 123})"
"ContextStack({'alpha': 'abc'}, {'numeric': 123})"
"""
return "%s%s" % (self.__class__.__name__, tuple(self._stack))
......@@ -114,18 +120,18 @@ class Context(object):
@staticmethod
def create(*context, **kwargs):
"""
Build a Context instance from a sequence of context-like items.
Build a ContextStack instance from a sequence of context-like items.
This factory-style method is more general than the Context class's
This factory-style method is more general than the ContextStack class's
constructor in that, unlike the constructor, the argument list
can itself contain Context instances.
can itself contain ContextStack instances.
Here is an example illustrating various aspects of this method:
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
>>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'})
>>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
>>>
>>> context = Context.create(obj1, None, obj2, mineral='gold')
>>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
>>>
>>> context.get('animal')
'cat'
......@@ -136,7 +142,7 @@ class Context(object):
Arguments:
*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 will be skipped. Items in the *context list are
added to the stack in order so that later items in the argument
......@@ -152,12 +158,12 @@ class Context(object):
"""
items = context
context = Context()
context = ContextStack()
for item in items:
if item is None:
continue
if isinstance(item, Context):
if isinstance(item, ContextStack):
context._stack.extend(item._stack)
else:
context.push(item)
......@@ -167,9 +173,22 @@ class Context(object):
return context
def get(self, key, default=None):
# TODO: add more unit tests for this.
# TODO: update the docstring for dotted names.
def get(self, name, default=u''):
"""
Query the stack for the given key, and return the resulting value.
Resolve a dotted name against the current context stack.
This function follows the rules outlined in the section of the
spec regarding tag interpolation. This function returns the value
as is and does not coerce the return value to a string.
Arguments:
name: a dotted or non-dotted name.
default: the value to return if name resolution fails at any point.
Defaults to the empty string per the Mustache spec.
This method queries items in the stack in order from last-added
objects to first (last in, first out). The value returned is
......@@ -177,30 +196,21 @@ class Context(object):
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.
stack for a key differently depending on whether the item is a
hash, object, or neither (as defined in the module docstring):
(1) Hash: if the item is a hash, then the key's value is the
dictionary value of the key. If the dictionary doesn't contain
the key, then the key is considered not found.
(2) Object: if the item is an an object, then the method looks for
an attribute with the same name as the key. If an attribute
with that name exists, the value of the attribute is returned.
If the attribute is callable, however (i.e. if the attribute
is a method), then the attribute is called with no arguments
and that value is returned. If there is no attribute with
the same name as the key, then the key is considered not found.
(3) Neither: if the item is neither a hash nor an object, then
the key is considered not found.
......@@ -226,23 +236,59 @@ class Context(object):
>>>
>>> dct['greet'] is obj.greet
True
>>> Context(dct).get('greet') #doctest: +ELLIPSIS
>>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
>>> Context(obj).get('greet')
>>> ContextStack(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:
if name == '.':
# TODO: should we add a test case for an empty context stack?
return self.top()
parts = name.split('.')
result = self._get_simple(parts[0])
for part in parts[1:]:
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if result is _NOT_FOUND:
break
# The full context stack is not used to resolve the remaining parts.
# From the spec--
#
# 5) If any name parts were retained in step 1, each should be
# resolved against a context stack containing only the result
# from the former resolution. If any part fails resolution, the
# result should be considered falsey, and should interpolate as
# the empty string.
#
# TODO: make sure we have a test case for the above point.
result = _get_value(result, part)
if result is _NOT_FOUND:
return default
return result
def _get_simple(self, name):
"""
Query the stack for a non-dotted name.
"""
result = _NOT_FOUND
for item in reversed(self._stack):
result = _get_value(item, name)
if result is _NOT_FOUND:
continue
# Otherwise, the key was found.
return val
# Otherwise, no item in the stack contained the key.
break
return default
return result
def push(self, item):
"""
......@@ -270,4 +316,4 @@ class Context(object):
Return a copy of this instance.
"""
return Context(*self._stack)
return ContextStack(*self._stack)
......@@ -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)
......
......@@ -10,8 +10,8 @@ import unittest
from pystache.context import _NOT_FOUND
from pystache.context import _get_value
from pystache.context import Context
from pystache.tests.common import AssertIsMixin
from pystache.context import ContextStack
from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable
class SimpleObject(object):
......@@ -204,10 +204,10 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertNotFound(item2, 'pop')
class ContextTests(unittest.TestCase, AssertIsMixin):
class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
Test the Context class.
Test the ContextStack class.
"""
......@@ -216,34 +216,34 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Check that passing nothing to __init__() raises no exception.
"""
context = Context()
context = ContextStack()
def test_init__many_elements(self):
"""
Check that passing more than two items to __init__() raises no exception.
"""
context = Context({}, {}, {})
context = ContextStack({}, {}, {})
def test__repr(self):
context = Context()
self.assertEqual(repr(context), 'Context()')
context = ContextStack()
self.assertEqual(repr(context), 'ContextStack()')
context = Context({'foo': 'bar'})
self.assertEqual(repr(context), "Context({'foo': 'bar'},)")
context = ContextStack({'foo': 'bar'})
self.assertEqual(repr(context), "ContextStack({'foo': 'bar'},)")
context = Context({'foo': 'bar'}, {'abc': 123})
self.assertEqual(repr(context), "Context({'foo': 'bar'}, {'abc': 123})")
context = ContextStack({'foo': 'bar'}, {'abc': 123})
self.assertEqual(repr(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
def test__str(self):
context = Context()
self.assertEqual(str(context), 'Context()')
context = ContextStack()
self.assertEqual(str(context), 'ContextStack()')
context = Context({'foo': 'bar'})
self.assertEqual(str(context), "Context({'foo': 'bar'},)")
context = ContextStack({'foo': 'bar'})
self.assertEqual(str(context), "ContextStack({'foo': 'bar'},)")
context = Context({'foo': 'bar'}, {'abc': 123})
self.assertEqual(str(context), "Context({'foo': 'bar'}, {'abc': 123})")
context = ContextStack({'foo': 'bar'}, {'abc': 123})
self.assertEqual(str(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
## Test the static create() method.
......@@ -252,7 +252,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing a dictionary.
"""
context = Context.create({'foo': 'bar'})
context = ContextStack.create({'foo': 'bar'})
self.assertEqual(context.get('foo'), 'bar')
def test_create__none(self):
......@@ -260,7 +260,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing None.
"""
context = Context.create({'foo': 'bar'}, None)
context = ContextStack.create({'foo': 'bar'}, None)
self.assertEqual(context.get('foo'), 'bar')
def test_create__object(self):
......@@ -270,16 +270,16 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
class Foo(object):
foo = 'bar'
context = Context.create(Foo())
context = ContextStack.create(Foo())
self.assertEqual(context.get('foo'), 'bar')
def test_create__context(self):
"""
Test passing a Context instance.
Test passing a ContextStack instance.
"""
obj = Context({'foo': 'bar'})
context = Context.create(obj)
obj = ContextStack({'foo': 'bar'})
context = ContextStack.create(obj)
self.assertEqual(context.get('foo'), 'bar')
def test_create__kwarg(self):
......@@ -287,7 +287,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing a keyword argument.
"""
context = Context.create(foo='bar')
context = ContextStack.create(foo='bar')
self.assertEqual(context.get('foo'), 'bar')
def test_create__precedence_positional(self):
......@@ -295,7 +295,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test precedence of positional arguments.
"""
context = Context.create({'foo': 'bar'}, {'foo': 'buzz'})
context = ContextStack.create({'foo': 'bar'}, {'foo': 'buzz'})
self.assertEqual(context.get('foo'), 'buzz')
def test_create__precedence_keyword(self):
......@@ -303,7 +303,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test precedence of keyword arguments.
"""
context = Context.create({'foo': 'bar'}, foo='buzz')
context = ContextStack.create({'foo': 'bar'}, foo='buzz')
self.assertEqual(context.get('foo'), 'buzz')
def test_get__key_present(self):
......@@ -311,7 +311,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test getting a key.
"""
context = Context({"foo": "bar"})
context = ContextStack({"foo": "bar"})
self.assertEqual(context.get("foo"), "bar")
def test_get__key_missing(self):
......@@ -319,15 +319,15 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test getting a missing key.
"""
context = Context()
self.assertTrue(context.get("foo") is None)
context = ContextStack()
self.assertString(context.get("foo"), u'')
def test_get__default(self):
"""
Test that get() respects the default value.
"""
context = Context()
context = ContextStack()
self.assertEqual(context.get("foo", "bar"), "bar")
def test_get__precedence(self):
......@@ -335,7 +335,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test that get() respects the order of precedence (later items first).
"""
context = Context({"foo": "bar"}, {"foo": "buzz"})
context = ContextStack({"foo": "bar"}, {"foo": "buzz"})
self.assertEqual(context.get("foo"), "buzz")
def test_get__fallback(self):
......@@ -343,7 +343,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Check that first-added stack items are queried on context misses.
"""
context = Context({"fuzz": "buzz"}, {"foo": "bar"})
context = ContextStack({"fuzz": "buzz"}, {"foo": "bar"})
self.assertEqual(context.get("fuzz"), "buzz")
def test_push(self):
......@@ -352,7 +352,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
key = "foo"
context = Context({key: "bar"})
context = ContextStack({key: "bar"})
self.assertEqual(context.get(key), "bar")
context.push({key: "buzz"})
......@@ -364,7 +364,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
key = "foo"
context = Context({key: "bar"}, {key: "buzz"})
context = ContextStack({key: "bar"}, {key: "buzz"})
self.assertEqual(context.get(key), "buzz")
item = context.pop()
......@@ -373,7 +373,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
def test_top(self):
key = "foo"
context = Context({key: "bar"}, {key: "buzz"})
context = ContextStack({key: "bar"}, {key: "buzz"})
self.assertEqual(context.get(key), "buzz")
top = context.top()
......@@ -383,7 +383,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
def test_copy(self):
key = "foo"
original = Context({key: "bar"}, {key: "buzz"})
original = ContextStack({key: "bar"}, {key: "buzz"})
self.assertEqual(original.get(key), "buzz")
new = original.copy()
......@@ -395,3 +395,76 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
# Confirm the original is unchanged.
self.assertEqual(original.get(key), "buzz")
def test_dot_notation__dict(self):
name = "foo.bar"
stack = ContextStack({"foo": {"bar": "baz"}})
self.assertEqual(stack.get(name), "baz")
# Works all the way down
name = "a.b.c.d.e.f.g"
stack = ContextStack({"a": {"b": {"c": {"d": {"e": {"f": {"g": "w00t!"}}}}}}})
self.assertEqual(stack.get(name), "w00t!")
def test_dot_notation__user_object(self):
name = "foo.bar"
stack = ContextStack({"foo": Attachable(bar="baz")})
self.assertEquals(stack.get(name), "baz")
# Works on multiple levels, too
name = "a.b.c.d.e.f.g"
A = Attachable
stack = ContextStack({"a": A(b=A(c=A(d=A(e=A(f=A(g="w00t!"))))))})
self.assertEquals(stack.get(name), "w00t!")
def test_dot_notation__mixed_dict_and_obj(self):
name = "foo.bar.baz.bak"
stack = ContextStack({"foo": Attachable(bar={"baz": Attachable(bak=42)})})
self.assertEquals(stack.get(name), 42)
def test_dot_notation__missing_attr_or_key(self):
name = "foo.bar.baz.bak"
stack = ContextStack({"foo": {"bar": {}}})
self.assertString(stack.get(name), u'')
stack = ContextStack({"foo": Attachable(bar=Attachable())})
self.assertString(stack.get(name), u'')
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
"""
stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'})
self.assertEqual(stack.get('a'), 'A')
self.assertString(stack.get('a.b'), u'')
stack.pop()
self.assertEqual(stack.get('a.b'), 'A.B')
def test_dot_notation__autocall(self):
name = "foo.bar.baz"
# When any element in the path is callable, it should be automatically invoked
stack = ContextStack({"foo": Attachable(bar=Attachable(baz=lambda: "Called!"))})
self.assertEquals(stack.get(name), "Called!")
class Foo(object):
def bar(self):
return Attachable(baz='Baz')
stack = ContextStack({"foo": Foo()})
self.assertEquals(stack.get(name), "Baz")
......@@ -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