Commit 012bdba1 by Rodrigo Bernardo Pimentel

Merge branch 'development' of https://github.com/defunkt/pystache into development

Conflicts:
	pystache/context.py
	pystache/tests/test_context.py
	pystache/tests/test_renderengine.py
	tests/common.py
parents 544b7a35 54eb4b50
*.pyc *.pyc
.DS_Store
# Tox support. See: http://pypi.python.org/pypi/tox
.tox
# Our tox runs convert the doctests in *.rst files to Python 3 prior to
# running tests. Ignore these temporary files.
*.temp2to3.rst
# TextMate project file
*.tmproj
# Distribution-related folders and files.
build build
MANIFEST
dist dist
MANIFEST
.DS_Store pystache.egg-info
\ No newline at end of file
History History
======= =======
0.6.0 (TBD)
-----------
* Bugfix: falsey values now coerced to strings using str().
* Bugfix: section-lambda return values no longer pushed onto context stack.
0.5.1 (2012-04-24)
------------------
* Added support for Python 3.1 and 3.2.
* Added tox support to test multiple Python versions.
* Added test script entry point: pystache-test.
* Added __version__ package attribute.
* Test harness now supports both YAML and JSON forms of Mustache spec.
* Test harness no longer requires nose.
0.5.0 (2012-04-03) 0.5.0 (2012-04-03)
------------------ ------------------
...@@ -93,5 +109,6 @@ Bug fixes: ...@@ -93,5 +109,6 @@ Bug fixes:
* First release * First release
.. _2to3: http://docs.python.org/library/2to3.html
.. _issue #13: https://github.com/defunkt/pystache/issues/13 .. _issue #13: https://github.com/defunkt/pystache/issues/13
.. _Mustache spec: https://github.com/mustache/spec .. _Mustache spec: https://github.com/mustache/spec
include LICENSE include LICENSE
include HISTORY.rst README.rst include HISTORY.rst
include README.rst
include tox.ini
include test_pystache.py
# You cannot use package_data, for example, to include data files in a
# source distribution when using Distribute.
recursive-include pystache/tests *.mustache *.txt
...@@ -23,18 +23,24 @@ Logo: `David Phillips`_ ...@@ -23,18 +23,24 @@ Logo: `David Phillips`_
Requirements 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.4 (requires simplejson `version 2.0.9`_ or earlier)
* Python 2.5 (requires simplejson) * Python 2.5 (requires simplejson_)
* Python 2.6 * Python 2.6
* Python 2.7 * Python 2.7
* Python 3.1
* Python 3.2
JSON support is needed only for the command-line interface and to run the 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 spec tests. We require simplejson for earlier versions of Python since
simplejson_ package works with earlier versions of Python. Because Python's json_ module was added in Python 2.6.
simplejson stopped officially supporting Python 2.4 as of version 2.1.0,
Python 2.4 requires an earlier version. For Python 2.4 we require an earlier version of simplejson since simplejson
stopped officially supporting Python 2.4 in simplejson version 2.1.0.
Earlier versions of simplejson can be installed manually, as follows: ::
pip install 'simplejson<2.1.0'
Install It Install It
...@@ -43,6 +49,9 @@ Install It ...@@ -43,6 +49,9 @@ Install It
:: ::
pip install pystache pip install pystache
pystache-test
To install and test from source (e.g. from GitHub), see the Develop section.
Use It Use It
...@@ -51,8 +60,8 @@ Use It ...@@ -51,8 +60,8 @@ Use It
:: ::
>>> import pystache >>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'}) >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
u'Hi Mom!' Hi Mom!
You can also create dedicated view classes to hold your view logic. You can also create dedicated view classes to hold your view logic.
...@@ -65,44 +74,68 @@ Here's your view class (in examples/readme.py):: ...@@ -65,44 +74,68 @@ Here's your view class (in examples/readme.py)::
Like so:: Like so::
>>> from examples.readme import SayHello >>> from pystache.tests.examples.readme import SayHello
>>> hello = SayHello() >>> hello = SayHello()
Then your template, say_hello.mustache:: Then your template, say_hello.mustache (in the same directory by default
as your class definition)::
Hello, {{to}}! Hello, {{to}}!
Pull it together:: Pull it together::
>>> renderer = pystache.Renderer() >>> renderer = pystache.Renderer()
>>> renderer.render(hello) >>> print renderer.render(hello)
u'Hello, Pizza!' Hello, Pizza!
For greater control over rendering (e.g. to specify a custom template directory),
use the ``Renderer`` class directly. One can pass attributes to the class's
constructor or set them on an instance.
To customize template loading on a per-view basis, subclass ``TemplateSpec``.
See the docstrings of the Renderer_ class and TemplateSpec_ class for
more information.
Python 3
========
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
``html.escape()`` does escape single quotes.
* In both Python 2 and 3, the string and file encodings default to
``sys.getdefaultencoding()``. However, this function can return different
values under Python 2 and 3, even when run from the same system. Check
your own system for the behavior on your system, or do not rely on the
defaults by passing in the encodings explicitly (e.g. to the ``Renderer`` class).
Unicode Handling
================
This section describes Pystache's handling of unicode (e.g. strings and Unicode
encodings). =======
This section describes how Pystache handles unicode, strings, and encodings.
Internally, Pystache uses `only unicode strings`_. For input, Pystache accepts Internally, Pystache uses `only unicode strings`_ (``str`` in Python 3 and
both ``unicode`` and ``str`` strings. For output, Pystache's template ``unicode`` in Python 2). For input, Pystache accepts both unicode strings
rendering methods return only unicode. and byte strings (``bytes`` in Python 3 and ``str`` in Python 2). For output,
Pystache's template rendering methods return only unicode.
Pystache's ``Renderer`` class supports a number of attributes that control how Pystache's ``Renderer`` class supports a number of attributes to control how
Pystache converts ``str`` strings to unicode on input. These include the Pystache converts byte strings to unicode on input. These include the
``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes. ``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes.
The ``file_encoding`` attribute is the encoding the renderer uses to convert The ``file_encoding`` attribute is the encoding the renderer uses to convert
to unicode any files read from the file system. Similarly, ``string_encoding`` 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 is the encoding the renderer uses to convert any other byte strings encountered
type ``str`` encountered during the rendering process (e.g. context values during the rendering process into unicode (e.g. context values that are
of type ``str``). encoded byte strings).
The ``decode_errors`` attribute is what the renderer passes as the ``errors`` The ``decode_errors`` attribute is what the renderer passes as the ``errors``
argument to Python's `built-in unicode function`_ ``unicode()`` when argument to Python's built-in unicode-decoding function (``str()`` in Python 3
converting. The valid values for this argument are ``strict``, ``ignore``, and ``unicode()`` in Python 2). The valid values for this argument are
and ``replace``. ``strict``, ``ignore``, and ``replace``.
Each of these attributes can be set via the ``Renderer`` class's constructor 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 using a keyword argument of the same name. See the Renderer class's
...@@ -112,63 +145,77 @@ attribute can be controlled on a per-view basis by subclassing the ...@@ -112,63 +145,77 @@ attribute can be controlled on a per-view basis by subclassing the
default to values set in Pystache's ``defaults`` module. default to values set in Pystache's ``defaults`` module.
Test It Develop
======= =======
nose_ works great! :: To test from a source distribution (without installing)-- ::
pip install nose python test_pystache.py
cd pystache
nosetests To test Pystache with multiple versions of Python (with a single command!),
you can use tox_: ::
pip install tox
tox
Depending on your Python version and nose installation, you may need If you do not have all Python versions listed in ``tox.ini``-- ::
to type, for example ::
nosetests-2.4 tox -e py26,py32 # for example
To include tests from the Mustache spec in your test runs: :: The source distribution tests also include doctests and tests from the
Mustache spec. To include tests from the Mustache spec in your test runs: ::
git submodule init git submodule init
git submodule update git submodule update
To run all available tests (including doctests):: The test harness parses the spec's (more human-readable) yaml files if PyYAML_
is present. Otherwise, it parses the json files. To install PyYAML-- ::
nosetests --with-doctest --doctest-extension=rst pip install pyyaml
or alternatively (using setup.cfg):: To run a subset of the tests, you can use nose_: ::
python setup.py nosetests pip install nose
nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
To run a subset of the tests, you can use this pattern, for example: :: **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.
nosetests --tests tests/test_context.py:GetValueTests.test_dictionary__key_present 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.
Mailing List Mailing List
============ ============
As of November 2011, there's a mailing list, pystache@librelist.com. 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.
Archive: http://librelist.com/browser/pystache/
Note: There's a bit of a delay in seeing the latest emails appear
in the archive.
Authors
Author =======
======
:: ::
>>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' } >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' }
>>> pystache.render("{{author}} :: {{email}}", context) >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context)
u'Chris Wanstrath :: chris@ozmm.org' Author: Chris Wanstrath
Maintainer: Chris Jerdonek
.. _2to3: http://docs.python.org/library/2to3.html
.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode
.. _ctemplate: http://code.google.com/p/google-ctemplate/ .. _ctemplate: http://code.google.com/p/google-ctemplate/
.. _David Phillips: http://davidphillips.us/ .. _David Phillips: http://davidphillips.us/
.. _Distribute: http://pypi.python.org/pypi/distribute
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _json: http://docs.python.org/library/json.html .. _json: http://docs.python.org/library/json.html
.. _mailing list: http://librelist.com/browser/pystache/
.. _Mustache: http://mustache.github.com/ .. _Mustache: http://mustache.github.com/
.. _Mustache spec: https://github.com/mustache/spec .. _Mustache spec: https://github.com/mustache/spec
.. _mustache(5): http://mustache.github.com/mustache.5.html .. _mustache(5): http://mustache.github.com/mustache.5.html
...@@ -176,7 +223,12 @@ Author ...@@ -176,7 +223,12 @@ Author
.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs .. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
.. _PyPI: http://pypi.python.org/pypi/pystache .. _PyPI: http://pypi.python.org/pypi/pystache
.. _Pystache: https://github.com/defunkt/pystache .. _Pystache: https://github.com/defunkt/pystache
.. _PyYAML: http://pypi.python.org/pypi/PyYAML
.. _Renderer: https://github.com/defunkt/pystache/blob/master/pystache/renderer.py
.. _semantically versioned: http://semver.org .. _semantically versioned: http://semver.org
.. _simplejson: http://pypi.python.org/pypi/simplejson/ .. _simplejson: http://pypi.python.org/pypi/simplejson/
.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode .. _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.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7
.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
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 command parsing to pystache-test doesn't break with Python 2.4 and earlier.
* Combine pystache-test with the main command.
"""
TODO: add a docstring.
"""
# We keep all initialization code in a separate module. # We keep all initialization code in a separate module.
from init import *
from pystache.init import render, Renderer, TemplateSpec
__all__ = ['render', 'Renderer', 'TemplateSpec']
__version__ = '0.5.1' # Also change in setup.py.
"""
TODO: add a docstring.
"""
...@@ -13,7 +13,16 @@ try: ...@@ -13,7 +13,16 @@ try:
except: except:
# The json module is new in Python 2.6, whereas simplejson is # The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions. # compatible with earlier versions.
import simplejson as json try:
import simplejson as json
except ImportError:
# Raise an error with a type different from ImportError as a hack around
# this issue:
# http://bugs.python.org/issue7559
from sys import exc_info
ex_type, ex_value, tb = exc_info()
new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
raise new_ex.__class__, new_ex, tb
# The optparse module is deprecated in Python 2.7 in favor of argparse. # The optparse module is deprecated in Python 2.7 in favor of argparse.
# However, argparse is not available in Python 2.6 and earlier. # However, argparse is not available in Python 2.6 and earlier.
...@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage): ...@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage):
return template, context return template, context
def main(sys_argv): # TODO: verify whether the setup() method's entry_points argument
# supports passing arguments to main:
#
# http://packages.python.org/distribute/setuptools.html#automatic-script-creation
#
def main(sys_argv=sys.argv):
template, context = parse_args(sys_argv, USAGE) template, context = parse_args(sys_argv, USAGE)
if template.endswith('.mustache'): if template.endswith('.mustache'):
...@@ -77,5 +91,4 @@ def main(sys_argv): ...@@ -77,5 +91,4 @@ def main(sys_argv):
if __name__=='__main__': if __name__=='__main__':
main(sys.argv) main()
# coding: utf-8
"""
This module provides a command to test pystache (unit tests, doctests, etc).
"""
import sys
from pystache.tests.main import main as run_tests
def main(sys_argv=sys.argv):
run_tests(sys_argv=sys_argv)
if __name__=='__main__':
main()
# coding: utf-8
"""
Exposes common functions.
"""
# This function was designed to be portable across Python versions -- both
# with older versions and with Python 3 after applying 2to3.
def read(path):
"""
Return the contents of a text file as a byte string.
"""
# Opening in binary mode is necessary for compatibility across Python
# 2 and 3. In both Python 2 and 3, open() defaults to opening files in
# text mode. However, in Python 2, open() returns file objects whose
# read() method returns byte strings (strings of type `str` in Python 2),
# whereas in Python 3, the file object returns unicode strings (strings
# of type `str` in Python 3).
f = open(path, 'rb')
# We avoid use of the with keyword for Python 2.4 support.
try:
return f.read()
finally:
f.close()
# coding: utf-8 # coding: utf-8
""" """
Defines a Context class to represent mustache(5)'s notion of context. Exposes a ContextStack class and functions to retrieve names from context.
""" """
class NotFound(object): pass # This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
_BUILTIN_MODULE = type(0).__module__
# We use this private global variable as a return value to represent a key # 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 # 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 -- # 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. # without having to rely on exceptions (e.g. KeyError) for flow control.
#
# TODO: eliminate the need for a private global variable, e.g. by using the
# preferred Python approach of "easier to ask for forgiveness than permission":
# http://docs.python.org/glossary.html#term-eafp
class NotFound(object):
pass
_NOT_FOUND = NotFound() _NOT_FOUND = NotFound()
...@@ -24,7 +33,7 @@ def _get_value(item, key): ...@@ -24,7 +33,7 @@ def _get_value(item, key):
Returns _NOT_FOUND if the key does not exist. 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.
""" """
parts = key.split('.') parts = key.split('.')
...@@ -39,7 +48,7 @@ def _get_value(item, key): ...@@ -39,7 +48,7 @@ def _get_value(item, key):
# (e.g. catching KeyError). # (e.g. catching KeyError).
if key in item: if key in item:
value = item[key] value = item[key]
elif type(item).__module__ != '__builtin__': elif type(item).__module__ != _BUILTIN_MODULE:
# Then we consider the argument an "object" for the purposes of # Then we consider the argument an "object" for the purposes of
# the spec. # the spec.
# #
...@@ -60,7 +69,26 @@ def _get_value(item, key): ...@@ -60,7 +69,26 @@ def _get_value(item, key):
return value return value
class Context(object): # TODO: add some unit tests for this.
def resolve(context, name):
"""
Resolve the given name against the given context stack.
This function follows the rules outlined in the section of the spec
regarding tag interpolation.
This function does not coerce the return value to a string.
"""
if name == '.':
return context.top()
# The spec says that if the name fails resolution, the result should be
# considered falsey, and should interpolate as the empty string.
return context.get(name, '')
class ContextStack(object):
""" """
Provides dictionary-like access to a stack of zero or more items. Provides dictionary-like access to a stack of zero or more items.
...@@ -75,7 +103,7 @@ class Context(object): ...@@ -75,7 +103,7 @@ class Context(object):
(last in, first out). (last in, first out).
Caution: this class does not currently support recursive nesting in 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. See the docstrings of the methods of this class for more details.
...@@ -92,7 +120,7 @@ class Context(object): ...@@ -92,7 +120,7 @@ class Context(object):
stack in order so that, in particular, items at the end of stack in order so that, in particular, items at the end of
the argument list are queried first when querying the stack. 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. recursive nesting does not behave as one might expect.
""" """
...@@ -104,9 +132,9 @@ class Context(object): ...@@ -104,9 +132,9 @@ class Context(object):
For example-- For example--
>>> context = Context({'alpha': 'abc'}, {'numeric': 123}) >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
>>> repr(context) >>> repr(context)
"Context({'alpha': 'abc'}, {'numeric': 123})" "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
""" """
return "%s%s" % (self.__class__.__name__, tuple(self._stack)) return "%s%s" % (self.__class__.__name__, tuple(self._stack))
...@@ -114,18 +142,18 @@ class Context(object): ...@@ -114,18 +142,18 @@ class Context(object):
@staticmethod @staticmethod
def create(*context, **kwargs): 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 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: Here is an example illustrating various aspects of this method:
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'} >>> 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') >>> context.get('animal')
'cat' 'cat'
...@@ -136,7 +164,7 @@ class Context(object): ...@@ -136,7 +164,7 @@ class Context(object):
Arguments: 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 with which to populate the initial context stack. None
arguments will be skipped. Items in the *context list are arguments will be skipped. Items in the *context list are
added to the stack in order so that later items in the argument added to the stack in order so that later items in the argument
...@@ -152,12 +180,12 @@ class Context(object): ...@@ -152,12 +180,12 @@ class Context(object):
""" """
items = context items = context
context = Context() context = ContextStack()
for item in items: for item in items:
if item is None: if item is None:
continue continue
if isinstance(item, Context): if isinstance(item, ContextStack):
context._stack.extend(item._stack) context._stack.extend(item._stack)
else: else:
context.push(item) context.push(item)
...@@ -226,9 +254,9 @@ class Context(object): ...@@ -226,9 +254,9 @@ class Context(object):
>>> >>>
>>> dct['greet'] is obj.greet >>> dct['greet'] is obj.greet
True True
>>> Context(dct).get('greet') #doctest: +ELLIPSIS >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...> <function greet at 0x...>
>>> Context(obj).get('greet') >>> ContextStack(obj).get('greet')
'Hi Bob!' 'Hi Bob!'
TODO: explain the rationale for this difference in treatment. TODO: explain the rationale for this difference in treatment.
...@@ -270,4 +298,4 @@ class Context(object): ...@@ -270,4 +298,4 @@ class Context(object):
Return a copy of this instance. Return a copy of this instance.
""" """
return Context(*self._stack) return ContextStack(*self._stack)
...@@ -8,7 +8,12 @@ does not otherwise specify a value. ...@@ -8,7 +8,12 @@ does not otherwise specify a value.
""" """
import cgi try:
# Python 3.2 adds html.escape() and deprecates cgi.escape().
from html import escape
except ImportError:
from cgi import escape
import os import os
import sys import sys
...@@ -39,12 +44,14 @@ SEARCH_DIRS = [os.curdir] # i.e. ['.'] ...@@ -39,12 +44,14 @@ SEARCH_DIRS = [os.curdir] # i.e. ['.']
# rendering templates (e.g. for tags enclosed in double braces). # rendering templates (e.g. for tags enclosed in double braces).
# Only unicode strings will be passed to this function. # Only unicode strings will be passed to this function.
# #
# The quote=True argument causes double quotes to be escaped, # The quote=True argument causes double but not single quotes to be escaped
# but not single quotes: # in Python 3.1 and earlier, and both double and single quotes to be
# escaped in Python 3.2 and later:
# #
# http://docs.python.org/library/cgi.html#cgi.escape # http://docs.python.org/library/cgi.html#cgi.escape
# http://docs.python.org/dev/library/html.html#html.escape
# #
TAG_ESCAPE = lambda u: cgi.escape(u, quote=True) TAG_ESCAPE = lambda u: escape(u, quote=True)
# The default template extension. # The default template extension.
TEMPLATE_EXTENSION = 'mustache' TEMPLATE_EXTENSION = 'mustache'
...@@ -9,9 +9,6 @@ from pystache.renderer import Renderer ...@@ -9,9 +9,6 @@ from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec from pystache.template_spec import TemplateSpec
__all__ = ['render', 'Renderer', 'TemplateSpec']
def render(template, context=None, **kwargs): def render(template, context=None, **kwargs):
""" """
Return the given template string rendered using the given context. Return the given template string rendered using the given context.
......
...@@ -8,18 +8,24 @@ This module provides a Loader class for locating and reading templates. ...@@ -8,18 +8,24 @@ This module provides a Loader class for locating and reading templates.
import os import os
import sys import sys
from pystache import common
from pystache import defaults from pystache import defaults
from pystache.locator import Locator from pystache.locator import Locator
def _to_unicode(s, encoding=None): # We make a function so that the current defaults take effect.
""" # TODO: revisit whether this is necessary.
Raises a TypeError exception if the given string is already unicode.
""" def _make_to_unicode():
if encoding is None: def to_unicode(s, encoding=None):
encoding = defaults.STRING_ENCODING """
return unicode(s, encoding, defaults.DECODE_ERRORS) 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)
return to_unicode
class Loader(object): class Loader(object):
...@@ -67,7 +73,7 @@ class Loader(object): ...@@ -67,7 +73,7 @@ class Loader(object):
search_dirs = defaults.SEARCH_DIRS search_dirs = defaults.SEARCH_DIRS
if to_unicode is None: if to_unicode is None:
to_unicode = _to_unicode to_unicode = _make_to_unicode()
self.extension = extension self.extension = extension
self.file_encoding = file_encoding self.file_encoding = file_encoding
...@@ -106,17 +112,12 @@ class Loader(object): ...@@ -106,17 +112,12 @@ class Loader(object):
Read the template at the given path, and return it as a unicode string. 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. b = common.read(path)
f = open(path, 'r')
try:
text = f.read()
finally:
f.close()
if encoding is None: if encoding is None:
encoding = self.file_encoding encoding = self.file_encoding
return self.unicode(text, encoding) return self.unicode(b, encoding)
# TODO: unit-test this method. # TODO: unit-test this method.
def load_name(self, name): def load_name(self, name):
......
...@@ -17,7 +17,7 @@ class ParsedTemplate(object): ...@@ -17,7 +17,7 @@ class ParsedTemplate(object):
parse_tree: a list, each element of which is either-- parse_tree: a list, each element of which is either--
(1) a unicode string, or (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. and returns a unicode string.
The possible rendering callables are the return values of the The possible rendering callables are the return values of the
......
...@@ -9,7 +9,7 @@ This module is only meant for internal use by the renderengine module. ...@@ -9,7 +9,7 @@ This module is only meant for internal use by the renderengine module.
import re import re
from parsed import ParsedTemplate from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = ('{{', '}}') DEFAULT_DELIMITERS = ('{{', '}}')
...@@ -17,7 +17,13 @@ END_OF_LINE_CHARACTERS = ['\r', '\n'] ...@@ -17,7 +17,13 @@ END_OF_LINE_CHARACTERS = ['\r', '\n']
NON_BLANK_RE = re.compile(r'^(.)', re.M) NON_BLANK_RE = re.compile(r'^(.)', 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, # The possible tag type characters following the opening tag,
# excluding "=" and "{". # excluding "=" and "{".
...@@ -74,19 +80,25 @@ class Parser(object): ...@@ -74,19 +80,25 @@ class Parser(object):
self._delimiters = delimiters self._delimiters = delimiters
self.compile_template_re() 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. This method uses the current tag delimiter.
Arguments: 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 = [] parse_tree = []
start_index = index index = start_index
while True: while True:
match = self._template_re.search(template, index) match = self._template_re.search(template, index)
...@@ -131,9 +143,9 @@ class Parser(object): ...@@ -131,9 +143,9 @@ class Parser(object):
if tag_type == '/': if tag_type == '/':
if tag_key != section_key: if tag_key != section_key:
raise ParsingError("Section end tag mismatch: %s != %s" % (repr(tag_key), repr(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) index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
...@@ -142,10 +154,33 @@ class Parser(object): ...@@ -142,10 +154,33 @@ class Parser(object):
return ParsedTemplate(parse_tree) return ParsedTemplate(parse_tree)
def _parse_section(self, template, index_start, section_key): def _parse_section(self, template, start_index, section_key):
parsed_template, template, index_end = self.parse(template=template, index=index_start, section_key=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): def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
...@@ -170,12 +205,12 @@ class Parser(object): ...@@ -170,12 +205,12 @@ class Parser(object):
elif tag_type == '#': 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_section(tag_key, parsed_section, template, self._delimiters) func = engine._make_get_section(tag_key, parsed_section, section_contents, self._delimiters)
elif tag_type == '^': 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) func = engine._make_get_inverse(tag_key, parsed_section)
elif tag_type == '>': elif tag_type == '>':
......
...@@ -7,7 +7,8 @@ Defines a class responsible for rendering logic. ...@@ -7,7 +7,8 @@ Defines a class responsible for rendering logic.
import re import re
from parser import Parser from pystache.context import resolve
from pystache.parser import Parser
class RenderEngine(object): class RenderEngine(object):
...@@ -55,7 +56,7 @@ class RenderEngine(object): ...@@ -55,7 +56,7 @@ class RenderEngine(object):
this class will not pass tag values to literal prior to passing this class will not pass tag values to literal prior to passing
them to this function. This allows for more flexibility, them to this function. This allows for more flexibility,
for example using a custom escape function that handles for example using a custom escape function that handles
incoming strings of type markupssafe.Markup differently incoming strings of type markupsafe.Markup differently
from plain unicode strings. from plain unicode strings.
""" """
...@@ -68,16 +69,7 @@ class RenderEngine(object): ...@@ -68,16 +69,7 @@ class RenderEngine(object):
Get a value from the given context as a basestring instance. Get a value from the given context as a basestring instance.
""" """
val = context.get(tag_name) val = resolve(context, 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): if callable(val):
# According to the spec: # According to the spec:
...@@ -142,6 +134,8 @@ class RenderEngine(object): ...@@ -142,6 +134,8 @@ class RenderEngine(object):
Returns a string with type unicode. 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) data = context.get(name)
if data: if data:
return u'' return u''
...@@ -167,9 +161,33 @@ class RenderEngine(object): ...@@ -167,9 +161,33 @@ class RenderEngine(object):
# TODO: should we check the arity? # TODO: should we check the arity?
template = data(template) template = data(template)
parsed_template = self._parse(template, delimiters=delims) parsed_template = self._parse(template, delimiters=delims)
data = [ data ] # Lambdas special case section rendering and bypass pushing
elif not hasattr(data, '__iter__') or isinstance(data, dict): # the data value onto the context stack. Also see--
data = [ data ] #
# https://github.com/defunkt/pystache/issues/113
#
return parsed_template.render(context)
else:
# The cleanest, least brittle way of determining whether
# something supports iteration is by trying to call iter() on it:
#
# http://docs.python.org/library/functions.html#iter
#
# It is not sufficient, for example, to check whether the item
# implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try:
iter(data)
except TypeError:
# Then the value does not support iteration.
data = [data]
else:
# We treat the value as a list (but do not treat strings
# and dicts as lists).
if isinstance(data, (basestring, dict)):
data = [data]
# Otherwise, leave it alone.
parts = [] parts = []
for element in data: for element in data:
...@@ -202,7 +220,7 @@ class RenderEngine(object): ...@@ -202,7 +220,7 @@ class RenderEngine(object):
Arguments: Arguments:
template: a template string of type unicode. 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 # We keep this type-check as an added check because this method is
...@@ -225,7 +243,7 @@ class RenderEngine(object): ...@@ -225,7 +243,7 @@ class RenderEngine(object):
template: a template string of type unicode (but not a proper template: a template string of type unicode (but not a proper
subclass of unicode). subclass of unicode).
context: a Context instance. context: a ContextStack instance.
""" """
# Be strict but not too strict. In other words, accept str instead # Be strict but not too strict. In other words, accept str instead
......
...@@ -5,14 +5,27 @@ This module provides a Renderer class to render templates. ...@@ -5,14 +5,27 @@ This module provides a Renderer class to render templates.
""" """
import sys
from pystache import defaults from pystache import defaults
from pystache.context import Context from pystache.context import ContextStack
from pystache.loader import Loader from pystache.loader import Loader
from pystache.renderengine import RenderEngine from pystache.renderengine import RenderEngine
from pystache.spec_loader import SpecLoader from pystache.specloader import SpecLoader
from pystache.template_spec import TemplateSpec from pystache.template_spec import TemplateSpec
# TODO: come up with a better solution for this. One of the issues here
# is that in Python 3 there is no common base class for unicode strings
# and byte strings, and 2to3 seems to convert all of "str", "unicode",
# and "basestring" to Python 3's "str".
if sys.version_info < (3, ):
_STRING_TYPES = basestring
else:
# The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3.
_STRING_TYPES = (unicode, type(u"a".encode('utf-8')))
class Renderer(object): class Renderer(object):
""" """
...@@ -27,8 +40,9 @@ class Renderer(object): ...@@ -27,8 +40,9 @@ class Renderer(object):
>>> partials = {'partial': 'Hello, {{thing}}!'} >>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials) >>> renderer = Renderer(partials=partials)
>>> renderer.render('{{>partial}}', {'thing': 'world'}) >>> # We apply print to make the test work in Python 3 after 2to3.
u'Hello, world!' >>> print renderer.render('{{>partial}}', {'thing': 'world'})
Hello, world!
""" """
...@@ -64,10 +78,10 @@ class Renderer(object): ...@@ -64,10 +78,10 @@ class Renderer(object):
this class will only pass it unicode strings. The constructor this class will only pass it unicode strings. The constructor
assigns this function to the constructed instance's escape() assigns this function to the constructed instance's escape()
method. method.
The argument defaults to `cgi.escape(s, quote=True)`. To To disable escaping entirely, one can pass `lambda u: u`
disable escaping entirely, one can pass `lambda u: u` as the as the escape function, for example. One may also wish to
escape function, for example. One may also wish to consider consider using markupsafe's escape function: markupsafe.escape().
using markupsafe's escape function: markupsafe.escape(). This argument defaults to the package default.
file_encoding: the name of the default encoding to use when reading file_encoding: the name of the default encoding to use when reading
template files. All templates are converted to unicode prior template files. All templates are converted to unicode prior
...@@ -160,9 +174,16 @@ class Renderer(object): ...@@ -160,9 +174,16 @@ class Renderer(object):
""" """
return unicode(self.escape(self._to_unicode_soft(s))) return unicode(self.escape(self._to_unicode_soft(s)))
def unicode(self, s, encoding=None): def unicode(self, b, encoding=None):
""" """
Convert a string to unicode, using string_encoding and decode_errors. Convert a byte string to unicode, using string_encoding and decode_errors.
Arguments:
b: a byte string.
encoding: the name of an encoding. Defaults to the string_encoding
attribute for this instance.
Raises: Raises:
...@@ -178,7 +199,7 @@ class Renderer(object): ...@@ -178,7 +199,7 @@ class Renderer(object):
# TODO: Wrap UnicodeDecodeErrors with a message about setting # TODO: Wrap UnicodeDecodeErrors with a message about setting
# the string_encoding and decode_errors attributes. # the string_encoding and decode_errors attributes.
return unicode(s, encoding, self.decode_errors) return unicode(b, encoding, self.decode_errors)
def _make_loader(self): def _make_loader(self):
""" """
...@@ -256,7 +277,7 @@ class Renderer(object): ...@@ -256,7 +277,7 @@ class Renderer(object):
# RenderEngine.render() requires that the template string be unicode. # RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template) template = self._to_unicode_hard(template)
context = Context.create(*context, **kwargs) context = ContextStack.create(*context, **kwargs)
self._context = context self._context = context
engine = self._make_render_engine() engine = self._make_render_engine()
...@@ -317,7 +338,7 @@ class Renderer(object): ...@@ -317,7 +338,7 @@ class Renderer(object):
uses the passed object as the first element of the context stack uses the passed object as the first element of the context stack
when rendering. 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 with which to populate the initial context stack. None
arguments are skipped. Items in the *context list are added to arguments are skipped. Items in the *context list are added to
the context stack in order so that later items in the argument the context stack in order so that later items in the argument
...@@ -329,7 +350,7 @@ class Renderer(object): ...@@ -329,7 +350,7 @@ class Renderer(object):
all items in the *context list. all items in the *context list.
""" """
if isinstance(template, basestring): if isinstance(template, _STRING_TYPES):
return self._render_string(template, *context, **kwargs) return self._render_string(template, *context, **kwargs)
# Otherwise, we assume the template is an object. # Otherwise, we assume the template is an object.
......
# coding: utf-8 # coding: utf-8
""" """
This module supports customized (aka special or specified) template loading. Provides a class to customize template information on a per-view basis.
To customize template properties for a particular view, create that view
from a class that subclasses TemplateSpec. The "Spec" in TemplateSpec
stands for template information that is "special" or "specified".
""" """
......
"""
TODO: add a docstring.
"""
# coding: utf-8
"""
Provides test-related code that can be used by all tests.
"""
import os
import pystache
from pystache import defaults
from pystache.tests import examples
# Save a reference to the original function to avoid recursion.
_DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE
_TESTS_DIR = os.path.dirname(pystache.tests.__file__)
DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'.
EXAMPLES_DIR = os.path.dirname(examples.__file__)
PACKAGE_DIR = os.path.dirname(pystache.__file__)
PROJECT_DIR = os.path.join(PACKAGE_DIR, '..')
SPEC_TEST_DIR = os.path.join(PROJECT_DIR, 'ext', 'spec', 'specs')
# TEXT_DOCTEST_PATHS: the paths to text files (i.e. non-module files)
# containing doctests. The paths should be relative to the project directory.
TEXT_DOCTEST_PATHS = ['README.rst']
UNITTEST_FILE_PREFIX = "test_"
def html_escape(u):
"""
An html escape function that behaves the same in both Python 2 and 3.
This function is needed because single quotes are escaped in Python 3
(to '&#x27;'), but not in Python 2.
The global defaults.TAG_ESCAPE can be set to this function in the
setUp() and tearDown() of unittest test cases, for example, for
consistent test results.
"""
u = _DEFAULT_TAG_ESCAPE(u)
return u.replace("'", '&#x27;')
def get_data_path(file_name):
return os.path.join(DATA_DIR, file_name)
# Functions related to get_module_names().
def _find_files(root_dir, should_include):
"""
Return a list of paths to all modules below the given directory.
Arguments:
should_include: a function that accepts a file path and returns True or False.
"""
paths = [] # Return value.
is_module = lambda path: path.endswith(".py")
# os.walk() is new in Python 2.3
# http://docs.python.org/library/os.html#os.walk
for dir_path, dir_names, file_names in os.walk(root_dir):
new_paths = [os.path.join(dir_path, file_name) for file_name in file_names]
new_paths = filter(is_module, new_paths)
new_paths = filter(should_include, new_paths)
paths.extend(new_paths)
return paths
def _make_module_names(package_dir, paths):
"""
Return a list of fully-qualified module names given a list of module paths.
"""
package_dir = os.path.abspath(package_dir)
package_name = os.path.split(package_dir)[1]
prefix_length = len(package_dir)
module_names = []
for path in paths:
path = os.path.abspath(path) # for example <path_to_package>/subpackage/module.py
rel_path = path[prefix_length:] # for example /subpackage/module.py
rel_path = os.path.splitext(rel_path)[0] # for example /subpackage/module
parts = []
while True:
(rel_path, tail) = os.path.split(rel_path)
if not tail:
break
parts.insert(0, tail)
# We now have, for example, ['subpackage', 'module'].
parts.insert(0, package_name)
module = ".".join(parts)
module_names.append(module)
return module_names
def get_module_names(package_dir=None, should_include=None):
"""
Return a list of fully-qualified module names in the given package.
"""
if package_dir is None:
package_dir = PACKAGE_DIR
if should_include is None:
should_include = lambda path: True
paths = _find_files(package_dir, should_include)
names = _make_module_names(package_dir, paths)
names.sort()
return names
class AssertStringMixin:
"""A unittest.TestCase mixin to check string equality."""
def assertString(self, actual, expected, format=None):
"""
Assert that the given strings are equal and have the same type.
Arguments:
format: a format string containing a single conversion specifier %s.
Defaults to "%s".
"""
if format is None:
format = "%s"
# Show both friendly and literal versions.
details = """String mismatch: %%s\
Expected: \"""%s\"""
Actual: \"""%s\"""
Expected: %s
Actual: %s""" % (expected, actual, repr(expected), repr(actual))
def make_message(reason):
description = details % reason
return format % description
self.assertEqual(actual, expected, make_message("different characters"))
reason = "types different: %s != %s (actual)" % (repr(type(expected)), repr(type(actual)))
self.assertEqual(type(expected), type(actual), make_message(reason))
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)))
class SetupDefaults(object):
"""
Mix this class in to a unittest.TestCase for standard defaults.
This class allows for consistent test results across Python 2/3.
"""
def setup_defaults(self):
self.original_decode_errors = defaults.DECODE_ERRORS
self.original_file_encoding = defaults.FILE_ENCODING
self.original_string_encoding = defaults.STRING_ENCODING
defaults.DECODE_ERRORS = 'strict'
defaults.FILE_ENCODING = 'ascii'
defaults.STRING_ENCODING = 'ascii'
def teardown_defaults(self):
defaults.DECODE_ERRORS = self.original_decode_errors
defaults.FILE_ENCODING = self.original_file_encoding
defaults.STRING_ENCODING = self.original_string_encoding
"""
TODO: add a docstring.
"""
"""
TODO: add a docstring.
"""
# coding: utf-8 # coding: utf-8
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class SayHello(object): class SayHello(object):
......
# coding: utf-8
"""
Exposes a get_doctests() function for the project's test harness.
"""
import doctest
import os
import pkgutil
import sys
import traceback
if sys.version_info >= (3,):
# Then pull in modules needed for 2to3 conversion. The modules
# below are not necessarily available in older versions of Python.
from lib2to3.main import main as lib2to3main # new in Python 2.6?
from shutil import copyfile
from pystache.tests.common import TEXT_DOCTEST_PATHS
from pystache.tests.common import get_module_names
# This module follows the guidance documented here:
#
# http://docs.python.org/library/doctest.html#unittest-api
#
def get_doctests(text_file_dir):
"""
Return a list of TestSuite instances for all doctests in the project.
Arguments:
text_file_dir: the directory in which to search for all text files
(i.e. non-module files) containing doctests.
"""
# Since module_relative is False in our calls to DocFileSuite below,
# paths should be OS-specific. See the following for more info--
#
# http://docs.python.org/library/doctest.html#doctest.DocFileSuite
#
paths = [os.path.normpath(os.path.join(text_file_dir, path)) for path in TEXT_DOCTEST_PATHS]
if sys.version_info >= (3,):
paths = _convert_paths(paths)
suites = []
for path in paths:
suite = doctest.DocFileSuite(path, module_relative=False)
suites.append(suite)
modules = get_module_names()
for module in modules:
suite = doctest.DocTestSuite(module)
suites.append(suite)
return suites
def _convert_2to3(path):
"""
Convert the given file, and return the path to the converted files.
"""
base, ext = os.path.splitext(path)
# For example, "README.temp2to3.rst".
new_path = "%s.temp2to3%s" % (base, ext)
copyfile(path, new_path)
args = ['--doctests_only', '--no-diffs', '--write', '--nobackups', new_path]
lib2to3main("lib2to3.fixes", args=args)
return new_path
def _convert_paths(paths):
"""
Convert the given files, and return the paths to the converted files.
"""
new_paths = []
for path in paths:
new_path = _convert_2to3(path)
new_paths.append(new_path)
return new_paths
"""
TODO: add a docstring.
"""
"""
TODO: add a docstring.
"""
class Comments(object): class Comments(object):
def title(self): def title(self):
......
"""
TODO: add a docstring.
"""
class Complex(object): class Complex(object):
def header(self): def header(self):
......
"""
TODO: add a docstring.
"""
class Delimiters(object): class Delimiters(object):
def first(self): def first(self):
......
"""
TODO: add a docstring.
"""
class DoubleSection(object): class DoubleSection(object):
def t(self): def t(self):
......
"""
TODO: add a docstring.
"""
class Escaped(object): class Escaped(object):
def title(self): def title(self):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class Inverted(object): class Inverted(object):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
def rot(s, n=13): def rot(s, n=13):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class NestedContext(TemplateSpec): class NestedContext(TemplateSpec):
......
from examples.lambdas import rot
"""
TODO: add a docstring.
"""
from pystache.tests.examples.lambdas import rot
class PartialsWithLambdas(object): class PartialsWithLambdas(object):
def rot(self): def rot(self):
return rot return rot
\ No newline at end of file
"""
TODO: add a docstring.
"""
class SayHello(object): class SayHello(object):
def to(self): def to(self):
return "Pizza" return "Pizza"
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class Simple(TemplateSpec): class Simple(TemplateSpec):
...@@ -6,4 +12,4 @@ class Simple(TemplateSpec): ...@@ -6,4 +12,4 @@ class Simple(TemplateSpec):
return "pizza" return "pizza"
def blank(self): def blank(self):
pass return ''
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class TemplatePartial(TemplateSpec): class TemplatePartial(TemplateSpec):
...@@ -18,4 +24,4 @@ class TemplatePartial(TemplateSpec): ...@@ -18,4 +24,4 @@ class TemplatePartial(TemplateSpec):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
def thing(self): def thing(self):
return self._context_get('prop') return self._context_get('prop')
\ No newline at end of file
"""
TODO: add a docstring.
"""
class Unescaped(object): class Unescaped(object):
def title(self): def title(self):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class UnicodeInput(TemplateSpec): class UnicodeInput(TemplateSpec):
......
# encoding: utf-8 # encoding: utf-8
"""
TODO: add a docstring.
"""
class UnicodeOutput(object): class UnicodeOutput(object):
def name(self): def name(self):
......
# coding: utf-8
"""
Exposes a run_tests() function that runs all tests in the project.
This module is for our test console script.
"""
import os
import sys
import unittest
from unittest import TestProgram
import pystache
from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, SPEC_TEST_DIR, UNITTEST_FILE_PREFIX
from pystache.tests.common import get_module_names
from pystache.tests.doctesting import get_doctests
from pystache.tests.spectesting import get_spec_tests
# If this command option is present, then the spec test and doctest directories
# will be inserted if not provided.
FROM_SOURCE_OPTION = "--from-source"
# 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.
Arguments:
sys_argv: a reference to sys.argv.
"""
should_source_exist = False
spec_test_dir = None
project_dir = None
if len(sys_argv) > 1 and sys_argv[1] == FROM_SOURCE_OPTION:
should_source_exist = True
sys_argv.pop(1)
# TODO: use logging module
print "pystache: running tests: expecting source: %s" % should_source_exist
try:
# TODO: use optparse command options instead.
spec_test_dir = sys_argv[1]
sys_argv.pop(1)
except IndexError:
if should_source_exist:
spec_test_dir = SPEC_TEST_DIR
try:
# TODO: use optparse command options instead.
project_dir = sys_argv[1]
sys_argv.pop(1)
except IndexError:
if should_source_exist:
project_dir = PROJECT_DIR
if len(sys_argv) <= 1 or sys_argv[-1].startswith("-"):
# Then no explicit module or test names were provided, so
# auto-detect all unit tests.
module_names = _discover_test_modules(PACKAGE_DIR)
sys_argv.extend(module_names)
if project_dir is not None:
# Add the current module for unit tests contained here.
sys_argv.append(__name__)
_PystacheTestProgram._text_doctest_dir = project_dir
_PystacheTestProgram._spec_test_dir = spec_test_dir
SetupTests.project_dir = project_dir
# We pass None for the module because we do not want the unittest
# module to resolve module names relative to a given module.
# (This would require importing all of the unittest modules from
# this module.) See the loadTestsFromName() method of the
# unittest.TestLoader class for more details on this parameter.
_PystacheTestProgram(argv=sys_argv, module=None)
# No need to return since unitttest.main() exits.
def _discover_test_modules(package_dir):
"""
Discover and return a sorted list of the names of unit-test modules.
"""
def is_unittest_module(path):
file_name = os.path.basename(path)
return file_name.startswith(UNITTEST_FILE_PREFIX)
names = get_module_names(package_dir=package_dir, should_include=is_unittest_module)
# This is a sanity check to ensure that the unit-test discovery
# methods are working.
if len(names) < 1:
raise Exception("No unit-test modules found--\n in %s" % package_dir)
return names
class SetupTests(unittest.TestCase):
"""Tests about setup.py."""
project_dir = None
def test_version(self):
"""
Test that setup.py's version matches the package's version.
"""
original_path = list(sys.path)
sys.path.insert(0, self.project_dir)
try:
from setup import VERSION
self.assertEqual(VERSION, pystache.__version__)
finally:
sys.path = original_path
# The function unittest.main() is an alias for unittest.TestProgram's
# constructor. TestProgram's constructor calls self.runTests() as its
# final step, which expects self.test to be set. The constructor sets
# the self.test attribute by calling one of self.testLoader's "loadTests"
# methods prior to callint self.runTests(). Each loadTest method returns
# a unittest.TestSuite instance. Thus, self.test is set to a TestSuite
# instance prior to calling runTests().
class _PystacheTestProgram(TestProgram):
"""
Instantiating an instance of this class runs all tests.
"""
def runTests(self):
# self.test is a unittest.TestSuite instance:
# http://docs.python.org/library/unittest.html#unittest.TestSuite
tests = self.test
if self._text_doctest_dir is not None:
doctest_suites = get_doctests(self._text_doctest_dir)
tests.addTests(doctest_suites)
if self._spec_test_dir is not None:
spec_testcases = get_spec_tests(self._spec_test_dir)
tests.addTests(spec_testcases)
TestProgram.runTests(self)
# coding: utf-8
"""
Exposes a get_spec_tests() function for the project's test harness.
Creates a unittest.TestCase for the tests defined in the mustache spec.
"""
# TODO: this module can be cleaned up somewhat.
# TODO: move all of this code to pystache/tests/spectesting.py and
# have it expose a get_spec_tests(spec_test_dir) function.
FILE_ENCODING = 'utf-8' # the encoding of the spec test files.
yaml = None
try:
# We try yaml first since it is more convenient when adding and modifying
# test cases by hand (since the YAML is human-readable and is the master
# from which the JSON format is generated).
import yaml
except ImportError:
try:
import json
except:
# The module json is not available prior to Python 2.6, whereas
# simplejson is. The simplejson package dropped support for Python 2.4
# in simplejson v2.1.0, so Python 2.4 requires a simplejson install
# older than the most recent version.
try:
import simplejson as json
except ImportError:
# Raise an error with a type different from ImportError as a hack around
# this issue:
# http://bugs.python.org/issue7559
from sys import exc_info
ex_type, ex_value, tb = exc_info()
new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
raise new_ex.__class__, new_ex, tb
file_extension = 'json'
parser = json
else:
file_extension = 'yml'
parser = yaml
import codecs
import glob
import os.path
import unittest
import pystache
from pystache import common
from pystache.renderer import Renderer
from pystache.tests.common import AssertStringMixin
def get_spec_tests(spec_test_dir):
"""
Return a list of unittest.TestCase instances.
"""
# TODO: use logging module instead.
print "pystache: spec tests: using %s" % _get_parser_info()
cases = []
# Make this absolute for easier diagnosis in case of error.
spec_test_dir = os.path.abspath(spec_test_dir)
spec_paths = glob.glob(os.path.join(spec_test_dir, '*.%s' % file_extension))
for path in spec_paths:
new_cases = _read_spec_tests(path)
cases.extend(new_cases)
# Store this as a value so that CheckSpecTestsFound is not checking
# a reference to cases that contains itself.
spec_test_count = len(cases)
# This test case lets us alert the user that spec tests are missing.
class CheckSpecTestsFound(unittest.TestCase):
def runTest(self):
if spec_test_count > 0:
return
raise Exception("Spec tests not found--\n in %s\n"
" Consult the README file on how to add the Mustache spec tests." % repr(spec_test_dir))
case = CheckSpecTestsFound()
cases.append(case)
return cases
def _get_parser_info():
return "%s (version %s)" % (parser.__name__, parser.__version__)
def _read_spec_tests(path):
"""
Return a list of unittest.TestCase instances.
"""
b = common.read(path)
u = unicode(b, encoding=FILE_ENCODING)
spec_data = parse(u)
tests = spec_data['tests']
cases = []
for data in tests:
case = _deserialize_spec_test(data, path)
cases.append(case)
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.
Arguments:
data: the dictionary of attributes for a single test.
"""
context = data['data']
description = data['desc']
# PyYAML seems to leave ASCII strings as byte strings.
expected = unicode(data['expected'])
# TODO: switch to using dict.get().
partials = data.has_key('partials') and data['partials'] or {}
template = data['template']
test_name = data['name']
_convert_children(context)
test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path)
return test_case
def _make_spec_test(expected, template, context, partials, description, test_name, file_path):
"""
Return a unittest.TestCase instance representing a spec test.
"""
file_name = os.path.basename(file_path)
test_method_name = "Mustache spec (%s): %s" % (file_name, repr(test_name))
# We subclass SpecTestBase in order to control the test method name (for
# the purposes of improved reporting).
class SpecTest(SpecTestBase):
pass
def run_test(self):
self._runTest()
# TODO: should we restore this logic somewhere?
# 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)
setattr(SpecTest, test_method_name, run_test)
case = SpecTest(test_method_name)
case._context = context
case._description = description
case._expected = expected
case._file_path = file_path
case._partials = partials
case._template = template
case._test_name = test_name
return case
def parse(u):
"""
Parse the contents of a spec test file, and return a dict.
Arguments:
u: a unicode string.
"""
# TODO: find a cleaner mechanism for choosing between the two.
if yaml is None:
# Then use json.
# The only way to get the simplejson module to return unicode strings
# is to pass it unicode. See, for example--
#
# http://code.google.com/p/simplejson/issues/detail?id=40
#
# and the documentation of simplejson.loads():
#
# "If s is a str then decoded JSON strings that contain only ASCII
# characters may be parsed as str for performance and memory reasons.
# If your code expects only unicode the appropriate solution is
# decode s to unicode prior to calling loads."
#
return json.loads(u)
# Otherwise, yaml.
def code_constructor(loader, node):
value = loader.construct_mapping(node)
return eval(value['python'], {})
yaml.add_constructor(u'!code', code_constructor)
return yaml.load(u)
class SpecTestBase(unittest.TestCase, AssertStringMixin):
def _runTest(self):
context = self._context
description = self._description
expected = self._expected
file_path = self._file_path
partials = self._partials
template = self._template
test_name = self._test_name
renderer = Renderer(partials=partials)
actual = renderer.render(template, context)
# We need to escape the strings that occur in our format string because
# they can contain % symbols, for example (in delimiters.yml)--
#
# "template: '{{=<% %>=}}(<%text%>)'"
#
def escape(s):
return s.replace("%", "%%")
parser_info = _get_parser_info()
subs = [repr(test_name), description, os.path.abspath(file_path),
template, repr(context), parser_info]
subs = tuple([escape(sub) for sub in subs])
# We include the parsing module version info to help with troubleshooting
# yaml/json/simplejson issues.
message = """%s: %s
File: %s
Template: \"""%s\"""
Context: %s
%%s
[using %s]
""" % subs
self.assertString(actual, expected, format=message)
# coding: utf-8
"""
Tests of __init__.py.
"""
# Calling "import *" is allowed only at the module level.
GLOBALS_INITIAL = globals().keys()
from pystache import *
GLOBALS_PYSTACHE_IMPORTED = globals().keys()
import unittest
import pystache
class InitTests(unittest.TestCase):
def test___all__(self):
"""
Test that "from pystache import *" works as expected.
"""
actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL)
expected = set(['render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
self.assertEqual(actual, expected)
def test_version_defined(self):
"""
Test that pystache.__version__ is set.
"""
actual_version = pystache.__version__
self.assertTrue(actual_version)
...@@ -8,7 +8,7 @@ Unit tests of commands.py. ...@@ -8,7 +8,7 @@ Unit tests of commands.py.
import sys import sys
import unittest import unittest
from pystache.commands import main from pystache.commands.render import main
ORIGINAL_STDOUT = sys.stdout ORIGINAL_STDOUT = sys.stdout
...@@ -39,7 +39,7 @@ class CommandsTestCase(unittest.TestCase): ...@@ -39,7 +39,7 @@ class CommandsTestCase(unittest.TestCase):
""" """
actual = self.callScript("Hi {{thing}}", '{"thing": "world"}') actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
self.assertEquals(actual, u"Hi world\n") self.assertEqual(actual, u"Hi world\n")
def tearDown(self): def tearDown(self):
sys.stdout = ORIGINAL_STDOUT sys.stdout = ORIGINAL_STDOUT
...@@ -10,8 +10,8 @@ import unittest ...@@ -10,8 +10,8 @@ import unittest
from pystache.context import _NOT_FOUND from pystache.context import _NOT_FOUND
from pystache.context import _get_value from pystache.context import _get_value
from pystache.context import Context from pystache.context import ContextStack
from tests.common import AssertIsMixin, Attachable from pystache.tests.common import AssertIsMixin, Attachable
class SimpleObject(object): class SimpleObject(object):
...@@ -58,7 +58,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -58,7 +58,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
""" """
item = {"foo": "bar"} item = {"foo": "bar"}
self.assertEquals(_get_value(item, "foo"), "bar") self.assertEqual(_get_value(item, "foo"), "bar")
def test_dictionary__callable_not_called(self): def test_dictionary__callable_not_called(self):
""" """
...@@ -69,7 +69,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -69,7 +69,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
return "bar" return "bar"
item = {"foo": foo_callable} item = {"foo": foo_callable}
self.assertNotEquals(_get_value(item, "foo"), "bar") self.assertNotEqual(_get_value(item, "foo"), "bar")
self.assertTrue(_get_value(item, "foo") is foo_callable) self.assertTrue(_get_value(item, "foo") is foo_callable)
def test_dictionary__key_missing(self): def test_dictionary__key_missing(self):
...@@ -85,9 +85,11 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -85,9 +85,11 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
Test that dictionary attributes are not checked. Test that dictionary attributes are not checked.
""" """
item = {} item = {1: 2, 3: 4}
attr_name = "keys" # I was not able to find a "public" attribute of dict that is
self.assertEquals(getattr(item, attr_name)(), []) # the same across Python 2/3.
attr_name = "__len__"
self.assertEqual(getattr(item, attr_name)(), 2)
self.assertNotFound(item, attr_name) self.assertNotFound(item, attr_name)
def test_dictionary__dict_subclass(self): def test_dictionary__dict_subclass(self):
...@@ -100,7 +102,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -100,7 +102,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
item = DictSubclass() item = DictSubclass()
item["foo"] = "bar" item["foo"] = "bar"
self.assertEquals(_get_value(item, "foo"), "bar") self.assertEqual(_get_value(item, "foo"), "bar")
### Case: the item is an object. ### Case: the item is an object.
...@@ -110,7 +112,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -110,7 +112,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
""" """
item = SimpleObject() item = SimpleObject()
self.assertEquals(_get_value(item, "foo"), "bar") self.assertEqual(_get_value(item, "foo"), "bar")
def test_object__attribute_missing(self): def test_object__attribute_missing(self):
""" """
...@@ -126,7 +128,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -126,7 +128,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
""" """
item = SimpleObject() item = SimpleObject()
self.assertEquals(_get_value(item, "foo_callable"), "called...") self.assertEqual(_get_value(item, "foo_callable"), "called...")
def test_object__non_built_in_type(self): def test_object__non_built_in_type(self):
""" """
...@@ -134,7 +136,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -134,7 +136,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
""" """
item = datetime(2012, 1, 2) item = datetime(2012, 1, 2)
self.assertEquals(_get_value(item, "day"), 2) self.assertEqual(_get_value(item, "day"), 2)
def test_object__dict_like(self): def test_object__dict_like(self):
""" """
...@@ -142,7 +144,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -142,7 +144,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
""" """
item = DictLike() item = DictLike()
self.assertEquals(item["foo"], "bar") self.assertEqual(item["foo"], "bar")
self.assertNotFound(item, "foo") self.assertNotFound(item, "foo")
### Case: the item is an instance of a built-in type. ### Case: the item is an instance of a built-in type.
...@@ -154,25 +156,20 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -154,25 +156,20 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
""" """
class MyInt(int): pass class MyInt(int): pass
item1 = MyInt(10) cust_int = MyInt(10)
item2 = 10 pure_int = 10
try:
item2.real
except AttributeError:
# Then skip this unit test. The numeric type hierarchy was
# added only in Python 2.6, in which case integers inherit
# from complex numbers the "real" attribute, etc:
#
# http://docs.python.org/library/numbers.html
#
return
self.assertEquals(item1.real, 10) # We have to use a built-in method like __neg__ because "public"
self.assertEquals(item2.real, 10) # attributes like "real" were not added to Python until Python 2.6,
# when the numeric type hierarchy was added:
#
# http://docs.python.org/library/numbers.html
#
self.assertEqual(cust_int.__neg__(), -10)
self.assertEqual(pure_int.__neg__(), -10)
self.assertEquals(_get_value(item1, 'real'), 10) self.assertEqual(_get_value(cust_int, '__neg__'), -10)
self.assertNotFound(item2, 'real') self.assertNotFound(pure_int, '__neg__')
def test_built_in_type__string(self): def test_built_in_type__string(self):
""" """
...@@ -184,10 +181,10 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -184,10 +181,10 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
item1 = MyStr('abc') item1 = MyStr('abc')
item2 = 'abc' item2 = 'abc'
self.assertEquals(item1.upper(), 'ABC') self.assertEqual(item1.upper(), 'ABC')
self.assertEquals(item2.upper(), 'ABC') self.assertEqual(item2.upper(), 'ABC')
self.assertEquals(_get_value(item1, 'upper'), 'ABC') self.assertEqual(_get_value(item1, 'upper'), 'ABC')
self.assertNotFound(item2, 'upper') self.assertNotFound(item2, 'upper')
def test_built_in_type__list(self): def test_built_in_type__list(self):
...@@ -200,17 +197,17 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -200,17 +197,17 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
item1 = MyList([1, 2, 3]) item1 = MyList([1, 2, 3])
item2 = [1, 2, 3] item2 = [1, 2, 3]
self.assertEquals(item1.pop(), 3) self.assertEqual(item1.pop(), 3)
self.assertEquals(item2.pop(), 3) self.assertEqual(item2.pop(), 3)
self.assertEquals(_get_value(item1, 'pop'), 2) self.assertEqual(_get_value(item1, 'pop'), 2)
self.assertNotFound(item2, 'pop') self.assertNotFound(item2, 'pop')
class ContextTests(unittest.TestCase, AssertIsMixin): class ContextStackTests(unittest.TestCase, AssertIsMixin):
""" """
Test the Context class. Test the ContextStack class.
""" """
...@@ -219,34 +216,34 @@ class ContextTests(unittest.TestCase, AssertIsMixin): ...@@ -219,34 +216,34 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Check that passing nothing to __init__() raises no exception. Check that passing nothing to __init__() raises no exception.
""" """
context = Context() context = ContextStack()
def test_init__many_elements(self): def test_init__many_elements(self):
""" """
Check that passing more than two items to __init__() raises no exception. Check that passing more than two items to __init__() raises no exception.
""" """
context = Context({}, {}, {}) context = ContextStack({}, {}, {})
def test__repr(self): def test__repr(self):
context = Context() context = ContextStack()
self.assertEquals(repr(context), 'Context()') self.assertEqual(repr(context), 'ContextStack()')
context = Context({'foo': 'bar'}) context = ContextStack({'foo': 'bar'})
self.assertEquals(repr(context), "Context({'foo': 'bar'},)") self.assertEqual(repr(context), "ContextStack({'foo': 'bar'},)")
context = Context({'foo': 'bar'}, {'abc': 123}) context = ContextStack({'foo': 'bar'}, {'abc': 123})
self.assertEquals(repr(context), "Context({'foo': 'bar'}, {'abc': 123})") self.assertEqual(repr(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
def test__str(self): def test__str(self):
context = Context() context = ContextStack()
self.assertEquals(str(context), 'Context()') self.assertEqual(str(context), 'ContextStack()')
context = Context({'foo': 'bar'}) context = ContextStack({'foo': 'bar'})
self.assertEquals(str(context), "Context({'foo': 'bar'},)") self.assertEqual(str(context), "ContextStack({'foo': 'bar'},)")
context = Context({'foo': 'bar'}, {'abc': 123}) context = ContextStack({'foo': 'bar'}, {'abc': 123})
self.assertEquals(str(context), "Context({'foo': 'bar'}, {'abc': 123})") self.assertEqual(str(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
## Test the static create() method. ## Test the static create() method.
...@@ -255,16 +252,16 @@ class ContextTests(unittest.TestCase, AssertIsMixin): ...@@ -255,16 +252,16 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing a dictionary. Test passing a dictionary.
""" """
context = Context.create({'foo': 'bar'}) context = ContextStack.create({'foo': 'bar'})
self.assertEquals(context.get('foo'), 'bar') self.assertEqual(context.get('foo'), 'bar')
def test_create__none(self): def test_create__none(self):
""" """
Test passing None. Test passing None.
""" """
context = Context.create({'foo': 'bar'}, None) context = ContextStack.create({'foo': 'bar'}, None)
self.assertEquals(context.get('foo'), 'bar') self.assertEqual(context.get('foo'), 'bar')
def test_create__object(self): def test_create__object(self):
""" """
...@@ -273,56 +270,56 @@ class ContextTests(unittest.TestCase, AssertIsMixin): ...@@ -273,56 +270,56 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
""" """
class Foo(object): class Foo(object):
foo = 'bar' foo = 'bar'
context = Context.create(Foo()) context = ContextStack.create(Foo())
self.assertEquals(context.get('foo'), 'bar') self.assertEqual(context.get('foo'), 'bar')
def test_create__context(self): def test_create__context(self):
""" """
Test passing a Context instance. Test passing a ContextStack instance.
""" """
obj = Context({'foo': 'bar'}) obj = ContextStack({'foo': 'bar'})
context = Context.create(obj) context = ContextStack.create(obj)
self.assertEquals(context.get('foo'), 'bar') self.assertEqual(context.get('foo'), 'bar')
def test_create__kwarg(self): def test_create__kwarg(self):
""" """
Test passing a keyword argument. Test passing a keyword argument.
""" """
context = Context.create(foo='bar') context = ContextStack.create(foo='bar')
self.assertEquals(context.get('foo'), 'bar') self.assertEqual(context.get('foo'), 'bar')
def test_create__precedence_positional(self): def test_create__precedence_positional(self):
""" """
Test precedence of positional arguments. Test precedence of positional arguments.
""" """
context = Context.create({'foo': 'bar'}, {'foo': 'buzz'}) context = ContextStack.create({'foo': 'bar'}, {'foo': 'buzz'})
self.assertEquals(context.get('foo'), 'buzz') self.assertEqual(context.get('foo'), 'buzz')
def test_create__precedence_keyword(self): def test_create__precedence_keyword(self):
""" """
Test precedence of keyword arguments. Test precedence of keyword arguments.
""" """
context = Context.create({'foo': 'bar'}, foo='buzz') context = ContextStack.create({'foo': 'bar'}, foo='buzz')
self.assertEquals(context.get('foo'), 'buzz') self.assertEqual(context.get('foo'), 'buzz')
def test_get__key_present(self): def test_get__key_present(self):
""" """
Test getting a key. Test getting a key.
""" """
context = Context({"foo": "bar"}) context = ContextStack({"foo": "bar"})
self.assertEquals(context.get("foo"), "bar") self.assertEqual(context.get("foo"), "bar")
def test_get__key_missing(self): def test_get__key_missing(self):
""" """
Test getting a missing key. Test getting a missing key.
""" """
context = Context() context = ContextStack()
self.assertTrue(context.get("foo") is None) self.assertTrue(context.get("foo") is None)
def test_get__default(self): def test_get__default(self):
...@@ -330,24 +327,24 @@ class ContextTests(unittest.TestCase, AssertIsMixin): ...@@ -330,24 +327,24 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test that get() respects the default value. Test that get() respects the default value.
""" """
context = Context() context = ContextStack()
self.assertEquals(context.get("foo", "bar"), "bar") self.assertEqual(context.get("foo", "bar"), "bar")
def test_get__precedence(self): def test_get__precedence(self):
""" """
Test that get() respects the order of precedence (later items first). Test that get() respects the order of precedence (later items first).
""" """
context = Context({"foo": "bar"}, {"foo": "buzz"}) context = ContextStack({"foo": "bar"}, {"foo": "buzz"})
self.assertEquals(context.get("foo"), "buzz") self.assertEqual(context.get("foo"), "buzz")
def test_get__fallback(self): def test_get__fallback(self):
""" """
Check that first-added stack items are queried on context misses. Check that first-added stack items are queried on context misses.
""" """
context = Context({"fuzz": "buzz"}, {"foo": "bar"}) context = ContextStack({"fuzz": "buzz"}, {"foo": "bar"})
self.assertEquals(context.get("fuzz"), "buzz") self.assertEqual(context.get("fuzz"), "buzz")
def test_push(self): def test_push(self):
""" """
...@@ -355,11 +352,11 @@ class ContextTests(unittest.TestCase, AssertIsMixin): ...@@ -355,11 +352,11 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
""" """
key = "foo" key = "foo"
context = Context({key: "bar"}) context = ContextStack({key: "bar"})
self.assertEquals(context.get(key), "bar") self.assertEqual(context.get(key), "bar")
context.push({key: "buzz"}) context.push({key: "buzz"})
self.assertEquals(context.get(key), "buzz") self.assertEqual(context.get(key), "buzz")
def test_pop(self): def test_pop(self):
""" """
...@@ -367,81 +364,81 @@ class ContextTests(unittest.TestCase, AssertIsMixin): ...@@ -367,81 +364,81 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
""" """
key = "foo" key = "foo"
context = Context({key: "bar"}, {key: "buzz"}) context = ContextStack({key: "bar"}, {key: "buzz"})
self.assertEquals(context.get(key), "buzz") self.assertEqual(context.get(key), "buzz")
item = context.pop() item = context.pop()
self.assertEquals(item, {"foo": "buzz"}) self.assertEqual(item, {"foo": "buzz"})
self.assertEquals(context.get(key), "bar") self.assertEqual(context.get(key), "bar")
def test_top(self): def test_top(self):
key = "foo" key = "foo"
context = Context({key: "bar"}, {key: "buzz"}) context = ContextStack({key: "bar"}, {key: "buzz"})
self.assertEquals(context.get(key), "buzz") self.assertEqual(context.get(key), "buzz")
top = context.top() top = context.top()
self.assertEquals(top, {"foo": "buzz"}) self.assertEqual(top, {"foo": "buzz"})
# Make sure calling top() didn't remove the item from the stack. # Make sure calling top() didn't remove the item from the stack.
self.assertEquals(context.get(key), "buzz") self.assertEqual(context.get(key), "buzz")
def test_copy(self): def test_copy(self):
key = "foo" key = "foo"
original = Context({key: "bar"}, {key: "buzz"}) original = ContextStack({key: "bar"}, {key: "buzz"})
self.assertEquals(original.get(key), "buzz") self.assertEqual(original.get(key), "buzz")
new = original.copy() new = original.copy()
# Confirm that the copy behaves the same. # Confirm that the copy behaves the same.
self.assertEquals(new.get(key), "buzz") self.assertEqual(new.get(key), "buzz")
# Change the copy, and confirm it is changed. # Change the copy, and confirm it is changed.
new.pop() new.pop()
self.assertEquals(new.get(key), "bar") self.assertEqual(new.get(key), "bar")
# Confirm the original is unchanged. # Confirm the original is unchanged.
self.assertEquals(original.get(key), "buzz") self.assertEqual(original.get(key), "buzz")
def test_dot_notation__dict(self): def test_dot_notation__dict(self):
key = "foo.bar" key = "foo.bar"
original = Context({"foo": {"bar": "baz"}}) original = ContextStack({"foo": {"bar": "baz"}})
self.assertEquals(original.get(key), "baz") self.assertEquals(original.get(key), "baz")
# Works all the way down # Works all the way down
key = "a.b.c.d.e.f.g" key = "a.b.c.d.e.f.g"
original = Context({"a": {"b": {"c": {"d": {"e": {"f": {"g": "w00t!"}}}}}}}) original = ContextStack({"a": {"b": {"c": {"d": {"e": {"f": {"g": "w00t!"}}}}}}})
self.assertEquals(original.get(key), "w00t!") self.assertEquals(original.get(key), "w00t!")
def test_dot_notation__user_object(self): def test_dot_notation__user_object(self):
key = "foo.bar" key = "foo.bar"
original = Context({"foo": Attachable(bar="baz")}) original = ContextStack({"foo": Attachable(bar="baz")})
self.assertEquals(original.get(key), "baz") self.assertEquals(original.get(key), "baz")
# Works on multiple levels, too # Works on multiple levels, too
key = "a.b.c.d.e.f.g" key = "a.b.c.d.e.f.g"
Obj = Attachable Obj = Attachable
original = Context({"a": Obj(b=Obj(c=Obj(d=Obj(e=Obj(f=Obj(g="w00t!"))))))}) original = ContextStack({"a": Obj(b=Obj(c=Obj(d=Obj(e=Obj(f=Obj(g="w00t!"))))))})
self.assertEquals(original.get(key), "w00t!") self.assertEquals(original.get(key), "w00t!")
def test_dot_notation__mixed_dict_and_obj(self): def test_dot_notation__mixed_dict_and_obj(self):
key = "foo.bar.baz.bak" key = "foo.bar.baz.bak"
original = Context({"foo": Attachable(bar={"baz": Attachable(bak=42)})}) original = ContextStack({"foo": Attachable(bar={"baz": Attachable(bak=42)})})
self.assertEquals(original.get(key), 42) self.assertEquals(original.get(key), 42)
def test_dot_notation__missing_attr_or_key(self): def test_dot_notation__missing_attr_or_key(self):
key = "foo.bar.baz.bak" key = "foo.bar.baz.bak"
original = Context({"foo": {"bar": {}}}) original = ContextStack({"foo": {"bar": {}}})
self.assertEquals(original.get(key), None) self.assertEquals(original.get(key), None)
original = Context({"foo": Attachable(bar=Attachable())}) original = ContextStack({"foo": Attachable(bar=Attachable())})
self.assertEquals(original.get(key), None) self.assertEquals(original.get(key), None)
def test_dot_notattion__autocall(self): def test_dot_notattion__autocall(self):
key = "foo.bar.baz" key = "foo.bar.baz"
# When any element in the path is callable, it should be automatically invoked # When any element in the path is callable, it should be automatically invoked
original = Context({"foo": Attachable(bar=Attachable(baz=lambda: "Called!"))}) original = ContextStack({"foo": Attachable(bar=Attachable(baz=lambda: "Called!"))})
self.assertEquals(original.get(key), "Called!") self.assertEquals(original.get(key), "Called!")
class Foo(object): class Foo(object):
def bar(self): def bar(self):
return Attachable(baz='Baz') return Attachable(baz='Baz')
original = Context({"foo": Foo()}) original = ContextStack({"foo": Foo()})
self.assertEquals(original.get(key), "Baz") self.assertEquals(original.get(key), "Baz")
# encoding: utf-8 # encoding: utf-8
"""
TODO: add a docstring.
"""
import unittest import unittest
from examples.comments import Comments from examples.comments import Comments
...@@ -12,8 +17,8 @@ from examples.unicode_output import UnicodeOutput ...@@ -12,8 +17,8 @@ from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput from examples.unicode_input import UnicodeInput
from examples.nested_context import NestedContext from examples.nested_context import NestedContext
from pystache import Renderer from pystache import Renderer
from tests.common import EXAMPLES_DIR from pystache.tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin from pystache.tests.common import AssertStringMixin
class TestView(unittest.TestCase, AssertStringMixin): class TestView(unittest.TestCase, AssertStringMixin):
...@@ -95,7 +100,7 @@ Again, Welcome!""") ...@@ -95,7 +100,7 @@ Again, Welcome!""")
view.template = '''{{>partial_in_partial}}''' view.template = '''{{>partial_in_partial}}'''
actual = renderer.render(view, {'prop': 'derp'}) actual = renderer.render(view, {'prop': 'derp'})
self.assertEquals(actual, 'Hi derp!') self.assertEqual(actual, 'Hi derp!')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
# encoding: utf-8 # encoding: utf-8
""" """
Unit tests of reader.py. Unit tests of loader.py.
""" """
...@@ -9,42 +9,45 @@ import os ...@@ -9,42 +9,45 @@ import os
import sys import sys
import unittest import unittest
from tests.common import AssertStringMixin from pystache.tests.common import AssertStringMixin, DATA_DIR, SetupDefaults
from pystache import defaults from pystache import defaults
from pystache.loader import Loader from pystache.loader import Loader
DATA_DIR = 'tests/data' class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
def setUp(self):
self.setup_defaults()
class LoaderTests(unittest.TestCase, AssertStringMixin): def tearDown(self):
self.teardown_defaults()
def test_init__extension(self): def test_init__extension(self):
loader = Loader(extension='foo') loader = Loader(extension='foo')
self.assertEquals(loader.extension, 'foo') self.assertEqual(loader.extension, 'foo')
def test_init__extension__default(self): def test_init__extension__default(self):
# Test the default value. # Test the default value.
loader = Loader() loader = Loader()
self.assertEquals(loader.extension, 'mustache') self.assertEqual(loader.extension, 'mustache')
def test_init__file_encoding(self): def test_init__file_encoding(self):
loader = Loader(file_encoding='bar') loader = Loader(file_encoding='bar')
self.assertEquals(loader.file_encoding, 'bar') self.assertEqual(loader.file_encoding, 'bar')
def test_init__file_encoding__default(self): def test_init__file_encoding__default(self):
file_encoding = defaults.FILE_ENCODING file_encoding = defaults.FILE_ENCODING
try: try:
defaults.FILE_ENCODING = 'foo' defaults.FILE_ENCODING = 'foo'
loader = Loader() loader = Loader()
self.assertEquals(loader.file_encoding, 'foo') self.assertEqual(loader.file_encoding, 'foo')
finally: finally:
defaults.FILE_ENCODING = file_encoding defaults.FILE_ENCODING = file_encoding
def test_init__to_unicode(self): def test_init__to_unicode(self):
to_unicode = lambda x: x to_unicode = lambda x: x
loader = Loader(to_unicode=to_unicode) loader = Loader(to_unicode=to_unicode)
self.assertEquals(loader.to_unicode, to_unicode) self.assertEqual(loader.to_unicode, to_unicode)
def test_init__to_unicode__default(self): def test_init__to_unicode__default(self):
loader = Loader() loader = Loader()
...@@ -53,25 +56,19 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -53,25 +56,19 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
decode_errors = defaults.DECODE_ERRORS decode_errors = defaults.DECODE_ERRORS
string_encoding = defaults.STRING_ENCODING string_encoding = defaults.STRING_ENCODING
nonascii = 'abcdé' nonascii = u'abcdé'.encode('utf-8')
try: loader = Loader()
defaults.DECODE_ERRORS = 'strict' self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
defaults.STRING_ENCODING = 'ascii'
loader = Loader()
self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
defaults.DECODE_ERRORS = 'ignore' defaults.DECODE_ERRORS = 'ignore'
loader = Loader() loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcd') self.assertString(loader.to_unicode(nonascii), u'abcd')
defaults.STRING_ENCODING = 'utf-8' defaults.STRING_ENCODING = 'utf-8'
loader = Loader() loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcdé') self.assertString(loader.to_unicode(nonascii), u'abcdé')
finally:
defaults.DECODE_ERRORS = decode_errors
defaults.STRING_ENCODING = string_encoding
def _get_path(self, filename): def _get_path(self, filename):
return os.path.join(DATA_DIR, filename) return os.path.join(DATA_DIR, filename)
...@@ -81,8 +78,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -81,8 +78,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): default arguments with str input. Test unicode(): default arguments with str input.
""" """
reader = Loader() loader = Loader()
actual = reader.unicode("foo") actual = loader.unicode("foo")
self.assertString(actual, u"foo") self.assertString(actual, u"foo")
...@@ -91,8 +88,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -91,8 +88,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): default arguments with unicode input. Test unicode(): default arguments with unicode input.
""" """
reader = Loader() loader = Loader()
actual = reader.unicode(u"foo") actual = loader.unicode(u"foo")
self.assertString(actual, u"foo") self.assertString(actual, u"foo")
...@@ -106,8 +103,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -106,8 +103,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
s = UnicodeSubclass(u"foo") s = UnicodeSubclass(u"foo")
reader = Loader() loader = Loader()
actual = reader.unicode(s) actual = loader.unicode(s)
self.assertString(actual, u"foo") self.assertString(actual, u"foo")
...@@ -116,32 +113,31 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -116,32 +113,31 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): encoding attribute. Test unicode(): encoding attribute.
""" """
reader = Loader() loader = Loader()
non_ascii = u'abcdé'.encode('utf-8') non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
def to_unicode(s, encoding=None): def to_unicode(s, encoding=None):
if encoding is None: if encoding is None:
encoding = 'utf-8' encoding = 'utf-8'
return unicode(s, encoding) return unicode(s, encoding)
reader.to_unicode = to_unicode loader.to_unicode = to_unicode
self.assertString(reader.unicode(non_ascii), u"abcdé") self.assertString(loader.unicode(non_ascii), u"abcdé")
def test_unicode__encoding_argument(self): def test_unicode__encoding_argument(self):
""" """
Test unicode(): encoding argument. Test unicode(): encoding argument.
""" """
reader = Loader() loader = Loader()
non_ascii = u'abcdé'.encode('utf-8') non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
actual = reader.unicode(non_ascii, encoding='utf-8') actual = loader.unicode(non_ascii, encoding='utf-8')
self.assertString(actual, u'abcdé') self.assertString(actual, u'abcdé')
# TODO: check the read() unit tests. # TODO: check the read() unit tests.
...@@ -150,9 +146,9 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -150,9 +146,9 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test read(). Test read().
""" """
reader = Loader() loader = Loader()
path = self._get_path('ascii.mustache') path = self._get_path('ascii.mustache')
actual = reader.read(path) actual = loader.read(path)
self.assertString(actual, u'ascii: abc') self.assertString(actual, u'ascii: abc')
def test_read__file_encoding__attribute(self): def test_read__file_encoding__attribute(self):
...@@ -174,25 +170,25 @@ class LoaderTests(unittest.TestCase, AssertStringMixin): ...@@ -174,25 +170,25 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test read(): encoding argument respected. Test read(): encoding argument respected.
""" """
reader = Loader() loader = Loader()
path = self._get_path('non_ascii.mustache') path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path) self.assertRaises(UnicodeDecodeError, loader.read, path)
actual = reader.read(path, encoding='utf-8') actual = loader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é') self.assertString(actual, u'non-ascii: é')
def test_reader__to_unicode__attribute(self): def test_loader__to_unicode__attribute(self):
""" """
Test read(): to_unicode attribute respected. Test read(): to_unicode attribute respected.
""" """
reader = Loader() loader = Loader()
path = self._get_path('non_ascii.mustache') path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path) self.assertRaises(UnicodeDecodeError, loader.read, path)
#reader.decode_errors = 'ignore' #loader.decode_errors = 'ignore'
#actual = reader.read(path) #actual = loader.read(path)
#self.assertString(actual, u'non-ascii: ') #self.assertString(actual, u'non-ascii: ')
# encoding: utf-8 # encoding: utf-8
""" """
Contains locator.py unit tests. Unit tests for locator.py.
""" """
...@@ -14,8 +14,8 @@ import unittest ...@@ -14,8 +14,8 @@ import unittest
from pystache.loader import Loader as Reader from pystache.loader import Loader as Reader
from pystache.locator import Locator from pystache.locator import Locator
from tests.common import DATA_DIR from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from data.views import SayHello from pystache.tests.data.views import SayHello
class LocatorTests(unittest.TestCase): class LocatorTests(unittest.TestCase):
...@@ -26,58 +26,65 @@ class LocatorTests(unittest.TestCase): ...@@ -26,58 +26,65 @@ class LocatorTests(unittest.TestCase):
def test_init__extension(self): def test_init__extension(self):
# Test the default value. # Test the default value.
locator = Locator() locator = Locator()
self.assertEquals(locator.template_extension, 'mustache') self.assertEqual(locator.template_extension, 'mustache')
locator = Locator(extension='txt') locator = Locator(extension='txt')
self.assertEquals(locator.template_extension, 'txt') self.assertEqual(locator.template_extension, 'txt')
locator = Locator(extension=False) locator = Locator(extension=False)
self.assertTrue(locator.template_extension is False) self.assertTrue(locator.template_extension is False)
def _assert_paths(self, actual, expected):
"""
Assert that two paths are the same.
"""
self.assertEqual(actual, expected)
def test_get_object_directory(self): def test_get_object_directory(self):
locator = Locator() locator = Locator()
obj = SayHello() obj = SayHello()
actual = locator.get_object_directory(obj) actual = locator.get_object_directory(obj)
self.assertEquals(actual, os.path.abspath(DATA_DIR)) self._assert_paths(actual, DATA_DIR)
def test_get_object_directory__not_hasattr_module(self): def test_get_object_directory__not_hasattr_module(self):
locator = Locator() locator = Locator()
obj = datetime(2000, 1, 1) obj = datetime(2000, 1, 1)
self.assertFalse(hasattr(obj, '__module__')) self.assertFalse(hasattr(obj, '__module__'))
self.assertEquals(locator.get_object_directory(obj), None) self.assertEqual(locator.get_object_directory(obj), None)
self.assertFalse(hasattr(None, '__module__')) self.assertFalse(hasattr(None, '__module__'))
self.assertEquals(locator.get_object_directory(None), None) self.assertEqual(locator.get_object_directory(None), None)
def test_make_file_name(self): def test_make_file_name(self):
locator = Locator() locator = Locator()
locator.template_extension = 'bar' locator.template_extension = 'bar'
self.assertEquals(locator.make_file_name('foo'), 'foo.bar') self.assertEqual(locator.make_file_name('foo'), 'foo.bar')
locator.template_extension = False locator.template_extension = False
self.assertEquals(locator.make_file_name('foo'), 'foo') self.assertEqual(locator.make_file_name('foo'), 'foo')
locator.template_extension = '' locator.template_extension = ''
self.assertEquals(locator.make_file_name('foo'), 'foo.') self.assertEqual(locator.make_file_name('foo'), 'foo.')
def test_make_file_name__template_extension_argument(self): def test_make_file_name__template_extension_argument(self):
locator = Locator() locator = Locator()
self.assertEquals(locator.make_file_name('foo', template_extension='bar'), 'foo.bar') self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
def test_find_name(self): def test_find_name(self):
locator = Locator() locator = Locator()
path = locator.find_name(search_dirs=['examples'], template_name='simple') path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
self.assertEquals(os.path.basename(path), 'simple.mustache') self.assertEqual(os.path.basename(path), 'simple.mustache')
def test_find_name__using_list_of_paths(self): def test_find_name__using_list_of_paths(self):
locator = Locator() locator = Locator()
path = locator.find_name(search_dirs=['doesnt_exist', 'examples'], template_name='simple') path = locator.find_name(search_dirs=[EXAMPLES_DIR, 'doesnt_exist'], template_name='simple')
self.assertTrue(path) self.assertTrue(path)
...@@ -98,7 +105,7 @@ class LocatorTests(unittest.TestCase): ...@@ -98,7 +105,7 @@ class LocatorTests(unittest.TestCase):
dirpath = os.path.dirname(path) dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1] dirname = os.path.split(dirpath)[-1]
self.assertEquals(dirname, 'locator') self.assertEqual(dirname, 'locator')
def test_find_name__non_existent_template_fails(self): def test_find_name__non_existent_template_fails(self):
locator = Locator() locator = Locator()
...@@ -111,9 +118,9 @@ class LocatorTests(unittest.TestCase): ...@@ -111,9 +118,9 @@ class LocatorTests(unittest.TestCase):
obj = SayHello() obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') 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')) expected = os.path.join(DATA_DIR, 'sample_view.mustache')
self.assertEquals(actual, expected) self._assert_paths(actual, expected)
def test_find_object__none_file_name(self): def test_find_object__none_file_name(self):
locator = Locator() locator = Locator()
...@@ -121,20 +128,20 @@ class LocatorTests(unittest.TestCase): ...@@ -121,20 +128,20 @@ class LocatorTests(unittest.TestCase):
obj = SayHello() obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj) actual = locator.find_object(search_dirs=[], obj=obj)
expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache')) expected = os.path.join(DATA_DIR, 'say_hello.mustache')
self.assertEquals(actual, expected) self.assertEqual(actual, expected)
def test_find_object__none_object_directory(self): def test_find_object__none_object_directory(self):
locator = Locator() locator = Locator()
obj = None obj = None
self.assertEquals(None, locator.get_object_directory(obj)) self.assertEqual(None, locator.get_object_directory(obj))
actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache') actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache')
expected = os.path.join(DATA_DIR, 'say_hello.mustache') expected = os.path.join(DATA_DIR, 'say_hello.mustache')
self.assertEquals(actual, expected) self.assertEqual(actual, expected)
def test_make_template_name(self): def test_make_template_name(self):
""" """
...@@ -147,4 +154,4 @@ class LocatorTests(unittest.TestCase): ...@@ -147,4 +154,4 @@ class LocatorTests(unittest.TestCase):
pass pass
foo = FooBar() foo = FooBar()
self.assertEquals(locator.make_template_name(foo), 'foo_bar') self.assertEqual(locator.make_template_name(foo), 'foo_bar')
# 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)
# encoding: utf-8 # encoding: utf-8
import unittest import unittest
import pystache import pystache
from pystache import defaults
from pystache import renderer from pystache import renderer
from pystache.tests.common import html_escape
class PystacheTests(unittest.TestCase): class PystacheTests(unittest.TestCase):
def setUp(self):
self.original_escape = defaults.TAG_ESCAPE
defaults.TAG_ESCAPE = html_escape
def tearDown(self):
defaults.TAG_ESCAPE = self.original_escape
def _assert_rendered(self, expected, template, context): def _assert_rendered(self, expected, template, context):
actual = pystache.render(template, context) actual = pystache.render(template, context)
self.assertEquals(actual, expected) self.assertEqual(actual, expected)
def test_basic(self): def test_basic(self):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' }) ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
self.assertEquals(ret, "Hi world!") self.assertEqual(ret, "Hi world!")
def test_kwargs(self): def test_kwargs(self):
ret = pystache.render("Hi {{thing}}!", thing='world') ret = pystache.render("Hi {{thing}}!", thing='world')
self.assertEquals(ret, "Hi world!") self.assertEqual(ret, "Hi world!")
def test_less_basic(self): def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?" template = "It's a nice day for {{beverage}}, right {{person}}?"
...@@ -42,7 +53,7 @@ class PystacheTests(unittest.TestCase): ...@@ -42,7 +53,7 @@ class PystacheTests(unittest.TestCase):
def test_comments(self): def test_comments(self):
template = "What {{! the }} what?" template = "What {{! the }} what?"
actual = pystache.render(template) actual = pystache.render(template)
self.assertEquals("What what?", actual) self.assertEqual("What what?", actual)
def test_false_sections_are_hidden(self): def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!" template = "Ready {{#set}}set {{/set}}go!"
...@@ -54,7 +65,7 @@ class PystacheTests(unittest.TestCase): ...@@ -54,7 +65,7 @@ class PystacheTests(unittest.TestCase):
context = { 'set': True } context = { 'set': True }
self._assert_rendered("Ready set go!", template, context) self._assert_rendered("Ready set go!", template, context)
non_strings_expected = """(123 & ['something'])(chris & 0.9)""" non_strings_expected = """(123 & [&#x27;something&#x27;])(chris & 0.9)"""
def test_non_strings(self): def test_non_strings(self):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}" template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
......
...@@ -5,13 +5,34 @@ Unit tests of renderengine.py. ...@@ -5,13 +5,34 @@ Unit tests of renderengine.py.
""" """
import cgi
import unittest import unittest
from pystache.context import Context from pystache.context import ContextStack
from pystache import defaults
from pystache.parser import ParsingError from pystache.parser import ParsingError
from pystache.renderengine import RenderEngine from pystache.renderengine import RenderEngine
from tests.common import AssertStringMixin, Attachable from pystache.tests.common import AssertStringMixin, Attachable
def mock_literal(s):
"""
For use as the literal keyword argument to the RenderEngine constructor.
Arguments:
s: a byte string or unicode string.
"""
if isinstance(s, unicode):
# Strip off unicode super classes, if present.
u = unicode(s)
else:
u = unicode(s, encoding='ascii')
# We apply upper() to make sure we are actually using our custom
# function in the tests
return u.upper()
class RenderEngineTestCase(unittest.TestCase): class RenderEngineTestCase(unittest.TestCase):
...@@ -26,9 +47,9 @@ class RenderEngineTestCase(unittest.TestCase): ...@@ -26,9 +47,9 @@ class RenderEngineTestCase(unittest.TestCase):
# In real-life, these arguments would be functions # In real-life, these arguments would be functions
engine = RenderEngine(load_partial="foo", literal="literal", escape="escape") engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
self.assertEquals(engine.escape, "escape") self.assertEqual(engine.escape, "escape")
self.assertEquals(engine.literal, "literal") self.assertEqual(engine.literal, "literal")
self.assertEquals(engine.load_partial, "foo") self.assertEqual(engine.load_partial, "foo")
class RenderTests(unittest.TestCase, AssertStringMixin): class RenderTests(unittest.TestCase, AssertStringMixin):
...@@ -47,7 +68,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -47,7 +68,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Create and return a default RenderEngine for testing. Create and return a default RenderEngine for testing.
""" """
escape = lambda s: unicode(cgi.escape(s)) escape = defaults.TAG_ESCAPE
engine = RenderEngine(literal=unicode, escape=escape, load_partial=None) engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
return engine return engine
...@@ -62,7 +83,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -62,7 +83,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
if partials is not None: if partials is not None:
engine.load_partial = lambda key: unicode(partials[key]) engine.load_partial = lambda key: unicode(partials[key])
context = Context(*context) context = ContextStack(*context)
actual = engine.render(template, context) actual = engine.render(template, context)
...@@ -154,12 +175,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -154,12 +175,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Test a context value that is not a basestring instance. Test a context value that is not a basestring instance.
""" """
# We use include upper() to make sure we are actually using
# our custom function in the tests
to_unicode = lambda s: unicode(s, encoding='ascii').upper()
engine = self._engine() engine = self._engine()
engine.escape = to_unicode engine.escape = mock_literal
engine.literal = to_unicode engine.literal = mock_literal
self.assertRaises(TypeError, engine.literal, 100) self.assertRaises(TypeError, engine.literal, 100)
...@@ -186,37 +204,85 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -186,37 +204,85 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': '{{#hello}}'} context = {'test': '{{#hello}}'}
self._assert_render(u'{{#hello}}', template, context) 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,
# for example by calling a method on a built-in type instance when it
# has a method whose name matches the current key.
#
# Each test case puts an instance of a built-in type on top of the
# context stack before interpolating a tag whose key matches an
# attribute (method or property) of the instance.
#
def _assert_builtin_attr(self, item, attr_name, expected_attr):
self.assertTrue(hasattr(item, attr_name))
actual = getattr(item, attr_name)
if callable(actual):
actual = actual()
self.assertEqual(actual, expected_attr)
def _assert_builtin_type(self, item, attr_name, expected_attr, expected_template):
self._assert_builtin_attr(item, attr_name, expected_attr)
template = '{{#section}}{{%s}}{{/section}}' % attr_name
context = {'section': item, attr_name: expected_template}
self._assert_render(expected_template, template, context)
def test_interpolation__built_in_type__string(self): def test_interpolation__built_in_type__string(self):
""" """
Check tag interpolation with a string on the top of the context stack. Check tag interpolation with a built-in type: string.
""" """
item = 'abc' self._assert_builtin_type('abc', 'upper', 'ABC', u'xyz')
# item.upper() == 'ABC'
template = '{{#section}}{{upper}}{{/section}}'
context = {'section': item, 'upper': 'XYZ'}
self._assert_render(u'XYZ', template, context)
def test_interpolation__built_in_type__integer(self): def test_interpolation__built_in_type__integer(self):
""" """
Check tag interpolation with an integer on the top of the context stack. Check tag interpolation with a built-in type: integer.
""" """
item = 10 # Since public attributes weren't added to integers until Python 2.6
# item.real == 10 # (for example the "real" attribute of the numeric type hierarchy)--
template = '{{#section}}{{real}}{{/section}}' #
context = {'section': item, 'real': 1000} # http://docs.python.org/library/numbers.html
self._assert_render(u'1000', template, context) #
# we need to resort to built-in attributes (double-underscored) on
# the integer type.
self._assert_builtin_type(15, '__neg__', -15, u'999')
def test_interpolation__built_in_type__list(self): def test_interpolation__built_in_type__list(self):
""" """
Check tag interpolation with a list on the top of the context stack. Check tag interpolation with a built-in type: list.
""" """
item = [[1, 2, 3]] item = [[1, 2, 3]]
# item[0].pop() == 3 attr_name = 'pop'
template = '{{#section}}{{pop}}{{/section}}' # Make a copy to prevent changes to item[0].
context = {'section': item, 'pop': 7} self._assert_builtin_attr(list(item[0]), attr_name, 3)
template = '{{#section}}{{%s}}{{/section}}' % attr_name
context = {'section': item, attr_name: 7}
self._assert_render(u'7', template, context) self._assert_render(u'7', template, context)
def test_implicit_iterator__literal(self): def test_implicit_iterator__literal(self):
...@@ -288,7 +354,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -288,7 +354,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
try: try:
self._assert_render(None, template) self._assert_render(None, template)
except ParsingError, err: except ParsingError, err:
self.assertEquals(str(err), "Section end tag mismatch: u'section' != None") self.assertEqual(str(err), "Section end tag mismatch: section != None")
def test_section__end_tag_mismatch(self): def test_section__end_tag_mismatch(self):
""" """
...@@ -299,7 +365,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -299,7 +365,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
try: try:
self._assert_render(None, template) self._assert_render(None, template)
except ParsingError, err: except ParsingError, err:
self.assertEquals(str(err), "Section end tag mismatch: u'section_end' != u'section_start'") self.assertEqual(str(err), "Section end tag mismatch: section_end != section_start")
def test_section__context_values(self): def test_section__context_values(self):
""" """
...@@ -349,6 +415,17 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -349,6 +415,17 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'} context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
self._assert_render(u'{{planet}}: Earth', template, context) self._assert_render(u'{{planet}}: Earth', template, context)
# TODO: have this test case added to the spec.
def test_section__string_values_not_lists(self):
"""
Check that string section values are not interpreted as lists.
"""
template = '{{#section}}foo{{/section}}'
context = {'section': '123'}
# If strings were interpreted as lists, this would give "foofoofoo".
self._assert_render(u'foo', template, context)
def test_section__nested_truthy(self): def test_section__nested_truthy(self):
""" """
Check that "nested truthy" sections get rendered. Check that "nested truthy" sections get rendered.
...@@ -424,6 +501,40 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -424,6 +501,40 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'person': 'Mom', 'test': (lambda text: text + " :)")} context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
self._assert_render(u'Hi Mom :)', template, context) self._assert_render(u'Hi Mom :)', 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): def test_comment__multiline(self):
""" """
Check that multiline comments are permitted. Check that multiline comments are permitted.
......
...@@ -15,9 +15,24 @@ from pystache import Renderer ...@@ -15,9 +15,24 @@ from pystache import Renderer
from pystache import TemplateSpec from pystache import TemplateSpec
from pystache.loader import Loader from pystache.loader import Loader
from tests.common import get_data_path from pystache.tests.common import get_data_path, AssertStringMixin
from tests.common import AssertStringMixin from pystache.tests.data.views import SayHello
from tests.data.views import SayHello
def _make_renderer():
"""
Return a default Renderer instance for testing purposes.
"""
renderer = Renderer(string_encoding='ascii', file_encoding='ascii')
return renderer
def mock_unicode(b, encoding=None):
if encoding is None:
encoding = 'ascii'
u = unicode(b, encoding=encoding)
return u.upper()
class RendererInitTestCase(unittest.TestCase): class RendererInitTestCase(unittest.TestCase):
...@@ -41,20 +56,24 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -41,20 +56,24 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(partials={'foo': 'bar'}) renderer = Renderer(partials={'foo': 'bar'})
self.assertEquals(renderer.partials, {'foo': 'bar'}) self.assertEqual(renderer.partials, {'foo': 'bar'})
def test_escape__default(self): def test_escape__default(self):
escape = Renderer().escape escape = Renderer().escape
self.assertEquals(escape(">"), "&gt;") self.assertEqual(escape(">"), "&gt;")
self.assertEquals(escape('"'), "&quot;") self.assertEqual(escape('"'), "&quot;")
# Single quotes are not escaped. # Single quotes are escaped only in Python 3.2 and later.
self.assertEquals(escape("'"), "'") if sys.version_info < (3, 2):
expected = "'"
else:
expected = '&#x27;'
self.assertEqual(escape("'"), expected)
def test_escape(self): def test_escape(self):
escape = lambda s: "**" + s escape = lambda s: "**" + s
renderer = Renderer(escape=escape) renderer = Renderer(escape=escape)
self.assertEquals(renderer.escape("bar"), "**bar") self.assertEqual(renderer.escape("bar"), "**bar")
def test_decode_errors__default(self): def test_decode_errors__default(self):
""" """
...@@ -62,7 +81,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -62,7 +81,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
self.assertEquals(renderer.decode_errors, 'strict') self.assertEqual(renderer.decode_errors, 'strict')
def test_decode_errors(self): def test_decode_errors(self):
""" """
...@@ -70,7 +89,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -70,7 +89,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(decode_errors="foo") renderer = Renderer(decode_errors="foo")
self.assertEquals(renderer.decode_errors, "foo") self.assertEqual(renderer.decode_errors, "foo")
def test_file_encoding__default(self): def test_file_encoding__default(self):
""" """
...@@ -78,7 +97,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -78,7 +97,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
self.assertEquals(renderer.file_encoding, renderer.string_encoding) self.assertEqual(renderer.file_encoding, renderer.string_encoding)
def test_file_encoding(self): def test_file_encoding(self):
""" """
...@@ -86,7 +105,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -86,7 +105,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(file_encoding='foo') renderer = Renderer(file_encoding='foo')
self.assertEquals(renderer.file_encoding, 'foo') self.assertEqual(renderer.file_encoding, 'foo')
def test_file_extension__default(self): def test_file_extension__default(self):
""" """
...@@ -94,7 +113,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -94,7 +113,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
self.assertEquals(renderer.file_extension, 'mustache') self.assertEqual(renderer.file_extension, 'mustache')
def test_file_extension(self): def test_file_extension(self):
""" """
...@@ -102,7 +121,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -102,7 +121,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(file_extension='foo') renderer = Renderer(file_extension='foo')
self.assertEquals(renderer.file_extension, 'foo') self.assertEqual(renderer.file_extension, 'foo')
def test_search_dirs__default(self): def test_search_dirs__default(self):
""" """
...@@ -110,7 +129,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -110,7 +129,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
self.assertEquals(renderer.search_dirs, [os.curdir]) self.assertEqual(renderer.search_dirs, [os.curdir])
def test_search_dirs__string(self): def test_search_dirs__string(self):
""" """
...@@ -118,7 +137,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -118,7 +137,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(search_dirs='foo') renderer = Renderer(search_dirs='foo')
self.assertEquals(renderer.search_dirs, ['foo']) self.assertEqual(renderer.search_dirs, ['foo'])
def test_search_dirs__list(self): def test_search_dirs__list(self):
""" """
...@@ -126,7 +145,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -126,7 +145,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(search_dirs=['foo']) renderer = Renderer(search_dirs=['foo'])
self.assertEquals(renderer.search_dirs, ['foo']) self.assertEqual(renderer.search_dirs, ['foo'])
def test_string_encoding__default(self): def test_string_encoding__default(self):
""" """
...@@ -134,7 +153,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -134,7 +153,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
self.assertEquals(renderer.string_encoding, sys.getdefaultencoding()) self.assertEqual(renderer.string_encoding, sys.getdefaultencoding())
def test_string_encoding(self): def test_string_encoding(self):
""" """
...@@ -142,7 +161,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -142,7 +161,7 @@ class RendererInitTestCase(unittest.TestCase):
""" """
renderer = Renderer(string_encoding="foo") renderer = Renderer(string_encoding="foo")
self.assertEquals(renderer.string_encoding, "foo") self.assertEqual(renderer.string_encoding, "foo")
class RendererTests(unittest.TestCase, AssertStringMixin): class RendererTests(unittest.TestCase, AssertStringMixin):
...@@ -159,30 +178,30 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -159,30 +178,30 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
Test that the string_encoding attribute is respected. Test that the string_encoding attribute is respected.
""" """
renderer = Renderer() renderer = self._renderer()
s = "é" b = u"é".encode('utf-8')
renderer.string_encoding = "ascii" renderer.string_encoding = "ascii"
self.assertRaises(UnicodeDecodeError, renderer.unicode, s) self.assertRaises(UnicodeDecodeError, renderer.unicode, b)
renderer.string_encoding = "utf-8" renderer.string_encoding = "utf-8"
self.assertEquals(renderer.unicode(s), u"é") self.assertEqual(renderer.unicode(b), u"é")
def test_unicode__decode_errors(self): def test_unicode__decode_errors(self):
""" """
Test that the decode_errors attribute is respected. Test that the decode_errors attribute is respected.
""" """
renderer = Renderer() renderer = self._renderer()
renderer.string_encoding = "ascii" renderer.string_encoding = "ascii"
s = "déf" b = u"déf".encode('utf-8')
renderer.decode_errors = "ignore" renderer.decode_errors = "ignore"
self.assertEquals(renderer.unicode(s), "df") self.assertEqual(renderer.unicode(b), "df")
renderer.decode_errors = "replace" renderer.decode_errors = "replace"
# U+FFFD is the official Unicode replacement character. # U+FFFD is the official Unicode replacement character.
self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf') self.assertEqual(renderer.unicode(b), u'd\ufffd\ufffdf')
## Test the _make_loader() method. ## Test the _make_loader() method.
...@@ -191,10 +210,10 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -191,10 +210,10 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
Test that _make_loader() returns a Loader. Test that _make_loader() returns a Loader.
""" """
renderer = Renderer() renderer = self._renderer()
loader = renderer._make_loader() loader = renderer._make_loader()
self.assertEquals(type(loader), Loader) self.assertEqual(type(loader), Loader)
def test__make_loader__attributes(self): def test__make_loader__attributes(self):
""" """
...@@ -203,16 +222,16 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -203,16 +222,16 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
""" """
unicode_ = lambda x: x unicode_ = lambda x: x
renderer = Renderer() renderer = self._renderer()
renderer.file_encoding = 'enc' renderer.file_encoding = 'enc'
renderer.file_extension = 'ext' renderer.file_extension = 'ext'
renderer.unicode = unicode_ renderer.unicode = unicode_
loader = renderer._make_loader() loader = renderer._make_loader()
self.assertEquals(loader.extension, 'ext') self.assertEqual(loader.extension, 'ext')
self.assertEquals(loader.file_encoding, 'enc') self.assertEqual(loader.file_encoding, 'enc')
self.assertEquals(loader.to_unicode, unicode_) self.assertEqual(loader.to_unicode, unicode_)
## Test the render() method. ## Test the render() method.
...@@ -221,57 +240,57 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -221,57 +240,57 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
Check that render() returns a string of type unicode. Check that render() returns a string of type unicode.
""" """
renderer = Renderer() renderer = self._renderer()
rendered = renderer.render('foo') rendered = renderer.render('foo')
self.assertEquals(type(rendered), unicode) self.assertEqual(type(rendered), unicode)
def test_render__unicode(self): def test_render__unicode(self):
renderer = Renderer() renderer = self._renderer()
actual = renderer.render(u'foo') actual = renderer.render(u'foo')
self.assertEquals(actual, u'foo') self.assertEqual(actual, u'foo')
def test_render__str(self): def test_render__str(self):
renderer = Renderer() renderer = self._renderer()
actual = renderer.render('foo') actual = renderer.render('foo')
self.assertEquals(actual, 'foo') self.assertEqual(actual, 'foo')
def test_render__non_ascii_character(self): def test_render__non_ascii_character(self):
renderer = Renderer() renderer = self._renderer()
actual = renderer.render(u'Poincaré') actual = renderer.render(u'Poincaré')
self.assertEquals(actual, u'Poincaré') self.assertEqual(actual, u'Poincaré')
def test_render__context(self): def test_render__context(self):
""" """
Test render(): passing a context. Test render(): passing a context.
""" """
renderer = Renderer() renderer = self._renderer()
self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom') self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom')
def test_render__context_and_kwargs(self): def test_render__context_and_kwargs(self):
""" """
Test render(): passing a context and **kwargs. Test render(): passing a context and **kwargs.
""" """
renderer = Renderer() renderer = self._renderer()
template = 'Hi {{person1}} and {{person2}}' template = 'Hi {{person1}} and {{person2}}'
self.assertEquals(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') self.assertEqual(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad')
def test_render__kwargs_and_no_context(self): def test_render__kwargs_and_no_context(self):
""" """
Test render(): passing **kwargs and no context. Test render(): passing **kwargs and no context.
""" """
renderer = Renderer() renderer = self._renderer()
self.assertEquals(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom') self.assertEqual(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom')
def test_render__context_and_kwargs__precedence(self): def test_render__context_and_kwargs__precedence(self):
""" """
Test render(): **kwargs takes precedence over context. Test render(): **kwargs takes precedence over context.
""" """
renderer = Renderer() renderer = self._renderer()
self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad') self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad')
def test_render__kwargs_does_not_modify_context(self): def test_render__kwargs_does_not_modify_context(self):
""" """
...@@ -279,25 +298,25 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -279,25 +298,25 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
""" """
context = {} context = {}
renderer = Renderer() renderer = self._renderer()
renderer.render('Hi {{person}}', context=context, foo="bar") renderer.render('Hi {{person}}', context=context, foo="bar")
self.assertEquals(context, {}) self.assertEqual(context, {})
def test_render__nonascii_template(self): def test_render__nonascii_template(self):
""" """
Test passing a non-unicode template with non-ascii characters. Test passing a non-unicode template with non-ascii characters.
""" """
renderer = Renderer() renderer = _make_renderer()
template = "déf" template = u"déf".encode("utf-8")
# Check that decode_errors and string_encoding are both respected. # Check that decode_errors and string_encoding are both respected.
renderer.decode_errors = 'ignore' renderer.decode_errors = 'ignore'
renderer.string_encoding = 'ascii' renderer.string_encoding = 'ascii'
self.assertEquals(renderer.render(template), "df") self.assertEqual(renderer.render(template), "df")
renderer.string_encoding = 'utf_8' renderer.string_encoding = 'utf_8'
self.assertEquals(renderer.render(template), u"déf") self.assertEqual(renderer.render(template), u"déf")
def test_make_load_partial(self): def test_make_load_partial(self):
""" """
...@@ -309,8 +328,8 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -309,8 +328,8 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
load_partial = renderer._make_load_partial() load_partial = renderer._make_load_partial()
actual = load_partial('foo') actual = load_partial('foo')
self.assertEquals(actual, 'bar') self.assertEqual(actual, 'bar')
self.assertEquals(type(actual), unicode, "RenderEngine requires that " self.assertEqual(type(actual), unicode, "RenderEngine requires that "
"load_partial return unicode strings.") "load_partial return unicode strings.")
def test_make_load_partial__unicode(self): def test_make_load_partial__unicode(self):
...@@ -322,14 +341,14 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -322,14 +341,14 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer.partials = {'partial': 'foo'} renderer.partials = {'partial': 'foo'}
load_partial = renderer._make_load_partial() load_partial = renderer._make_load_partial()
self.assertEquals(load_partial("partial"), "foo") self.assertEqual(load_partial("partial"), "foo")
# Now with a value that is already unicode. # Now with a value that is already unicode.
renderer.partials = {'partial': u'foo'} renderer.partials = {'partial': u'foo'}
load_partial = renderer._make_load_partial() load_partial = renderer._make_load_partial()
# If the next line failed, we would get the following error: # If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported # TypeError: decoding Unicode is not supported
self.assertEquals(load_partial("partial"), "foo") self.assertEqual(load_partial("partial"), "foo")
def test_render_path(self): def test_render_path(self):
""" """
...@@ -339,7 +358,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -339,7 +358,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer = Renderer() renderer = Renderer()
path = get_data_path('say_hello.mustache') path = get_data_path('say_hello.mustache')
actual = renderer.render_path(path, to='foo') actual = renderer.render_path(path, to='foo')
self.assertEquals(actual, "Hello, foo") self.assertEqual(actual, "Hello, foo")
def test_render__object(self): def test_render__object(self):
""" """
...@@ -350,10 +369,10 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -350,10 +369,10 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
say_hello = SayHello() say_hello = SayHello()
actual = renderer.render(say_hello) actual = renderer.render(say_hello)
self.assertEquals('Hello, World', actual) self.assertEqual('Hello, World', actual)
actual = renderer.render(say_hello, to='Mars') actual = renderer.render(say_hello, to='Mars')
self.assertEquals('Hello, Mars', actual) self.assertEqual('Hello, Mars', actual)
def test_render__template_spec(self): def test_render__template_spec(self):
""" """
...@@ -379,7 +398,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -379,7 +398,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
view = Simple() view = Simple()
actual = renderer.render(view) actual = renderer.render(view)
self.assertEquals('Hi pizza!', actual) self.assertEqual('Hi pizza!', actual)
# By testing that Renderer.render() constructs the right RenderEngine, # By testing that Renderer.render() constructs the right RenderEngine,
...@@ -393,6 +412,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -393,6 +412,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
""" """
def _make_renderer(self):
"""
Return a default Renderer instance for testing purposes.
"""
return _make_renderer()
## Test the engine's load_partial attribute. ## Test the engine's load_partial attribute.
def test__load_partial__returns_unicode(self): def test__load_partial__returns_unicode(self):
...@@ -410,13 +436,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -410,13 +436,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
actual = engine.load_partial('str') actual = engine.load_partial('str')
self.assertEquals(actual, "foo") self.assertEqual(actual, "foo")
self.assertEquals(type(actual), unicode) self.assertEqual(type(actual), unicode)
# Check that unicode subclasses are not preserved. # Check that unicode subclasses are not preserved.
actual = engine.load_partial('subclass') actual = engine.load_partial('subclass')
self.assertEquals(actual, "abc") self.assertEqual(actual, "abc")
self.assertEquals(type(actual), unicode) self.assertEqual(type(actual), unicode)
def test__load_partial__not_found(self): def test__load_partial__not_found(self):
""" """
...@@ -433,7 +459,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -433,7 +459,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
load_partial("foo") load_partial("foo")
raise Exception("Shouldn't get here") raise Exception("Shouldn't get here")
except Exception, err: except Exception, err:
self.assertEquals(str(err), "Partial not found with name: 'foo'") self.assertEqual(str(err), "Partial not found with name: 'foo'")
## Test the engine's literal attribute. ## Test the engine's literal attribute.
...@@ -442,13 +468,14 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -442,13 +468,14 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
Test that literal uses the renderer's unicode function. Test that literal uses the renderer's unicode function.
""" """
renderer = Renderer() renderer = self._make_renderer()
renderer.unicode = lambda s: s.upper() renderer.unicode = mock_unicode
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
literal = engine.literal literal = engine.literal
self.assertEquals(literal("foo"), "FOO") b = u"foo".encode("ascii")
self.assertEqual(literal(b), "FOO")
def test__literal__handles_unicode(self): def test__literal__handles_unicode(self):
""" """
...@@ -461,7 +488,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -461,7 +488,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
literal = engine.literal literal = engine.literal
self.assertEquals(literal(u"foo"), "foo") self.assertEqual(literal(u"foo"), "foo")
def test__literal__returns_unicode(self): def test__literal__returns_unicode(self):
""" """
...@@ -474,16 +501,16 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -474,16 +501,16 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
literal = engine.literal literal = engine.literal
self.assertEquals(type(literal("foo")), unicode) self.assertEqual(type(literal("foo")), unicode)
class MyUnicode(unicode): class MyUnicode(unicode):
pass pass
s = MyUnicode("abc") s = MyUnicode("abc")
self.assertEquals(type(s), MyUnicode) self.assertEqual(type(s), MyUnicode)
self.assertTrue(isinstance(s, unicode)) self.assertTrue(isinstance(s, unicode))
self.assertEquals(type(literal(s)), unicode) self.assertEqual(type(literal(s)), unicode)
## Test the engine's escape attribute. ## Test the engine's escape attribute.
...@@ -498,7 +525,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -498,7 +525,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
escape = engine.escape escape = engine.escape
self.assertEquals(escape("foo"), "**foo") self.assertEqual(escape("foo"), "**foo")
def test__escape__uses_renderer_unicode(self): def test__escape__uses_renderer_unicode(self):
""" """
...@@ -506,12 +533,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -506,12 +533,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
renderer.unicode = lambda s: s.upper() renderer.unicode = mock_unicode
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
escape = engine.escape escape = engine.escape
self.assertEquals(escape("foo"), "FOO") b = u"foo".encode('ascii')
self.assertEqual(escape(b), "FOO")
def test__escape__has_access_to_original_unicode_subclass(self): def test__escape__has_access_to_original_unicode_subclass(self):
""" """
...@@ -519,7 +547,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -519,7 +547,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
renderer.escape = lambda s: type(s).__name__ renderer.escape = lambda s: unicode(type(s).__name__)
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
escape = engine.escape escape = engine.escape
...@@ -527,9 +555,9 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -527,9 +555,9 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
class MyUnicode(unicode): class MyUnicode(unicode):
pass pass
self.assertEquals(escape("foo"), "unicode") self.assertEqual(escape(u"foo".encode('ascii')), unicode.__name__)
self.assertEquals(escape(u"foo"), "unicode") self.assertEqual(escape(u"foo"), unicode.__name__)
self.assertEquals(escape(MyUnicode("foo")), "MyUnicode") self.assertEqual(escape(MyUnicode("foo")), MyUnicode.__name__)
def test__escape__returns_unicode(self): def test__escape__returns_unicode(self):
""" """
...@@ -542,7 +570,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -542,7 +570,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
escape = engine.escape escape = engine.escape
self.assertEquals(type(escape("foo")), unicode) self.assertEqual(type(escape("foo")), unicode)
# Check that literal doesn't preserve unicode subclasses. # Check that literal doesn't preserve unicode subclasses.
class MyUnicode(unicode): class MyUnicode(unicode):
...@@ -550,7 +578,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): ...@@ -550,7 +578,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
s = MyUnicode("abc") s = MyUnicode("abc")
self.assertEquals(type(s), MyUnicode) self.assertEqual(type(s), MyUnicode)
self.assertTrue(isinstance(s, unicode)) self.assertTrue(isinstance(s, unicode))
self.assertEquals(type(escape(s)), unicode) self.assertEqual(type(escape(s)), unicode)
...@@ -8,8 +8,8 @@ from examples.lambdas import Lambdas ...@@ -8,8 +8,8 @@ from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial from examples.template_partial import TemplatePartial
from examples.simple import Simple from examples.simple import Simple
from tests.common import EXAMPLES_DIR from pystache.tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin from pystache.tests.common import AssertStringMixin
class TestSimple(unittest.TestCase, AssertStringMixin): class TestSimple(unittest.TestCase, AssertStringMixin):
...@@ -28,11 +28,11 @@ class TestSimple(unittest.TestCase, AssertStringMixin): ...@@ -28,11 +28,11 @@ class TestSimple(unittest.TestCase, AssertStringMixin):
renderer = Renderer() renderer = Renderer()
actual = renderer.render(template, context) actual = renderer.render(template, context)
self.assertEquals(actual, "Colors: red Colors: green Colors: blue ") self.assertEqual(actual, "Colors: red Colors: green Colors: blue ")
def test_empty_context(self): def test_empty_context(self):
template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}' template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}'
self.assertEquals(pystache.Renderer().render(template), "Should see me") self.assertEqual(pystache.Renderer().render(template), "Should see me")
def test_callables(self): def test_callables(self):
view = Lambdas() view = Lambdas()
...@@ -58,7 +58,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin): ...@@ -58,7 +58,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin):
def test_non_existent_value_renders_blank(self): def test_non_existent_value_renders_blank(self):
view = Simple() view = Simple()
template = '{{not_set}} {{blank}}' template = '{{not_set}} {{blank}}'
self.assertEquals(pystache.Renderer().render(template), ' ') self.assertEqual(pystache.Renderer().render(template), ' ')
def test_template_partial_extension(self): def test_template_partial_extension(self):
""" """
......
...@@ -18,13 +18,11 @@ from pystache import Renderer ...@@ -18,13 +18,11 @@ from pystache import Renderer
from pystache import TemplateSpec from pystache import TemplateSpec
from pystache.locator import Locator from pystache.locator import Locator
from pystache.loader import Loader from pystache.loader import Loader
from pystache.spec_loader import SpecLoader from pystache.specloader import SpecLoader
from tests.common import DATA_DIR from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from tests.common import EXAMPLES_DIR from pystache.tests.common import AssertIsMixin, AssertStringMixin
from tests.common import AssertIsMixin from pystache.tests.data.views import SampleView
from tests.common import AssertStringMixin from pystache.tests.data.views import NonAscii
from tests.data.views import SampleView
from tests.data.views import NonAscii
class Thing(object): class Thing(object):
...@@ -46,9 +44,10 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -46,9 +44,10 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(IOError, renderer.render, view) self.assertRaises(IOError, renderer.render, view)
view.template_rel_directory = "../examples" # TODO: change this test to remove the following brittle line.
view.template_rel_directory = "examples"
actual = renderer.render(view) actual = renderer.render(view)
self.assertEquals(actual, "No tags...") self.assertEqual(actual, "No tags...")
def test_template_path_for_partials(self): def test_template_path_for_partials(self):
""" """
...@@ -64,7 +63,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -64,7 +63,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(IOError, renderer1.render, spec) self.assertRaises(IOError, renderer1.render, spec)
actual = renderer2.render(spec) actual = renderer2.render(spec)
self.assertEquals(actual, "Partial: No tags...") self.assertEqual(actual, "Partial: No tags...")
def test_basic_method_calls(self): def test_basic_method_calls(self):
renderer = Renderer() renderer = Renderer()
...@@ -78,7 +77,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -78,7 +77,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer = Renderer() renderer = Renderer()
actual = renderer.render(view) actual = renderer.render(view)
self.assertEquals(actual, "Hi Chris!") self.assertEqual(actual, "Hi Chris!")
def test_complex(self): def test_complex(self):
renderer = Renderer() renderer = Renderer()
...@@ -94,7 +93,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -94,7 +93,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_higher_order_replace(self): def test_higher_order_replace(self):
renderer = Renderer() renderer = Renderer()
actual = renderer.render(Lambdas()) actual = renderer.render(Lambdas())
self.assertEquals(actual, 'bar != bar. oh, it does!') self.assertEqual(actual, 'bar != bar. oh, it does!')
def test_higher_order_rot13(self): def test_higher_order_rot13(self):
view = Lambdas() view = Lambdas()
...@@ -118,7 +117,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -118,7 +117,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer = Renderer(search_dirs=EXAMPLES_DIR) renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(view) actual = renderer.render(view)
self.assertEquals(actual, u'nopqrstuvwxyz') self.assertEqual(actual, u'nopqrstuvwxyz')
def test_hierarchical_partials_with_lambdas(self): def test_hierarchical_partials_with_lambdas(self):
view = Lambdas() view = Lambdas()
...@@ -151,6 +150,28 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -151,6 +150,28 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertString(actual, u"""one, two, three, empty list""") self.assertString(actual, u"""one, two, three, empty list""")
def _make_specloader():
"""
Return a default SpecLoader instance for testing purposes.
"""
# Python 2 and 3 have different default encodings. Thus, to have
# consistent test results across both versions, we need to specify
# the string and file encodings explicitly rather than relying on
# the defaults.
def to_unicode(s, encoding=None):
"""
Raises a TypeError exception if the given string is already unicode.
"""
if encoding is None:
encoding = 'ascii'
return unicode(s, encoding, 'strict')
loader = Loader(file_encoding='ascii', to_unicode=to_unicode)
return SpecLoader(loader=loader)
class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
""" """
...@@ -158,13 +179,16 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -158,13 +179,16 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
""" """
def _make_specloader(self):
return _make_specloader()
def test_init__defaults(self): def test_init__defaults(self):
custom = SpecLoader() spec_loader = SpecLoader()
# Check the loader attribute. # Check the loader attribute.
loader = custom.loader loader = spec_loader.loader
self.assertEquals(loader.extension, 'mustache') self.assertEqual(loader.extension, 'mustache')
self.assertEquals(loader.file_encoding, sys.getdefaultencoding()) self.assertEqual(loader.file_encoding, sys.getdefaultencoding())
# TODO: finish testing the other Loader attributes. # TODO: finish testing the other Loader attributes.
to_unicode = loader.to_unicode to_unicode = loader.to_unicode
...@@ -186,7 +210,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -186,7 +210,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec() custom = TemplateSpec()
custom.template = "abc" custom.template = "abc"
self._assert_template(SpecLoader(), custom, u"abc") spec_loader = self._make_specloader()
self._assert_template(spec_loader, custom, u"abc")
def test_load__template__type_unicode(self): def test_load__template__type_unicode(self):
""" """
...@@ -196,7 +221,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -196,7 +221,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec() custom = TemplateSpec()
custom.template = u"abc" custom.template = u"abc"
self._assert_template(SpecLoader(), custom, u"abc") spec_loader = self._make_specloader()
self._assert_template(spec_loader, custom, u"abc")
def test_load__template__unicode_non_ascii(self): def test_load__template__unicode_non_ascii(self):
""" """
...@@ -206,7 +232,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -206,7 +232,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec() custom = TemplateSpec()
custom.template = u"é" custom.template = u"é"
self._assert_template(SpecLoader(), custom, u"é") spec_loader = self._make_specloader()
self._assert_template(spec_loader, custom, u"é")
def test_load__template__with_template_encoding(self): def test_load__template__with_template_encoding(self):
""" """
...@@ -216,10 +243,12 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -216,10 +243,12 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec() custom = TemplateSpec()
custom.template = u'é'.encode('utf-8') custom.template = u'é'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, self._assert_template, SpecLoader(), custom, u'é') spec_loader = self._make_specloader()
self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, u'é')
custom.template_encoding = 'utf-8' custom.template_encoding = 'utf-8'
self._assert_template(SpecLoader(), custom, u'é') self._assert_template(spec_loader, custom, u'é')
# TODO: make this test complete. # TODO: make this test complete.
def test_load__template__correct_loader(self): def test_load__template__correct_loader(self):
...@@ -254,8 +283,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -254,8 +283,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# Check that our unicode() above was called. # Check that our unicode() above was called.
self._assert_template(custom_loader, view, u'foo') self._assert_template(custom_loader, view, u'foo')
self.assertEquals(loader.s, "template-foo") self.assertEqual(loader.s, "template-foo")
self.assertEquals(loader.encoding, "encoding-foo") self.assertEqual(loader.encoding, "encoding-foo")
# TODO: migrate these tests into the SpecLoaderTests class. # TODO: migrate these tests into the SpecLoaderTests class.
...@@ -265,14 +294,13 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -265,14 +294,13 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# TemplateSpec attributes or something). # TemplateSpec attributes or something).
class TemplateSpecTests(unittest.TestCase): class TemplateSpecTests(unittest.TestCase):
# TODO: rename this method to _make_loader(). def _make_loader(self):
def _make_locator(self): return _make_specloader()
return SpecLoader()
def _assert_template_location(self, view, expected): def _assert_template_location(self, view, expected):
locator = self._make_locator() loader = self._make_loader()
actual = locator._find_relative(view) actual = loader._find_relative(view)
self.assertEquals(actual, expected) self.assertEqual(actual, expected)
def test_find_relative(self): def test_find_relative(self):
""" """
...@@ -328,43 +356,50 @@ class TemplateSpecTests(unittest.TestCase): ...@@ -328,43 +356,50 @@ class TemplateSpecTests(unittest.TestCase):
view.template_extension = 'txt' view.template_extension = 'txt'
self._assert_template_location(view, (None, 'sample_view.txt')) self._assert_template_location(view, (None, 'sample_view.txt'))
def _assert_paths(self, actual, expected):
"""
Assert that two paths are the same.
"""
self.assertEqual(actual, expected)
def test_find__with_directory(self): def test_find__with_directory(self):
""" """
Test _find() with a view that has a directory specified. Test _find() with a view that has a directory specified.
""" """
locator = self._make_locator() loader = self._make_loader()
view = SampleView() view = SampleView()
view.template_rel_path = 'foo/bar.txt' view.template_rel_path = 'foo/bar.txt'
self.assertTrue(locator._find_relative(view)[0] is not None) self.assertTrue(loader._find_relative(view)[0] is not None)
actual = locator._find(view) actual = loader._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) expected = os.path.join(DATA_DIR, 'foo/bar.txt')
self.assertEquals(actual, expected) self._assert_paths(actual, expected)
def test_find__without_directory(self): def test_find__without_directory(self):
""" """
Test _find() with a view that doesn't have a directory specified. Test _find() with a view that doesn't have a directory specified.
""" """
locator = self._make_locator() loader = self._make_loader()
view = SampleView() view = SampleView()
self.assertTrue(locator._find_relative(view)[0] is None) self.assertTrue(loader._find_relative(view)[0] is None)
actual = locator._find(view) actual = loader._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) expected = os.path.join(DATA_DIR, 'sample_view.mustache')
self.assertEquals(actual, expected) self._assert_paths(actual, expected)
def _assert_get_template(self, custom, expected): def _assert_get_template(self, custom, expected):
locator = self._make_locator() loader = self._make_loader()
actual = locator.load(custom) actual = loader.load(custom)
self.assertEquals(type(actual), unicode) self.assertEqual(type(actual), unicode)
self.assertEquals(actual, expected) self.assertEqual(actual, expected)
def test_get_template(self): def test_get_template(self):
""" """
......
[nosetests]
with-doctest=1
doctest-extension=rst
...@@ -2,21 +2,23 @@ ...@@ -2,21 +2,23 @@
# coding: utf-8 # coding: utf-8
""" """
This script supports installing and distributing pystache. This script supports publishing Pystache to PyPI.
Below are instructions to pystache maintainers on how to push a new This docstring contains instructions to Pystache maintainers on how
version of pystache to PyPI-- 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 http://pypi.python.org/pypi/pystache
Create a PyPI user account. The user account will need permissions to push create a PyPI user account if you do not already have one. The user account
to PyPI. A current "Package Index Owner" of pystache can grant you those will need permissions to push to PyPI. A current "Package Index Owner" of
permissions. Pystache can grant you those permissions.
When you have permissions, run the following (after preparing the release, 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-- If you get an error like the following--
...@@ -33,16 +35,78 @@ as described here, for example: ...@@ -33,16 +35,78 @@ as described here, for example:
http://docs.python.org/release/2.5.2/dist/pypirc.html 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 import os
import sys import sys
py_version = sys.version_info
# Distribute works with Python 2.3.5 and above:
# http://packages.python.org/distribute/setuptools.html#building-and-distributing-packages-with-distribute
if py_version < (2, 3, 5):
# TODO: this might not work yet.
import distutils as dist
from distutils import core
setup = core.setup
else:
import setuptools as dist
setup = dist.setup
# TODO: use the logging module instead of printing.
# TODO: include the following in a verbose mode.
# print("Using: version %s of %s" % (repr(dist.__version__), repr(dist)))
VERSION = '0.5.1' # Also change in pystache/__init__.py.
HISTORY_PATH = 'HISTORY.rst'
LICENSE_PATH = 'LICENSE'
README_PATH = 'README.rst'
CLASSIFIERS = (
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.4',
'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
)
def read(path):
"""
Read and return the contents of a text file as a unicode string.
try: """
from setuptools import setup # This function implementation was chosen to be compatible across Python 2/3.
except ImportError: f = open(path, 'rb')
from distutils.core import setup # We avoid use of the with keyword for Python 2.4 support.
try:
b = f.read()
finally:
f.close()
return b.decode('utf-8')
def publish(): def publish():
...@@ -58,37 +122,96 @@ def make_long_description(): ...@@ -58,37 +122,96 @@ def make_long_description():
Return the long description for the package. Return the long description for the package.
""" """
long_description = open('README.rst').read() + '\n\n' + open('HISTORY.rst').read() license = """\
License
=======
return long_description """ + read(LICENSE_PATH)
sections = [read(README_PATH), read(HISTORY_PATH), license]
return '\n\n'.join(sections)
if sys.argv[-1] == 'publish': if sys.argv[-1] == 'publish':
publish() publish()
sys.exit() sys.exit()
long_description = make_long_description() # We follow the guidance here for compatibility with using setuptools instead
# of Distribute under Python 2 (on the subject of new, unrecognized keyword
setup(name='pystache', # arguments to setup()):
version='0.5.0-rc', #
description='Mustache for Python', # http://packages.python.org/distribute/python3.html#note-on-compatibility-with-setuptools
long_description=long_description, #
author='Chris Wanstrath', if py_version < (3, ):
author_email='chris@ozmm.org', extra = {}
maintainer='Chris Jerdonek', else:
url='http://github.com/defunkt/pystache', extra = {
packages=['pystache'], # Causes 2to3 to be run during the build step.
license='MIT', 'use_2to3': True,
entry_points = { }
'console_scripts': ['pystache=pystache.commands:main'],
}, # We use the package simplejson for older Python versions since Python
classifiers = ( # does not contain the module json before 2.6:
'Development Status :: 4 - Beta', #
'License :: OSI Approved :: MIT License', # http://docs.python.org/library/json.html
'Programming Language :: Python', #
'Programming Language :: Python :: 2.4', # Moreover, simplejson stopped officially support for Python 2.4 in version 2.1.0:
'Programming Language :: Python :: 2.5', #
'Programming Language :: Python :: 2.6', # https://github.com/simplejson/simplejson/blob/master/CHANGES.txt
'Programming Language :: Python :: 2.7', #
) requires = []
) if py_version < (2, 5):
requires.append('simplejson<2.1')
elif py_version < (2, 6):
requires.append('simplejson')
INSTALL_REQUIRES = requires
# TODO: decide whether to use find_packages() instead. I'm not sure that
# find_packages() is available with distutils, for example.
PACKAGES = [
'pystache',
'pystache.commands',
# The following packages are only for testing.
'pystache.tests',
'pystache.tests.data',
'pystache.tests.data.locator',
'pystache.tests.examples',
]
def main(sys_argv):
long_description = make_long_description()
template_files = ['*.mustache', '*.txt']
setup(name='pystache',
version=VERSION,
license='MIT',
description='Mustache for Python',
long_description=long_description,
author='Chris Wanstrath',
author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache',
install_requires=INSTALL_REQUIRES,
packages=PACKAGES,
package_data = {
# Include template files so tests can be run.
'pystache.tests.data': template_files,
'pystache.tests.data.locator': template_files,
'pystache.tests.examples': template_files,
},
entry_points = {
'console_scripts': [
'pystache=pystache.commands.render:main',
'pystache-test=pystache.commands.test:main',
],
},
classifiers = CLASSIFIERS,
**extra
)
if __name__=='__main__':
main(sys.argv)
#!/usr/bin/env python
# coding: utf-8
"""
Runs project tests.
This script is a substitute for running--
python -m pystache.commands.test
It is useful in Python 2.4 because the -m flag does not accept subpackages
in Python 2.4:
http://docs.python.org/using/cmdline.html#cmdoption-m
"""
import sys
from pystache.commands import test
from pystache.tests.main import FROM_SOURCE_OPTION
def main(sys_argv=sys.argv):
sys.argv.insert(1, FROM_SOURCE_OPTION)
test.main()
if __name__=='__main__':
main()
...@@ -52,22 +52,3 @@ class AssertIsMixin: ...@@ -52,22 +52,3 @@ class AssertIsMixin:
def assertIs(self, first, second): def assertIs(self, first, second):
self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
class Attachable(object):
"""A trivial object that attaches all constructor named parameters as attributes.
For instance,
>>> o = Attachable(foo=42, size="of the universe")
>>> o.foo
42
>>> o.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 "A(%s)" % (", ".join("%s=%s" % (k, v)
for k, v in self.__args__.iteritems()))
# 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()
# A tox configuration file to test across multiple Python versions.
#
# http://pypi.python.org/pypi/tox
#
[tox]
envlist = py24,py25,py26,py27,py27-yaml,py27-noargs,py31,py32
[testenv]
# Change the working directory so that we don't import the pystache located
# in the original location.
changedir =
{envbindir}
commands =
pystache-test {toxinidir}/ext/spec/specs {toxinidir}
# Check that the spec tests work with PyYAML.
[testenv:py27-yaml]
basepython =
python2.7
deps =
PyYAML
changedir =
{envbindir}
commands =
pystache-test {toxinidir}/ext/spec/specs {toxinidir}
# Check that pystache-test works from an install with no arguments.
[testenv:py27-noargs]
basepython =
python2.7
changedir =
{envbindir}
commands =
pystache-test
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