summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore14
-rw-r--r--HISTORY.rst17
-rw-r--r--MANIFEST.in8
-rw-r--r--README.rst162
-rw-r--r--TODO.md8
-rw-r--r--examples/__init__.py0
-rw-r--r--examples/partials_with_lambdas.py6
-rw-r--r--pystache/__init__.py13
-rw-r--r--pystache/commands/__init__.py4
-rw-r--r--pystache/commands/render.py (renamed from pystache/commands.py)21
-rw-r--r--pystache/commands/test.py18
-rw-r--r--pystache/common.py26
-rw-r--r--pystache/context.py68
-rw-r--r--pystache/defaults.py15
-rw-r--r--pystache/init.py3
-rw-r--r--pystache/loader.py31
-rw-r--r--pystache/parsed.py2
-rw-r--r--pystache/parser.py63
-rw-r--r--pystache/renderengine.py52
-rw-r--r--pystache/renderer.py49
-rw-r--r--pystache/specloader.py (renamed from pystache/spec_loader.py)0
-rw-r--r--pystache/template_spec.py6
-rw-r--r--pystache/tests/__init__.py4
-rwxr-xr-xpystache/tests/benchmark.py (renamed from tests/benchmark.py)0
-rw-r--r--pystache/tests/common.py193
-rw-r--r--pystache/tests/data/__init__.py4
-rw-r--r--pystache/tests/data/ascii.mustache (renamed from tests/data/ascii.mustache)0
-rw-r--r--pystache/tests/data/duplicate.mustache (renamed from tests/data/duplicate.mustache)0
-rw-r--r--pystache/tests/data/locator/__init__.py4
-rw-r--r--pystache/tests/data/locator/duplicate.mustache (renamed from tests/data/locator/duplicate.mustache)0
-rw-r--r--pystache/tests/data/non_ascii.mustache (renamed from tests/data/non_ascii.mustache)0
-rw-r--r--pystache/tests/data/sample_view.mustache (renamed from tests/data/sample_view.mustache)0
-rw-r--r--pystache/tests/data/say_hello.mustache (renamed from tests/data/say_hello.mustache)0
-rw-r--r--pystache/tests/data/views.py (renamed from tests/data/views.py)5
-rw-r--r--pystache/tests/doctesting.py90
-rw-r--r--pystache/tests/examples/__init__.py4
-rw-r--r--pystache/tests/examples/comments.mustache (renamed from examples/comments.mustache)0
-rw-r--r--pystache/tests/examples/comments.py (renamed from examples/comments.py)6
-rw-r--r--pystache/tests/examples/complex.mustache (renamed from examples/complex.mustache)0
-rw-r--r--pystache/tests/examples/complex.py (renamed from examples/complex.py)6
-rw-r--r--pystache/tests/examples/delimiters.mustache (renamed from examples/delimiters.mustache)0
-rw-r--r--pystache/tests/examples/delimiters.py (renamed from examples/delimiters.py)6
-rw-r--r--pystache/tests/examples/double_section.mustache (renamed from examples/double_section.mustache)0
-rw-r--r--pystache/tests/examples/double_section.py (renamed from examples/double_section.py)6
-rw-r--r--pystache/tests/examples/escaped.mustache (renamed from examples/escaped.mustache)0
-rw-r--r--pystache/tests/examples/escaped.py (renamed from examples/escaped.py)6
-rw-r--r--pystache/tests/examples/extensionless (renamed from examples/extensionless)0
-rw-r--r--pystache/tests/examples/inner_partial.mustache (renamed from examples/inner_partial.mustache)0
-rw-r--r--pystache/tests/examples/inner_partial.txt (renamed from examples/inner_partial.txt)0
-rw-r--r--pystache/tests/examples/inverted.mustache (renamed from examples/inverted.mustache)0
-rw-r--r--pystache/tests/examples/inverted.py (renamed from examples/inverted.py)6
-rw-r--r--pystache/tests/examples/lambdas.mustache (renamed from examples/lambdas.mustache)0
-rw-r--r--pystache/tests/examples/lambdas.py (renamed from examples/lambdas.py)6
-rw-r--r--pystache/tests/examples/looping_partial.mustache (renamed from examples/looping_partial.mustache)0
-rw-r--r--pystache/tests/examples/nested_context.mustache (renamed from examples/nested_context.mustache)0
-rw-r--r--pystache/tests/examples/nested_context.py (renamed from examples/nested_context.py)6
-rw-r--r--pystache/tests/examples/partial_in_partial.mustache (renamed from examples/partial_in_partial.mustache)0
-rw-r--r--pystache/tests/examples/partial_with_lambda.mustache (renamed from examples/partial_with_lambda.mustache)0
-rw-r--r--pystache/tests/examples/partial_with_partial_and_lambda.mustache (renamed from examples/partial_with_partial_and_lambda.mustache)0
-rw-r--r--pystache/tests/examples/partials_with_lambdas.py12
-rw-r--r--pystache/tests/examples/readme.py (renamed from examples/readme.py)6
-rw-r--r--pystache/tests/examples/say_hello.mustache (renamed from examples/say_hello.mustache)0
-rw-r--r--pystache/tests/examples/simple.mustache (renamed from examples/simple.mustache)0
-rw-r--r--pystache/tests/examples/simple.py (renamed from examples/simple.py)8
-rw-r--r--pystache/tests/examples/tagless.mustache (renamed from examples/tagless.mustache)0
-rw-r--r--pystache/tests/examples/template_partial.mustache (renamed from examples/template_partial.mustache)0
-rw-r--r--pystache/tests/examples/template_partial.py (renamed from examples/template_partial.py)8
-rw-r--r--pystache/tests/examples/template_partial.txt (renamed from examples/template_partial.txt)0
-rw-r--r--pystache/tests/examples/unescaped.mustache (renamed from examples/unescaped.mustache)0
-rw-r--r--pystache/tests/examples/unescaped.py (renamed from examples/unescaped.py)6
-rw-r--r--pystache/tests/examples/unicode_input.mustache (renamed from examples/unicode_input.mustache)0
-rw-r--r--pystache/tests/examples/unicode_input.py (renamed from examples/unicode_input.py)6
-rw-r--r--pystache/tests/examples/unicode_output.mustache (renamed from examples/unicode_output.mustache)0
-rw-r--r--pystache/tests/examples/unicode_output.py (renamed from examples/unicode_output.py)5
-rw-r--r--pystache/tests/main.py155
-rw-r--r--pystache/tests/spectesting.py285
-rw-r--r--pystache/tests/test___init__.py36
-rw-r--r--pystache/tests/test_commands.py (renamed from tests/test_commands.py)4
-rw-r--r--pystache/tests/test_context.py (renamed from tests/test_context.py)199
-rw-r--r--pystache/tests/test_examples.py (renamed from tests/test_examples.py)11
-rw-r--r--pystache/tests/test_loader.py (renamed from tests/test_loader.py)92
-rw-r--r--pystache/tests/test_locator.py (renamed from tests/test_locator.py)53
-rw-r--r--pystache/tests/test_parser.py26
-rw-r--r--pystache/tests/test_pystache.py (renamed from tests/test_pystache.py)21
-rw-r--r--pystache/tests/test_renderengine.py (renamed from tests/test_renderengine.py)173
-rw-r--r--pystache/tests/test_renderer.py (renamed from tests/test_renderer.py)200
-rw-r--r--pystache/tests/test_simple.py (renamed from tests/test_simple.py)10
-rw-r--r--pystache/tests/test_specloader.py (renamed from tests/test_template_spec.py)123
-rw-r--r--setup.cfg3
-rw-r--r--setup.py201
-rw-r--r--test_pystache.py30
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/common.py19
-rw-r--r--tests/data/__init__.py0
-rw-r--r--tests/test_spec.py99
-rw-r--r--tox.ini34
96 files changed, 2094 insertions, 669 deletions
diff --git a/.gitignore b/.gitignore
index a66e33e..c9b5217 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,14 @@
*.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
-MANIFEST
dist
-
-.DS_Store \ No newline at end of file
+MANIFEST
+pystache.egg-info
diff --git a/HISTORY.rst b/HISTORY.rst
index dcf8a99..1169112 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,6 +1,22 @@
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)
------------------
@@ -93,5 +109,6 @@ Bug fixes:
* First release
+.. _2to3: http://docs.python.org/library/2to3.html
.. _issue #13: https://github.com/defunkt/pystache/issues/13
.. _Mustache spec: https://github.com/mustache/spec
diff --git a/MANIFEST.in b/MANIFEST.in
index c53247f..56a1d52 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,8 @@
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
diff --git a/README.rst b/README.rst
index c16611a..2f3ea76 100644
--- a/README.rst
+++ b/README.rst
@@ -23,18 +23,24 @@ Logo: `David Phillips`_
Requirements
============
-Pystache is tested with the following versions of Python:
+Pystache is tested with--
-* Python 2.4 (requires simplejson version 2.0.9 or earlier)
-* Python 2.5 (requires simplejson)
+* Python 2.4 (requires simplejson `version 2.0.9`_ or earlier)
+* Python 2.5 (requires simplejson_)
* Python 2.6
* Python 2.7
+* Python 3.1
+* Python 3.2
JSON support is needed only for the command-line interface and to run the
-spec tests. Python's json_ module is new as of Python 2.6. Python's
-simplejson_ package works with earlier versions of Python. Because
-simplejson stopped officially supporting Python 2.4 as of version 2.1.0,
-Python 2.4 requires an earlier version.
+spec tests. We require simplejson for earlier versions of Python since
+Python's json_ module was added in Python 2.6.
+
+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
@@ -43,6 +49,9 @@ Install It
::
pip install pystache
+ pystache-test
+
+To install and test from source (e.g. from GitHub), see the Develop section.
Use It
@@ -51,8 +60,8 @@ Use It
::
>>> import pystache
- >>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
- u'Hi Mom!'
+ >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
+ Hi Mom!
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)::
Like so::
- >>> from examples.readme import SayHello
+ >>> from pystache.tests.examples.readme import 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}}!
Pull it together::
>>> renderer = pystache.Renderer()
- >>> renderer.render(hello)
- u'Hello, Pizza!'
+ >>> print renderer.render(hello)
+ 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
-encodings).
+Unicode
+=======
+
+This section describes how Pystache handles unicode, strings, and encodings.
-Internally, Pystache uses `only unicode strings`_. For input, Pystache accepts
-both ``unicode`` and ``str`` strings. For output, Pystache's template
-rendering methods return only unicode.
+Internally, Pystache uses `only unicode strings`_ (``str`` in Python 3 and
+``unicode`` in Python 2). For input, Pystache accepts both unicode strings
+and byte strings (``bytes`` in Python 3 and ``str`` in Python 2). For output,
+Pystache's template rendering methods return only unicode.
-Pystache's ``Renderer`` class supports a number of attributes that control how
-Pystache converts ``str`` strings to unicode on input. These include the
+Pystache's ``Renderer`` class supports a number of attributes to control how
+Pystache converts byte strings to unicode on input. These include the
``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes.
The ``file_encoding`` attribute is the encoding the renderer uses to convert
to unicode any files read from the file system. Similarly, ``string_encoding``
-is the encoding the renderer uses to convert to unicode any other strings of
-type ``str`` encountered during the rendering process (e.g. context values
-of type ``str``).
+is the encoding the renderer uses to convert any other byte strings encountered
+during the rendering process into unicode (e.g. context values that are
+encoded byte strings).
The ``decode_errors`` attribute is what the renderer passes as the ``errors``
-argument to Python's `built-in unicode function`_ ``unicode()`` when
-converting. The valid values for this argument are ``strict``, ``ignore``,
-and ``replace``.
+argument to Python's built-in unicode-decoding function (``str()`` in Python 3
+and ``unicode()`` in Python 2). The valid values for this argument are
+``strict``, ``ignore``, and ``replace``.
Each of these attributes can be set via the ``Renderer`` class's constructor
using a keyword argument of the same name. See the Renderer class's
@@ -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.
-Test It
+Develop
=======
-nose_ works great! ::
+To test from a source distribution (without installing)-- ::
- pip install nose
- cd pystache
- nosetests
+ python test_pystache.py
+
+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
-to type, for example ::
+If you do not have all Python versions listed in ``tox.ini``-- ::
- 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 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
============
-As of November 2011, there's a mailing list, pystache@librelist.com.
-
-Archive: http://librelist.com/browser/pystache/
+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.
-Note: There's a bit of a delay in seeing the latest emails appear
-in the archive.
-
-Author
-======
+Authors
+=======
::
- >>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
- >>> pystache.render("{{author}} :: {{email}}", context)
- u'Chris Wanstrath :: chris@ozmm.org'
+ >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' }
+ >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context)
+ 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/
.. _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
.. _json: http://docs.python.org/library/json.html
+.. _mailing list: http://librelist.com/browser/pystache/
.. _Mustache: http://mustache.github.com/
.. _Mustache spec: https://github.com/mustache/spec
.. _mustache(5): http://mustache.github.com/mustache.5.html
@@ -176,7 +223,12 @@ Author
.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
.. _PyPI: http://pypi.python.org/pypi/pystache
.. _Pystache: https://github.com/defunkt/pystache
+.. _PyYAML: http://pypi.python.org/pypi/PyYAML
+.. _Renderer: https://github.com/defunkt/pystache/blob/master/pystache/renderer.py
.. _semantically versioned: http://semver.org
.. _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 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..54f2851
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,8 @@
+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.
diff --git a/examples/__init__.py b/examples/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/examples/__init__.py
+++ /dev/null
diff --git a/examples/partials_with_lambdas.py b/examples/partials_with_lambdas.py
deleted file mode 100644
index 463a3ce..0000000
--- a/examples/partials_with_lambdas.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from examples.lambdas import rot
-
-class PartialsWithLambdas(object):
-
- def rot(self):
- return rot \ No newline at end of file
diff --git a/pystache/__init__.py b/pystache/__init__.py
index daf7f52..5f5035d 100644
--- a/pystache/__init__.py
+++ b/pystache/__init__.py
@@ -1,2 +1,13 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
# 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.
diff --git a/pystache/commands/__init__.py b/pystache/commands/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/commands/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/pystache/commands.py b/pystache/commands/render.py
index 1801d40..23b19f8 100644
--- a/pystache/commands.py
+++ b/pystache/commands/render.py
@@ -13,7 +13,16 @@ try:
except:
# The json module is new in Python 2.6, whereas simplejson is
# 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.
# However, argparse is not available in Python 2.6 and earlier.
@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage):
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)
if template.endswith('.mustache'):
@@ -77,5 +91,4 @@ def main(sys_argv):
if __name__=='__main__':
- main(sys.argv)
-
+ main()
diff --git a/pystache/commands/test.py b/pystache/commands/test.py
new file mode 100644
index 0000000..0872453
--- /dev/null
+++ b/pystache/commands/test.py
@@ -0,0 +1,18 @@
+# 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()
diff --git a/pystache/common.py b/pystache/common.py
new file mode 100644
index 0000000..00f8a77
--- /dev/null
+++ b/pystache/common.py
@@ -0,0 +1,26 @@
+# 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()
diff --git a/pystache/context.py b/pystache/context.py
index d5570da..d0cba5d 100644
--- a/pystache/context.py
+++ b/pystache/context.py
@@ -1,15 +1,24 @@
# 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
# not being found on lookup. This lets us distinguish between the case
# of a key's value being None with the case of a key not being found --
# without having to rely on exceptions (e.g. KeyError) for flow control.
+#
+# 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()
@@ -24,7 +33,7 @@ def _get_value(item, key):
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('.')
@@ -39,7 +48,7 @@ def _get_value(item, key):
# (e.g. catching KeyError).
if key in item:
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
# the spec.
#
@@ -60,7 +69,26 @@ def _get_value(item, key):
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.
@@ -75,7 +103,7 @@ class Context(object):
(last in, first out).
Caution: this class does not currently support recursive nesting in
- that items in the stack cannot themselves be Context instances.
+ that items in the stack cannot themselves be ContextStack instances.
See the docstrings of the methods of this class for more details.
@@ -92,7 +120,7 @@ class Context(object):
stack in order so that, in particular, items at the end of
the argument list are queried first when querying the stack.
- Caution: items should not themselves be Context instances, as
+ Caution: items should not themselves be ContextStack instances, as
recursive nesting does not behave as one might expect.
"""
@@ -104,9 +132,9 @@ class Context(object):
For example--
- >>> context = Context({'alpha': 'abc'}, {'numeric': 123})
+ >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
>>> repr(context)
- "Context({'alpha': 'abc'}, {'numeric': 123})"
+ "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
"""
return "%s%s" % (self.__class__.__name__, tuple(self._stack))
@@ -114,18 +142,18 @@ class Context(object):
@staticmethod
def create(*context, **kwargs):
"""
- Build a Context instance from a sequence of context-like items.
+ Build a ContextStack instance from a sequence of context-like items.
- This factory-style method is more general than the Context class's
+ This factory-style method is more general than the ContextStack class's
constructor in that, unlike the constructor, the argument list
- can itself contain Context instances.
+ can itself contain ContextStack instances.
Here is an example illustrating various aspects of this method:
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
- >>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'})
+ >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
>>>
- >>> context = Context.create(obj1, None, obj2, mineral='gold')
+ >>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
>>>
>>> context.get('animal')
'cat'
@@ -136,7 +164,7 @@ class Context(object):
Arguments:
- *context: zero or more dictionaries, Context instances, or objects
+ *context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
arguments will be skipped. Items in the *context list are
added to the stack in order so that later items in the argument
@@ -152,12 +180,12 @@ class Context(object):
"""
items = context
- context = Context()
+ context = ContextStack()
for item in items:
if item is None:
continue
- if isinstance(item, Context):
+ if isinstance(item, ContextStack):
context._stack.extend(item._stack)
else:
context.push(item)
@@ -226,9 +254,9 @@ class Context(object):
>>>
>>> dct['greet'] is obj.greet
True
- >>> Context(dct).get('greet') #doctest: +ELLIPSIS
+ >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
- >>> Context(obj).get('greet')
+ >>> ContextStack(obj).get('greet')
'Hi Bob!'
TODO: explain the rationale for this difference in treatment.
@@ -270,4 +298,4 @@ class Context(object):
Return a copy of this instance.
"""
- return Context(*self._stack)
+ return ContextStack(*self._stack)
diff --git a/pystache/defaults.py b/pystache/defaults.py
index b696410..fcd04c3 100644
--- a/pystache/defaults.py
+++ b/pystache/defaults.py
@@ -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 sys
@@ -39,12 +44,14 @@ SEARCH_DIRS = [os.curdir] # i.e. ['.']
# rendering templates (e.g. for tags enclosed in double braces).
# Only unicode strings will be passed to this function.
#
-# The quote=True argument causes double quotes to be escaped,
-# but not single quotes:
+# The quote=True argument causes double but not single quotes to be escaped
+# 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/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.
TEMPLATE_EXTENSION = 'mustache'
diff --git a/pystache/init.py b/pystache/init.py
index b285a5c..e9d854d 100644
--- a/pystache/init.py
+++ b/pystache/init.py
@@ -9,9 +9,6 @@ from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
-__all__ = ['render', 'Renderer', 'TemplateSpec']
-
-
def render(template, context=None, **kwargs):
"""
Return the given template string rendered using the given context.
diff --git a/pystache/loader.py b/pystache/loader.py
index bcba71b..0fdadc5 100644
--- a/pystache/loader.py
+++ b/pystache/loader.py
@@ -8,18 +8,24 @@ This module provides a Loader class for locating and reading templates.
import os
import sys
+from pystache import common
from pystache import defaults
from pystache.locator import Locator
-def _to_unicode(s, encoding=None):
- """
- Raises a TypeError exception if the given string is already unicode.
+# We make a function so that the current defaults take effect.
+# TODO: revisit whether this is necessary.
- """
- if encoding is None:
- encoding = defaults.STRING_ENCODING
- return unicode(s, encoding, defaults.DECODE_ERRORS)
+def _make_to_unicode():
+ def to_unicode(s, encoding=None):
+ """
+ Raises a TypeError exception if the given string is already unicode.
+
+ """
+ if encoding is None:
+ encoding = defaults.STRING_ENCODING
+ return unicode(s, encoding, defaults.DECODE_ERRORS)
+ return to_unicode
class Loader(object):
@@ -67,7 +73,7 @@ class Loader(object):
search_dirs = defaults.SEARCH_DIRS
if to_unicode is None:
- to_unicode = _to_unicode
+ to_unicode = _make_to_unicode()
self.extension = extension
self.file_encoding = file_encoding
@@ -106,17 +112,12 @@ class Loader(object):
Read the template at the given path, and return it as a unicode string.
"""
- # We avoid use of the with keyword for Python 2.4 support.
- f = open(path, 'r')
- try:
- text = f.read()
- finally:
- f.close()
+ b = common.read(path)
if encoding is None:
encoding = self.file_encoding
- return self.unicode(text, encoding)
+ return self.unicode(b, encoding)
# TODO: unit-test this method.
def load_name(self, name):
diff --git a/pystache/parsed.py b/pystache/parsed.py
index 5418ec1..552af55 100644
--- a/pystache/parsed.py
+++ b/pystache/parsed.py
@@ -17,7 +17,7 @@ class ParsedTemplate(object):
parse_tree: a list, each element of which is either--
(1) a unicode string, or
- (2) a "rendering" callable that accepts a Context instance
+ (2) a "rendering" callable that accepts a ContextStack instance
and returns a unicode string.
The possible rendering callables are the return values of the
diff --git a/pystache/parser.py b/pystache/parser.py
index d07ebf6..2b97405 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -9,7 +9,7 @@ This module is only meant for internal use by the renderengine module.
import re
-from parsed import ParsedTemplate
+from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = ('{{', '}}')
@@ -17,7 +17,13 @@ END_OF_LINE_CHARACTERS = ['\r', '\n']
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,
# excluding "=" and "{".
@@ -74,19 +80,25 @@ class Parser(object):
self._delimiters = delimiters
self.compile_template_re()
- def parse(self, template, index=0, section_key=None):
+ def parse(self, template, start_index=0, section_key=None):
"""
- Parse a template string into a ParsedTemplate instance.
+ Parse a template string starting at some index.
This method uses the current tag delimiter.
Arguments:
- template: a template string of type unicode.
+ template: a unicode string that is the template to parse.
+
+ index: the index at which to start parsing.
+
+ Returns:
+
+ a ParsedTemplate instance.
"""
parse_tree = []
- start_index = index
+ index = start_index
while True:
match = self._template_re.search(template, index)
@@ -131,9 +143,9 @@ class Parser(object):
if tag_type == '/':
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)
@@ -142,10 +154,33 @@ class Parser(object):
return ParsedTemplate(parse_tree)
- def _parse_section(self, template, index_start, section_key):
- parsed_template, template, index_end = self.parse(template=template, index=index_start, section_key=section_key)
+ def _parse_section(self, template, start_index, section_key):
+ """
+ Parse the contents of a template section.
+
+ Arguments:
+
+ template: a unicode template string.
+
+ start_index: the string index at which the section contents begin.
+
+ section_key: the tag key of the section.
+
+ Returns: a 3-tuple:
+
+ parsed_section: the section contents parsed as a ParsedTemplate
+ instance.
+
+ content_end_index: the string index after the section contents.
+
+ end_index: the string index after the closing section tag (and
+ including any trailing newlines).
+
+ """
+ parsed_section, content_end_index, end_index = \
+ self.parse(template=template, start_index=start_index, section_key=section_key)
- return parsed_template, template, index_end
+ return parsed_section, template[start_index:content_end_index], end_index
def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
@@ -170,12 +205,12 @@ class Parser(object):
elif tag_type == '#':
- parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
- func = engine._make_get_section(tag_key, parsed_section, template, self._delimiters)
+ parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
+ func = engine._make_get_section(tag_key, parsed_section, section_contents, self._delimiters)
elif tag_type == '^':
- parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
+ parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_inverse(tag_key, parsed_section)
elif tag_type == '>':
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index 4361dca..9e4da11 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -7,7 +7,8 @@ Defines a class responsible for rendering logic.
import re
-from parser import Parser
+from pystache.context import resolve
+from pystache.parser import Parser
class RenderEngine(object):
@@ -55,7 +56,7 @@ class RenderEngine(object):
this class will not pass tag values to literal prior to passing
them to this function. This allows for more flexibility,
for example using a custom escape function that handles
- incoming strings of type markupssafe.Markup differently
+ incoming strings of type markupsafe.Markup differently
from plain unicode strings.
"""
@@ -68,16 +69,7 @@ class RenderEngine(object):
Get a value from the given context as a basestring instance.
"""
- val = context.get(tag_name)
-
- # We use "==" rather than "is" to compare integers, as using "is"
- # relies on an implementation detail of CPython. The test about
- # rendering zeroes failed while using PyPy when using "is".
- # See issue #34: https://github.com/defunkt/pystache/issues/34
- if not val and val != 0:
- if tag_name != '.':
- return ''
- val = context.top()
+ val = resolve(context, tag_name)
if callable(val):
# According to the spec:
@@ -142,6 +134,8 @@ class RenderEngine(object):
Returns a string with type unicode.
"""
+ # TODO: is there a bug because we are not using the same
+ # logic as in _get_string_value()?
data = context.get(name)
if data:
return u''
@@ -167,9 +161,33 @@ class RenderEngine(object):
# TODO: should we check the arity?
template = data(template)
parsed_template = self._parse(template, delimiters=delims)
- data = [ data ]
- elif not hasattr(data, '__iter__') or isinstance(data, dict):
- data = [ data ]
+ # Lambdas special case section rendering and bypass pushing
+ # the data value onto the context stack. Also see--
+ #
+ # 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 = []
for element in data:
@@ -202,7 +220,7 @@ class RenderEngine(object):
Arguments:
template: a template string of type unicode.
- context: a Context instance.
+ context: a ContextStack instance.
"""
# We keep this type-check as an added check because this method is
@@ -225,7 +243,7 @@ class RenderEngine(object):
template: a template string of type unicode (but not a proper
subclass of unicode).
- context: a Context instance.
+ context: a ContextStack instance.
"""
# Be strict but not too strict. In other words, accept str instead
diff --git a/pystache/renderer.py b/pystache/renderer.py
index 5bd2a3f..26f271f 100644
--- a/pystache/renderer.py
+++ b/pystache/renderer.py
@@ -5,14 +5,27 @@ This module provides a Renderer class to render templates.
"""
+import sys
+
from pystache import defaults
-from pystache.context import Context
+from pystache.context import ContextStack
from pystache.loader import Loader
from pystache.renderengine import RenderEngine
-from pystache.spec_loader import SpecLoader
+from pystache.specloader import SpecLoader
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):
"""
@@ -27,8 +40,9 @@ class Renderer(object):
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials)
- >>> renderer.render('{{>partial}}', {'thing': 'world'})
- u'Hello, world!'
+ >>> # We apply print to make the test work in Python 3 after 2to3.
+ >>> print renderer.render('{{>partial}}', {'thing': 'world'})
+ Hello, world!
"""
@@ -64,10 +78,10 @@ class Renderer(object):
this class will only pass it unicode strings. The constructor
assigns this function to the constructed instance's escape()
method.
- The argument defaults to `cgi.escape(s, quote=True)`. To
- disable escaping entirely, one can pass `lambda u: u` as the
- escape function, for example. One may also wish to consider
- using markupsafe's escape function: markupsafe.escape().
+ To disable escaping entirely, one can pass `lambda u: u`
+ as the escape function, for example. One may also wish to
+ consider using markupsafe's escape function: markupsafe.escape().
+ This argument defaults to the package default.
file_encoding: the name of the default encoding to use when reading
template files. All templates are converted to unicode prior
@@ -160,9 +174,16 @@ class Renderer(object):
"""
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:
@@ -178,7 +199,7 @@ class Renderer(object):
# TODO: Wrap UnicodeDecodeErrors with a message about setting
# 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):
"""
@@ -256,7 +277,7 @@ class Renderer(object):
# RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template)
- context = Context.create(*context, **kwargs)
+ context = ContextStack.create(*context, **kwargs)
self._context = context
engine = self._make_render_engine()
@@ -317,7 +338,7 @@ class Renderer(object):
uses the passed object as the first element of the context stack
when rendering.
- *context: zero or more dictionaries, Context instances, or objects
+ *context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
arguments are skipped. Items in the *context list are added to
the context stack in order so that later items in the argument
@@ -329,7 +350,7 @@ class Renderer(object):
all items in the *context list.
"""
- if isinstance(template, basestring):
+ if isinstance(template, _STRING_TYPES):
return self._render_string(template, *context, **kwargs)
# Otherwise, we assume the template is an object.
diff --git a/pystache/spec_loader.py b/pystache/specloader.py
index 3cb0f1a..3cb0f1a 100644
--- a/pystache/spec_loader.py
+++ b/pystache/specloader.py
diff --git a/pystache/template_spec.py b/pystache/template_spec.py
index c33f30b..76ce784 100644
--- a/pystache/template_spec.py
+++ b/pystache/template_spec.py
@@ -1,7 +1,11 @@
# 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".
"""
diff --git a/pystache/tests/__init__.py b/pystache/tests/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/tests/benchmark.py b/pystache/tests/benchmark.py
index d46e973..d46e973 100755
--- a/tests/benchmark.py
+++ b/pystache/tests/benchmark.py
diff --git a/pystache/tests/common.py b/pystache/tests/common.py
new file mode 100644
index 0000000..a99e709
--- /dev/null
+++ b/pystache/tests/common.py
@@ -0,0 +1,193 @@
+# 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
+
diff --git a/pystache/tests/data/__init__.py b/pystache/tests/data/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/data/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/tests/data/ascii.mustache b/pystache/tests/data/ascii.mustache
index e86737b..e86737b 100644
--- a/tests/data/ascii.mustache
+++ b/pystache/tests/data/ascii.mustache
diff --git a/tests/data/duplicate.mustache b/pystache/tests/data/duplicate.mustache
index a0515e3..a0515e3 100644
--- a/tests/data/duplicate.mustache
+++ b/pystache/tests/data/duplicate.mustache
diff --git a/pystache/tests/data/locator/__init__.py b/pystache/tests/data/locator/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/data/locator/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/tests/data/locator/duplicate.mustache b/pystache/tests/data/locator/duplicate.mustache
index a0515e3..a0515e3 100644
--- a/tests/data/locator/duplicate.mustache
+++ b/pystache/tests/data/locator/duplicate.mustache
diff --git a/tests/data/non_ascii.mustache b/pystache/tests/data/non_ascii.mustache
index bd69b61..bd69b61 100644
--- a/tests/data/non_ascii.mustache
+++ b/pystache/tests/data/non_ascii.mustache
diff --git a/tests/data/sample_view.mustache b/pystache/tests/data/sample_view.mustache
index e86737b..e86737b 100644
--- a/tests/data/sample_view.mustache
+++ b/pystache/tests/data/sample_view.mustache
diff --git a/tests/data/say_hello.mustache b/pystache/tests/data/say_hello.mustache
index 84ab4c9..84ab4c9 100644
--- a/tests/data/say_hello.mustache
+++ b/pystache/tests/data/say_hello.mustache
diff --git a/tests/data/views.py b/pystache/tests/data/views.py
index 4d9df02..0b96309 100644
--- a/tests/data/views.py
+++ b/pystache/tests/data/views.py
@@ -1,5 +1,10 @@
# coding: utf-8
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
class SayHello(object):
diff --git a/pystache/tests/doctesting.py b/pystache/tests/doctesting.py
new file mode 100644
index 0000000..469c81e
--- /dev/null
+++ b/pystache/tests/doctesting.py
@@ -0,0 +1,90 @@
+# 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
diff --git a/pystache/tests/examples/__init__.py b/pystache/tests/examples/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/examples/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/examples/comments.mustache b/pystache/tests/examples/comments.mustache
index 2a2a08b..2a2a08b 100644
--- a/examples/comments.mustache
+++ b/pystache/tests/examples/comments.mustache
diff --git a/examples/comments.py b/pystache/tests/examples/comments.py
index f9c3125..8d75f88 100644
--- a/examples/comments.py
+++ b/pystache/tests/examples/comments.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class Comments(object):
def title(self):
diff --git a/examples/complex.mustache b/pystache/tests/examples/complex.mustache
index 6de758b..6de758b 100644
--- a/examples/complex.mustache
+++ b/pystache/tests/examples/complex.mustache
diff --git a/examples/complex.py b/pystache/tests/examples/complex.py
index e3f1767..c653db0 100644
--- a/examples/complex.py
+++ b/pystache/tests/examples/complex.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class Complex(object):
def header(self):
diff --git a/examples/delimiters.mustache b/pystache/tests/examples/delimiters.mustache
index 92bea6d..92bea6d 100644
--- a/examples/delimiters.mustache
+++ b/pystache/tests/examples/delimiters.mustache
diff --git a/examples/delimiters.py b/pystache/tests/examples/delimiters.py
index a132ed0..a31ec1b 100644
--- a/examples/delimiters.py
+++ b/pystache/tests/examples/delimiters.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class Delimiters(object):
def first(self):
diff --git a/examples/double_section.mustache b/pystache/tests/examples/double_section.mustache
index 61f1917..61f1917 100644
--- a/examples/double_section.mustache
+++ b/pystache/tests/examples/double_section.mustache
diff --git a/examples/double_section.py b/pystache/tests/examples/double_section.py
index 0bec602..c9736e4 100644
--- a/examples/double_section.py
+++ b/pystache/tests/examples/double_section.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class DoubleSection(object):
def t(self):
diff --git a/examples/escaped.mustache b/pystache/tests/examples/escaped.mustache
index 8be4ccb..8be4ccb 100644
--- a/examples/escaped.mustache
+++ b/pystache/tests/examples/escaped.mustache
diff --git a/examples/escaped.py b/pystache/tests/examples/escaped.py
index fed1705..5d72dde 100644
--- a/examples/escaped.py
+++ b/pystache/tests/examples/escaped.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class Escaped(object):
def title(self):
diff --git a/examples/extensionless b/pystache/tests/examples/extensionless
index 452c9fe..452c9fe 100644
--- a/examples/extensionless
+++ b/pystache/tests/examples/extensionless
diff --git a/examples/inner_partial.mustache b/pystache/tests/examples/inner_partial.mustache
index 2863764..2863764 100644
--- a/examples/inner_partial.mustache
+++ b/pystache/tests/examples/inner_partial.mustache
diff --git a/examples/inner_partial.txt b/pystache/tests/examples/inner_partial.txt
index 650c959..650c959 100644
--- a/examples/inner_partial.txt
+++ b/pystache/tests/examples/inner_partial.txt
diff --git a/examples/inverted.mustache b/pystache/tests/examples/inverted.mustache
index fbea98d..fbea98d 100644
--- a/examples/inverted.mustache
+++ b/pystache/tests/examples/inverted.mustache
diff --git a/examples/inverted.py b/pystache/tests/examples/inverted.py
index 2a05302..12212b4 100644
--- a/examples/inverted.py
+++ b/pystache/tests/examples/inverted.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
class Inverted(object):
diff --git a/examples/lambdas.mustache b/pystache/tests/examples/lambdas.mustache
index 9dffca5..9dffca5 100644
--- a/examples/lambdas.mustache
+++ b/pystache/tests/examples/lambdas.mustache
diff --git a/examples/lambdas.py b/pystache/tests/examples/lambdas.py
index 653531d..3bc08ff 100644
--- a/examples/lambdas.py
+++ b/pystache/tests/examples/lambdas.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
def rot(s, n=13):
diff --git a/examples/looping_partial.mustache b/pystache/tests/examples/looping_partial.mustache
index 577f736..577f736 100644
--- a/examples/looping_partial.mustache
+++ b/pystache/tests/examples/looping_partial.mustache
diff --git a/examples/nested_context.mustache b/pystache/tests/examples/nested_context.mustache
index ce570d6..ce570d6 100644
--- a/examples/nested_context.mustache
+++ b/pystache/tests/examples/nested_context.mustache
diff --git a/examples/nested_context.py b/pystache/tests/examples/nested_context.py
index 4626ac0..a2661b9 100644
--- a/examples/nested_context.py
+++ b/pystache/tests/examples/nested_context.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
class NestedContext(TemplateSpec):
diff --git a/examples/partial_in_partial.mustache b/pystache/tests/examples/partial_in_partial.mustache
index c61ceb1..c61ceb1 100644
--- a/examples/partial_in_partial.mustache
+++ b/pystache/tests/examples/partial_in_partial.mustache
diff --git a/examples/partial_with_lambda.mustache b/pystache/tests/examples/partial_with_lambda.mustache
index 2989f56..2989f56 100644
--- a/examples/partial_with_lambda.mustache
+++ b/pystache/tests/examples/partial_with_lambda.mustache
diff --git a/examples/partial_with_partial_and_lambda.mustache b/pystache/tests/examples/partial_with_partial_and_lambda.mustache
index 0729e10..0729e10 100644
--- a/examples/partial_with_partial_and_lambda.mustache
+++ b/pystache/tests/examples/partial_with_partial_and_lambda.mustache
diff --git a/pystache/tests/examples/partials_with_lambdas.py b/pystache/tests/examples/partials_with_lambdas.py
new file mode 100644
index 0000000..638aa36
--- /dev/null
+++ b/pystache/tests/examples/partials_with_lambdas.py
@@ -0,0 +1,12 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache.tests.examples.lambdas import rot
+
+class PartialsWithLambdas(object):
+
+ def rot(self):
+ return rot
diff --git a/examples/readme.py b/pystache/tests/examples/readme.py
index 23b44f5..8dcee43 100644
--- a/examples/readme.py
+++ b/pystache/tests/examples/readme.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class SayHello(object):
def to(self):
return "Pizza"
diff --git a/examples/say_hello.mustache b/pystache/tests/examples/say_hello.mustache
index 7d8dfea..7d8dfea 100644
--- a/examples/say_hello.mustache
+++ b/pystache/tests/examples/say_hello.mustache
diff --git a/examples/simple.mustache b/pystache/tests/examples/simple.mustache
index 9214dab..9214dab 100644
--- a/examples/simple.mustache
+++ b/pystache/tests/examples/simple.mustache
diff --git a/examples/simple.py b/pystache/tests/examples/simple.py
index 3252a81..ea82e9d 100644
--- a/examples/simple.py
+++ b/pystache/tests/examples/simple.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
class Simple(TemplateSpec):
@@ -6,4 +12,4 @@ class Simple(TemplateSpec):
return "pizza"
def blank(self):
- pass
+ return ''
diff --git a/examples/tagless.mustache b/pystache/tests/examples/tagless.mustache
index ad4dd31..ad4dd31 100644
--- a/examples/tagless.mustache
+++ b/pystache/tests/examples/tagless.mustache
diff --git a/examples/template_partial.mustache b/pystache/tests/examples/template_partial.mustache
index 03f76cf..03f76cf 100644
--- a/examples/template_partial.mustache
+++ b/pystache/tests/examples/template_partial.mustache
diff --git a/examples/template_partial.py b/pystache/tests/examples/template_partial.py
index e96c83b..1c4d1a0 100644
--- a/examples/template_partial.py
+++ b/pystache/tests/examples/template_partial.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
class TemplatePartial(TemplateSpec):
@@ -18,4 +24,4 @@ class TemplatePartial(TemplateSpec):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
def thing(self):
- return self._context_get('prop') \ No newline at end of file
+ return self._context_get('prop')
diff --git a/examples/template_partial.txt b/pystache/tests/examples/template_partial.txt
index d9b5f6e..d9b5f6e 100644
--- a/examples/template_partial.txt
+++ b/pystache/tests/examples/template_partial.txt
diff --git a/examples/unescaped.mustache b/pystache/tests/examples/unescaped.mustache
index 9982708..9982708 100644
--- a/examples/unescaped.mustache
+++ b/pystache/tests/examples/unescaped.mustache
diff --git a/examples/unescaped.py b/pystache/tests/examples/unescaped.py
index 67c12ca..92889af 100644
--- a/examples/unescaped.py
+++ b/pystache/tests/examples/unescaped.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
class Unescaped(object):
def title(self):
diff --git a/examples/unicode_input.mustache b/pystache/tests/examples/unicode_input.mustache
index f654cd1..f654cd1 100644
--- a/examples/unicode_input.mustache
+++ b/pystache/tests/examples/unicode_input.mustache
diff --git a/examples/unicode_input.py b/pystache/tests/examples/unicode_input.py
index 2c10fcb..d045757 100644
--- a/examples/unicode_input.py
+++ b/pystache/tests/examples/unicode_input.py
@@ -1,3 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
from pystache import TemplateSpec
class UnicodeInput(TemplateSpec):
diff --git a/examples/unicode_output.mustache b/pystache/tests/examples/unicode_output.mustache
index 8495f56..8495f56 100644
--- a/examples/unicode_output.mustache
+++ b/pystache/tests/examples/unicode_output.mustache
diff --git a/examples/unicode_output.py b/pystache/tests/examples/unicode_output.py
index d5579c3..da0e1d2 100644
--- a/examples/unicode_output.py
+++ b/pystache/tests/examples/unicode_output.py
@@ -1,5 +1,10 @@
# encoding: utf-8
+"""
+TODO: add a docstring.
+
+"""
+
class UnicodeOutput(object):
def name(self):
diff --git a/pystache/tests/main.py b/pystache/tests/main.py
new file mode 100644
index 0000000..de56c44
--- /dev/null
+++ b/pystache/tests/main.py
@@ -0,0 +1,155 @@
+# 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)
diff --git a/pystache/tests/spectesting.py b/pystache/tests/spectesting.py
new file mode 100644
index 0000000..ec8a08d
--- /dev/null
+++ b/pystache/tests/spectesting.py
@@ -0,0 +1,285 @@
+# 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)
diff --git a/pystache/tests/test___init__.py b/pystache/tests/test___init__.py
new file mode 100644
index 0000000..d4f3526
--- /dev/null
+++ b/pystache/tests/test___init__.py
@@ -0,0 +1,36 @@
+# 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)
diff --git a/tests/test_commands.py b/pystache/tests/test_commands.py
index f1817e7..2529d25 100644
--- a/tests/test_commands.py
+++ b/pystache/tests/test_commands.py
@@ -8,7 +8,7 @@ Unit tests of commands.py.
import sys
import unittest
-from pystache.commands import main
+from pystache.commands.render import main
ORIGINAL_STDOUT = sys.stdout
@@ -39,7 +39,7 @@ class CommandsTestCase(unittest.TestCase):
"""
actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
- self.assertEquals(actual, u"Hi world\n")
+ self.assertEqual(actual, u"Hi world\n")
def tearDown(self):
sys.stdout = ORIGINAL_STDOUT
diff --git a/tests/test_context.py b/pystache/tests/test_context.py
index 7597fa8..03377f7 100644
--- a/tests/test_context.py
+++ b/pystache/tests/test_context.py
@@ -10,8 +10,8 @@ import unittest
from pystache.context import _NOT_FOUND
from pystache.context import _get_value
-from pystache.context import Context
-from tests.common import AssertIsMixin, Attachable
+from pystache.context import ContextStack
+from pystache.tests.common import AssertIsMixin, Attachable
class SimpleObject(object):
@@ -58,7 +58,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
"""
item = {"foo": "bar"}
- self.assertEquals(_get_value(item, "foo"), "bar")
+ self.assertEqual(_get_value(item, "foo"), "bar")
def test_dictionary__callable_not_called(self):
"""
@@ -69,7 +69,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
return "bar"
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)
def test_dictionary__key_missing(self):
@@ -85,9 +85,11 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
Test that dictionary attributes are not checked.
"""
- item = {}
- attr_name = "keys"
- self.assertEquals(getattr(item, attr_name)(), [])
+ item = {1: 2, 3: 4}
+ # I was not able to find a "public" attribute of dict that is
+ # the same across Python 2/3.
+ attr_name = "__len__"
+ self.assertEqual(getattr(item, attr_name)(), 2)
self.assertNotFound(item, attr_name)
def test_dictionary__dict_subclass(self):
@@ -100,7 +102,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
item = DictSubclass()
item["foo"] = "bar"
- self.assertEquals(_get_value(item, "foo"), "bar")
+ self.assertEqual(_get_value(item, "foo"), "bar")
### Case: the item is an object.
@@ -110,7 +112,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
"""
item = SimpleObject()
- self.assertEquals(_get_value(item, "foo"), "bar")
+ self.assertEqual(_get_value(item, "foo"), "bar")
def test_object__attribute_missing(self):
"""
@@ -126,7 +128,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
"""
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):
"""
@@ -134,7 +136,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
"""
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):
"""
@@ -142,7 +144,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
"""
item = DictLike()
- self.assertEquals(item["foo"], "bar")
+ self.assertEqual(item["foo"], "bar")
self.assertNotFound(item, "foo")
### Case: the item is an instance of a built-in type.
@@ -154,25 +156,20 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
"""
class MyInt(int): pass
- item1 = MyInt(10)
- item2 = 10
-
- try:
- item2.real
- except AttributeError:
- # Then skip this unit test. The numeric type hierarchy was
- # added only in Python 2.6, in which case integers inherit
- # from complex numbers the "real" attribute, etc:
- #
- # http://docs.python.org/library/numbers.html
- #
- return
+ cust_int = MyInt(10)
+ pure_int = 10
- self.assertEquals(item1.real, 10)
- self.assertEquals(item2.real, 10)
+ # We have to use a built-in method like __neg__ because "public"
+ # 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.assertNotFound(item2, 'real')
+ self.assertEqual(_get_value(cust_int, '__neg__'), -10)
+ self.assertNotFound(pure_int, '__neg__')
def test_built_in_type__string(self):
"""
@@ -184,10 +181,10 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
item1 = MyStr('abc')
item2 = 'abc'
- self.assertEquals(item1.upper(), 'ABC')
- self.assertEquals(item2.upper(), 'ABC')
+ self.assertEqual(item1.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')
def test_built_in_type__list(self):
@@ -200,17 +197,17 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
item1 = MyList([1, 2, 3])
item2 = [1, 2, 3]
- self.assertEquals(item1.pop(), 3)
- self.assertEquals(item2.pop(), 3)
+ self.assertEqual(item1.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')
-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):
Check that passing nothing to __init__() raises no exception.
"""
- context = Context()
+ context = ContextStack()
def test_init__many_elements(self):
"""
Check that passing more than two items to __init__() raises no exception.
"""
- context = Context({}, {}, {})
+ context = ContextStack({}, {}, {})
def test__repr(self):
- context = Context()
- self.assertEquals(repr(context), 'Context()')
+ context = ContextStack()
+ self.assertEqual(repr(context), 'ContextStack()')
- context = Context({'foo': 'bar'})
- self.assertEquals(repr(context), "Context({'foo': 'bar'},)")
+ context = ContextStack({'foo': 'bar'})
+ self.assertEqual(repr(context), "ContextStack({'foo': 'bar'},)")
- context = Context({'foo': 'bar'}, {'abc': 123})
- self.assertEquals(repr(context), "Context({'foo': 'bar'}, {'abc': 123})")
+ context = ContextStack({'foo': 'bar'}, {'abc': 123})
+ self.assertEqual(repr(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
def test__str(self):
- context = Context()
- self.assertEquals(str(context), 'Context()')
+ context = ContextStack()
+ self.assertEqual(str(context), 'ContextStack()')
- context = Context({'foo': 'bar'})
- self.assertEquals(str(context), "Context({'foo': 'bar'},)")
+ context = ContextStack({'foo': 'bar'})
+ self.assertEqual(str(context), "ContextStack({'foo': 'bar'},)")
- context = Context({'foo': 'bar'}, {'abc': 123})
- self.assertEquals(str(context), "Context({'foo': 'bar'}, {'abc': 123})")
+ context = ContextStack({'foo': 'bar'}, {'abc': 123})
+ self.assertEqual(str(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
## Test the static create() method.
@@ -255,16 +252,16 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing a dictionary.
"""
- context = Context.create({'foo': 'bar'})
- self.assertEquals(context.get('foo'), 'bar')
+ context = ContextStack.create({'foo': 'bar'})
+ self.assertEqual(context.get('foo'), 'bar')
def test_create__none(self):
"""
Test passing None.
"""
- context = Context.create({'foo': 'bar'}, None)
- self.assertEquals(context.get('foo'), 'bar')
+ context = ContextStack.create({'foo': 'bar'}, None)
+ self.assertEqual(context.get('foo'), 'bar')
def test_create__object(self):
"""
@@ -273,56 +270,56 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
class Foo(object):
foo = 'bar'
- context = Context.create(Foo())
- self.assertEquals(context.get('foo'), 'bar')
+ context = ContextStack.create(Foo())
+ self.assertEqual(context.get('foo'), 'bar')
def test_create__context(self):
"""
- Test passing a Context instance.
+ Test passing a ContextStack instance.
"""
- obj = Context({'foo': 'bar'})
- context = Context.create(obj)
- self.assertEquals(context.get('foo'), 'bar')
+ obj = ContextStack({'foo': 'bar'})
+ context = ContextStack.create(obj)
+ self.assertEqual(context.get('foo'), 'bar')
def test_create__kwarg(self):
"""
Test passing a keyword argument.
"""
- context = Context.create(foo='bar')
- self.assertEquals(context.get('foo'), 'bar')
+ context = ContextStack.create(foo='bar')
+ self.assertEqual(context.get('foo'), 'bar')
def test_create__precedence_positional(self):
"""
Test precedence of positional arguments.
"""
- context = Context.create({'foo': 'bar'}, {'foo': 'buzz'})
- self.assertEquals(context.get('foo'), 'buzz')
+ context = ContextStack.create({'foo': 'bar'}, {'foo': 'buzz'})
+ self.assertEqual(context.get('foo'), 'buzz')
def test_create__precedence_keyword(self):
"""
Test precedence of keyword arguments.
"""
- context = Context.create({'foo': 'bar'}, foo='buzz')
- self.assertEquals(context.get('foo'), 'buzz')
+ context = ContextStack.create({'foo': 'bar'}, foo='buzz')
+ self.assertEqual(context.get('foo'), 'buzz')
def test_get__key_present(self):
"""
Test getting a key.
"""
- context = Context({"foo": "bar"})
- self.assertEquals(context.get("foo"), "bar")
+ context = ContextStack({"foo": "bar"})
+ self.assertEqual(context.get("foo"), "bar")
def test_get__key_missing(self):
"""
Test getting a missing key.
"""
- context = Context()
+ context = ContextStack()
self.assertTrue(context.get("foo") is None)
def test_get__default(self):
@@ -330,24 +327,24 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test that get() respects the default value.
"""
- context = Context()
- self.assertEquals(context.get("foo", "bar"), "bar")
+ context = ContextStack()
+ self.assertEqual(context.get("foo", "bar"), "bar")
def test_get__precedence(self):
"""
Test that get() respects the order of precedence (later items first).
"""
- context = Context({"foo": "bar"}, {"foo": "buzz"})
- self.assertEquals(context.get("foo"), "buzz")
+ context = ContextStack({"foo": "bar"}, {"foo": "buzz"})
+ self.assertEqual(context.get("foo"), "buzz")
def test_get__fallback(self):
"""
Check that first-added stack items are queried on context misses.
"""
- context = Context({"fuzz": "buzz"}, {"foo": "bar"})
- self.assertEquals(context.get("fuzz"), "buzz")
+ context = ContextStack({"fuzz": "buzz"}, {"foo": "bar"})
+ self.assertEqual(context.get("fuzz"), "buzz")
def test_push(self):
"""
@@ -355,11 +352,11 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
key = "foo"
- context = Context({key: "bar"})
- self.assertEquals(context.get(key), "bar")
+ context = ContextStack({key: "bar"})
+ self.assertEqual(context.get(key), "bar")
context.push({key: "buzz"})
- self.assertEquals(context.get(key), "buzz")
+ self.assertEqual(context.get(key), "buzz")
def test_pop(self):
"""
@@ -367,81 +364,81 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
key = "foo"
- context = Context({key: "bar"}, {key: "buzz"})
- self.assertEquals(context.get(key), "buzz")
+ context = ContextStack({key: "bar"}, {key: "buzz"})
+ self.assertEqual(context.get(key), "buzz")
item = context.pop()
- self.assertEquals(item, {"foo": "buzz"})
- self.assertEquals(context.get(key), "bar")
+ self.assertEqual(item, {"foo": "buzz"})
+ self.assertEqual(context.get(key), "bar")
def test_top(self):
key = "foo"
- context = Context({key: "bar"}, {key: "buzz"})
- self.assertEquals(context.get(key), "buzz")
+ context = ContextStack({key: "bar"}, {key: "buzz"})
+ self.assertEqual(context.get(key), "buzz")
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.
- self.assertEquals(context.get(key), "buzz")
+ self.assertEqual(context.get(key), "buzz")
def test_copy(self):
key = "foo"
- original = Context({key: "bar"}, {key: "buzz"})
- self.assertEquals(original.get(key), "buzz")
+ original = ContextStack({key: "bar"}, {key: "buzz"})
+ self.assertEqual(original.get(key), "buzz")
new = original.copy()
# 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.
new.pop()
- self.assertEquals(new.get(key), "bar")
+ self.assertEqual(new.get(key), "bar")
# Confirm the original is unchanged.
- self.assertEquals(original.get(key), "buzz")
+ self.assertEqual(original.get(key), "buzz")
def test_dot_notation__dict(self):
key = "foo.bar"
- original = Context({"foo": {"bar": "baz"}})
+ original = ContextStack({"foo": {"bar": "baz"}})
self.assertEquals(original.get(key), "baz")
# Works all the way down
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!")
def test_dot_notation__user_object(self):
key = "foo.bar"
- original = Context({"foo": Attachable(bar="baz")})
+ original = ContextStack({"foo": Attachable(bar="baz")})
self.assertEquals(original.get(key), "baz")
# Works on multiple levels, too
key = "a.b.c.d.e.f.g"
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!")
def test_dot_notation__mixed_dict_and_obj(self):
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)
def test_dot_notation__missing_attr_or_key(self):
key = "foo.bar.baz.bak"
- original = Context({"foo": {"bar": {}}})
+ original = ContextStack({"foo": {"bar": {}}})
self.assertEquals(original.get(key), None)
- original = Context({"foo": Attachable(bar=Attachable())})
+ original = ContextStack({"foo": Attachable(bar=Attachable())})
self.assertEquals(original.get(key), None)
def test_dot_notattion__autocall(self):
key = "foo.bar.baz"
# 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!")
class Foo(object):
def bar(self):
return Attachable(baz='Baz')
- original = Context({"foo": Foo()})
+ original = ContextStack({"foo": Foo()})
self.assertEquals(original.get(key), "Baz")
diff --git a/tests/test_examples.py b/pystache/tests/test_examples.py
index 179b089..5c9f74d 100644
--- a/tests/test_examples.py
+++ b/pystache/tests/test_examples.py
@@ -1,5 +1,10 @@
# encoding: utf-8
+"""
+TODO: add a docstring.
+
+"""
+
import unittest
from examples.comments import Comments
@@ -12,8 +17,8 @@ from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput
from examples.nested_context import NestedContext
from pystache import Renderer
-from tests.common import EXAMPLES_DIR
-from tests.common import AssertStringMixin
+from pystache.tests.common import EXAMPLES_DIR
+from pystache.tests.common import AssertStringMixin
class TestView(unittest.TestCase, AssertStringMixin):
@@ -95,7 +100,7 @@ Again, Welcome!""")
view.template = '''{{>partial_in_partial}}'''
actual = renderer.render(view, {'prop': 'derp'})
- self.assertEquals(actual, 'Hi derp!')
+ self.assertEqual(actual, 'Hi derp!')
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_loader.py b/pystache/tests/test_loader.py
index 119ebef..c47239c 100644
--- a/tests/test_loader.py
+++ b/pystache/tests/test_loader.py
@@ -1,7 +1,7 @@
# encoding: utf-8
"""
-Unit tests of reader.py.
+Unit tests of loader.py.
"""
@@ -9,42 +9,45 @@ import os
import sys
import unittest
-from tests.common import AssertStringMixin
+from pystache.tests.common import AssertStringMixin, DATA_DIR, SetupDefaults
from pystache import defaults
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):
loader = Loader(extension='foo')
- self.assertEquals(loader.extension, 'foo')
+ self.assertEqual(loader.extension, 'foo')
def test_init__extension__default(self):
# Test the default value.
loader = Loader()
- self.assertEquals(loader.extension, 'mustache')
+ self.assertEqual(loader.extension, 'mustache')
def test_init__file_encoding(self):
loader = Loader(file_encoding='bar')
- self.assertEquals(loader.file_encoding, 'bar')
+ self.assertEqual(loader.file_encoding, 'bar')
def test_init__file_encoding__default(self):
file_encoding = defaults.FILE_ENCODING
try:
defaults.FILE_ENCODING = 'foo'
loader = Loader()
- self.assertEquals(loader.file_encoding, 'foo')
+ self.assertEqual(loader.file_encoding, 'foo')
finally:
defaults.FILE_ENCODING = file_encoding
def test_init__to_unicode(self):
to_unicode = lambda x: x
loader = Loader(to_unicode=to_unicode)
- self.assertEquals(loader.to_unicode, to_unicode)
+ self.assertEqual(loader.to_unicode, to_unicode)
def test_init__to_unicode__default(self):
loader = Loader()
@@ -53,25 +56,19 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
decode_errors = defaults.DECODE_ERRORS
string_encoding = defaults.STRING_ENCODING
- nonascii = 'abcdé'
+ nonascii = u'abcdé'.encode('utf-8')
- try:
- defaults.DECODE_ERRORS = 'strict'
- defaults.STRING_ENCODING = 'ascii'
- loader = Loader()
- self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
+ loader = Loader()
+ self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
- defaults.DECODE_ERRORS = 'ignore'
- loader = Loader()
- self.assertString(loader.to_unicode(nonascii), u'abcd')
+ defaults.DECODE_ERRORS = 'ignore'
+ loader = Loader()
+ self.assertString(loader.to_unicode(nonascii), u'abcd')
- defaults.STRING_ENCODING = 'utf-8'
- loader = Loader()
- self.assertString(loader.to_unicode(nonascii), u'abcdé')
+ defaults.STRING_ENCODING = 'utf-8'
+ loader = Loader()
+ self.assertString(loader.to_unicode(nonascii), u'abcdé')
- finally:
- defaults.DECODE_ERRORS = decode_errors
- defaults.STRING_ENCODING = string_encoding
def _get_path(self, filename):
return os.path.join(DATA_DIR, filename)
@@ -81,8 +78,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): default arguments with str input.
"""
- reader = Loader()
- actual = reader.unicode("foo")
+ loader = Loader()
+ actual = loader.unicode("foo")
self.assertString(actual, u"foo")
@@ -91,8 +88,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): default arguments with unicode input.
"""
- reader = Loader()
- actual = reader.unicode(u"foo")
+ loader = Loader()
+ actual = loader.unicode(u"foo")
self.assertString(actual, u"foo")
@@ -106,8 +103,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
s = UnicodeSubclass(u"foo")
- reader = Loader()
- actual = reader.unicode(s)
+ loader = Loader()
+ actual = loader.unicode(s)
self.assertString(actual, u"foo")
@@ -116,32 +113,31 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): encoding attribute.
"""
- reader = Loader()
+ loader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
-
- self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
+ self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
def to_unicode(s, encoding=None):
if encoding is None:
encoding = 'utf-8'
return unicode(s, encoding)
- reader.to_unicode = to_unicode
- self.assertString(reader.unicode(non_ascii), u"abcdé")
+ loader.to_unicode = to_unicode
+ self.assertString(loader.unicode(non_ascii), u"abcdé")
def test_unicode__encoding_argument(self):
"""
Test unicode(): encoding argument.
"""
- reader = Loader()
+ loader = Loader()
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é')
# TODO: check the read() unit tests.
@@ -150,9 +146,9 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test read().
"""
- reader = Loader()
+ loader = Loader()
path = self._get_path('ascii.mustache')
- actual = reader.read(path)
+ actual = loader.read(path)
self.assertString(actual, u'ascii: abc')
def test_read__file_encoding__attribute(self):
@@ -174,25 +170,25 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test read(): encoding argument respected.
"""
- reader = Loader()
+ loader = Loader()
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: é')
- def test_reader__to_unicode__attribute(self):
+ def test_loader__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
"""
- reader = Loader()
+ loader = Loader()
path = self._get_path('non_ascii.mustache')
- self.assertRaises(UnicodeDecodeError, reader.read, path)
+ self.assertRaises(UnicodeDecodeError, loader.read, path)
- #reader.decode_errors = 'ignore'
- #actual = reader.read(path)
+ #loader.decode_errors = 'ignore'
+ #actual = loader.read(path)
#self.assertString(actual, u'non-ascii: ')
diff --git a/tests/test_locator.py b/pystache/tests/test_locator.py
index 94a55ad..3a8b229 100644
--- a/tests/test_locator.py
+++ b/pystache/tests/test_locator.py
@@ -1,7 +1,7 @@
# encoding: utf-8
"""
-Contains locator.py unit tests.
+Unit tests for locator.py.
"""
@@ -14,8 +14,8 @@ import unittest
from pystache.loader import Loader as Reader
from pystache.locator import Locator
-from tests.common import DATA_DIR
-from data.views import SayHello
+from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
+from pystache.tests.data.views import SayHello
class LocatorTests(unittest.TestCase):
@@ -26,58 +26,65 @@ class LocatorTests(unittest.TestCase):
def test_init__extension(self):
# Test the default value.
locator = Locator()
- self.assertEquals(locator.template_extension, 'mustache')
+ self.assertEqual(locator.template_extension, 'mustache')
locator = Locator(extension='txt')
- self.assertEquals(locator.template_extension, 'txt')
+ self.assertEqual(locator.template_extension, 'txt')
locator = Locator(extension=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):
locator = Locator()
obj = SayHello()
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):
locator = Locator()
obj = datetime(2000, 1, 1)
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.assertEquals(locator.get_object_directory(None), None)
+ self.assertEqual(locator.get_object_directory(None), None)
def test_make_file_name(self):
locator = Locator()
locator.template_extension = 'bar'
- self.assertEquals(locator.make_file_name('foo'), 'foo.bar')
+ self.assertEqual(locator.make_file_name('foo'), 'foo.bar')
locator.template_extension = False
- self.assertEquals(locator.make_file_name('foo'), 'foo')
+ self.assertEqual(locator.make_file_name('foo'), 'foo')
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):
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):
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):
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)
@@ -98,7 +105,7 @@ class LocatorTests(unittest.TestCase):
dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1]
- self.assertEquals(dirname, 'locator')
+ self.assertEqual(dirname, 'locator')
def test_find_name__non_existent_template_fails(self):
locator = Locator()
@@ -111,9 +118,9 @@ class LocatorTests(unittest.TestCase):
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache')
- expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
+ 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):
locator = Locator()
@@ -121,20 +128,20 @@ class LocatorTests(unittest.TestCase):
obj = SayHello()
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):
locator = Locator()
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')
expected = os.path.join(DATA_DIR, 'say_hello.mustache')
- self.assertEquals(actual, expected)
+ self.assertEqual(actual, expected)
def test_make_template_name(self):
"""
@@ -147,4 +154,4 @@ class LocatorTests(unittest.TestCase):
pass
foo = FooBar()
- self.assertEquals(locator.make_template_name(foo), 'foo_bar')
+ self.assertEqual(locator.make_template_name(foo), 'foo_bar')
diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py
new file mode 100644
index 0000000..4aa0959
--- /dev/null
+++ b/pystache/tests/test_parser.py
@@ -0,0 +1,26 @@
+# 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)
+
diff --git a/tests/test_pystache.py b/pystache/tests/test_pystache.py
index f9857cd..5447f8d 100644
--- a/tests/test_pystache.py
+++ b/pystache/tests/test_pystache.py
@@ -1,23 +1,34 @@
# encoding: utf-8
import unittest
+
import pystache
+from pystache import defaults
from pystache import renderer
+from pystache.tests.common import html_escape
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):
actual = pystache.render(template, context)
- self.assertEquals(actual, expected)
+ self.assertEqual(actual, expected)
def test_basic(self):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
- self.assertEquals(ret, "Hi world!")
+ self.assertEqual(ret, "Hi world!")
def test_kwargs(self):
ret = pystache.render("Hi {{thing}}!", thing='world')
- self.assertEquals(ret, "Hi world!")
+ self.assertEqual(ret, "Hi world!")
def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?"
@@ -42,7 +53,7 @@ class PystacheTests(unittest.TestCase):
def test_comments(self):
template = "What {{! the }} what?"
actual = pystache.render(template)
- self.assertEquals("What what?", actual)
+ self.assertEqual("What what?", actual)
def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!"
@@ -54,7 +65,7 @@ class PystacheTests(unittest.TestCase):
context = { 'set': True }
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):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
diff --git a/tests/test_renderengine.py b/pystache/tests/test_renderengine.py
index 7752161..2fcb95b 100644
--- a/tests/test_renderengine.py
+++ b/pystache/tests/test_renderengine.py
@@ -5,13 +5,34 @@ Unit tests of renderengine.py.
"""
-import cgi
import unittest
-from pystache.context import Context
+from pystache.context import ContextStack
+from pystache import defaults
from pystache.parser import ParsingError
from pystache.renderengine import RenderEngine
-from 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):
@@ -26,9 +47,9 @@ class RenderEngineTestCase(unittest.TestCase):
# In real-life, these arguments would be functions
engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
- self.assertEquals(engine.escape, "escape")
- self.assertEquals(engine.literal, "literal")
- self.assertEquals(engine.load_partial, "foo")
+ self.assertEqual(engine.escape, "escape")
+ self.assertEqual(engine.literal, "literal")
+ self.assertEqual(engine.load_partial, "foo")
class RenderTests(unittest.TestCase, AssertStringMixin):
@@ -47,7 +68,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
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)
return engine
@@ -62,7 +83,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
if partials is not None:
engine.load_partial = lambda key: unicode(partials[key])
- context = Context(*context)
+ context = ContextStack(*context)
actual = engine.render(template, context)
@@ -154,12 +175,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Test a context value that is not a basestring instance.
"""
- # We use include upper() to make sure we are actually using
- # our custom function in the tests
- to_unicode = lambda s: unicode(s, encoding='ascii').upper()
engine = self._engine()
- engine.escape = to_unicode
- engine.literal = to_unicode
+ engine.escape = mock_literal
+ engine.literal = mock_literal
self.assertRaises(TypeError, engine.literal, 100)
@@ -186,37 +204,85 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': '{{#hello}}'}
self._assert_render(u'{{#hello}}', template, context)
+ ## Test interpolation with "falsey" values
+ #
+ # In these test cases, we test the part of the spec that says that
+ # "data should be coerced into a string (and escaped, if appropriate)
+ # before interpolation." We test this for data that is "falsey."
+
+ def test_interpolation__falsey__zero(self):
+ template = '{{.}}'
+ context = 0
+ self._assert_render(u'0', template, context)
+
+ def test_interpolation__falsey__none(self):
+ template = '{{.}}'
+ context = None
+ self._assert_render(u'None', template, context)
+
+ def test_interpolation__falsey__zero(self):
+ template = '{{.}}'
+ context = False
+ self._assert_render(u'False', template, context)
+
+ # Built-in types:
+ #
+ # Confirm that we not treat instances of built-in types as objects,
+ # 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):
"""
- Check tag interpolation with a string on the top of the context stack.
+ Check tag interpolation with a built-in type: string.
"""
- item = 'abc'
- # item.upper() == 'ABC'
- template = '{{#section}}{{upper}}{{/section}}'
- context = {'section': item, 'upper': 'XYZ'}
- self._assert_render(u'XYZ', template, context)
+ self._assert_builtin_type('abc', 'upper', 'ABC', u'xyz')
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
- # item.real == 10
- template = '{{#section}}{{real}}{{/section}}'
- context = {'section': item, 'real': 1000}
- self._assert_render(u'1000', template, context)
+ # Since public attributes weren't added to integers until Python 2.6
+ # (for example the "real" attribute of the numeric type hierarchy)--
+ #
+ # http://docs.python.org/library/numbers.html
+ #
+ # 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):
"""
- 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[0].pop() == 3
- template = '{{#section}}{{pop}}{{/section}}'
- context = {'section': item, 'pop': 7}
+ attr_name = 'pop'
+ # Make a copy to prevent changes to item[0].
+ 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)
def test_implicit_iterator__literal(self):
@@ -288,7 +354,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
try:
self._assert_render(None, template)
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):
"""
@@ -299,7 +365,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
try:
self._assert_render(None, template)
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):
"""
@@ -349,6 +415,17 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
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):
"""
Check that "nested truthy" sections get rendered.
@@ -424,6 +501,40 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
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):
"""
Check that multiline comments are permitted.
diff --git a/tests/test_renderer.py b/pystache/tests/test_renderer.py
index a69d11a..64a4325 100644
--- a/tests/test_renderer.py
+++ b/pystache/tests/test_renderer.py
@@ -15,9 +15,24 @@ from pystache import Renderer
from pystache import TemplateSpec
from pystache.loader import Loader
-from tests.common import get_data_path
-from tests.common import AssertStringMixin
-from tests.data.views import SayHello
+from pystache.tests.common import get_data_path, AssertStringMixin
+from pystache.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):
@@ -41,20 +56,24 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(partials={'foo': 'bar'})
- self.assertEquals(renderer.partials, {'foo': 'bar'})
+ self.assertEqual(renderer.partials, {'foo': 'bar'})
def test_escape__default(self):
escape = Renderer().escape
- self.assertEquals(escape(">"), "&gt;")
- self.assertEquals(escape('"'), "&quot;")
- # Single quotes are not escaped.
- self.assertEquals(escape("'"), "'")
+ self.assertEqual(escape(">"), "&gt;")
+ self.assertEqual(escape('"'), "&quot;")
+ # Single quotes are escaped only in Python 3.2 and later.
+ if sys.version_info < (3, 2):
+ expected = "'"
+ else:
+ expected = '&#x27;'
+ self.assertEqual(escape("'"), expected)
def test_escape(self):
escape = lambda s: "**" + s
renderer = Renderer(escape=escape)
- self.assertEquals(renderer.escape("bar"), "**bar")
+ self.assertEqual(renderer.escape("bar"), "**bar")
def test_decode_errors__default(self):
"""
@@ -62,7 +81,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer()
- self.assertEquals(renderer.decode_errors, 'strict')
+ self.assertEqual(renderer.decode_errors, 'strict')
def test_decode_errors(self):
"""
@@ -70,7 +89,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(decode_errors="foo")
- self.assertEquals(renderer.decode_errors, "foo")
+ self.assertEqual(renderer.decode_errors, "foo")
def test_file_encoding__default(self):
"""
@@ -78,7 +97,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer()
- self.assertEquals(renderer.file_encoding, renderer.string_encoding)
+ self.assertEqual(renderer.file_encoding, renderer.string_encoding)
def test_file_encoding(self):
"""
@@ -86,7 +105,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(file_encoding='foo')
- self.assertEquals(renderer.file_encoding, 'foo')
+ self.assertEqual(renderer.file_encoding, 'foo')
def test_file_extension__default(self):
"""
@@ -94,7 +113,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer()
- self.assertEquals(renderer.file_extension, 'mustache')
+ self.assertEqual(renderer.file_extension, 'mustache')
def test_file_extension(self):
"""
@@ -102,7 +121,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(file_extension='foo')
- self.assertEquals(renderer.file_extension, 'foo')
+ self.assertEqual(renderer.file_extension, 'foo')
def test_search_dirs__default(self):
"""
@@ -110,7 +129,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer()
- self.assertEquals(renderer.search_dirs, [os.curdir])
+ self.assertEqual(renderer.search_dirs, [os.curdir])
def test_search_dirs__string(self):
"""
@@ -118,7 +137,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(search_dirs='foo')
- self.assertEquals(renderer.search_dirs, ['foo'])
+ self.assertEqual(renderer.search_dirs, ['foo'])
def test_search_dirs__list(self):
"""
@@ -126,7 +145,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(search_dirs=['foo'])
- self.assertEquals(renderer.search_dirs, ['foo'])
+ self.assertEqual(renderer.search_dirs, ['foo'])
def test_string_encoding__default(self):
"""
@@ -134,7 +153,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer()
- self.assertEquals(renderer.string_encoding, sys.getdefaultencoding())
+ self.assertEqual(renderer.string_encoding, sys.getdefaultencoding())
def test_string_encoding(self):
"""
@@ -142,7 +161,7 @@ class RendererInitTestCase(unittest.TestCase):
"""
renderer = Renderer(string_encoding="foo")
- self.assertEquals(renderer.string_encoding, "foo")
+ self.assertEqual(renderer.string_encoding, "foo")
class RendererTests(unittest.TestCase, AssertStringMixin):
@@ -159,30 +178,30 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
Test that the string_encoding attribute is respected.
"""
- renderer = Renderer()
- s = "é"
+ renderer = self._renderer()
+ b = u"é".encode('utf-8')
renderer.string_encoding = "ascii"
- self.assertRaises(UnicodeDecodeError, renderer.unicode, s)
+ self.assertRaises(UnicodeDecodeError, renderer.unicode, b)
renderer.string_encoding = "utf-8"
- self.assertEquals(renderer.unicode(s), u"é")
+ self.assertEqual(renderer.unicode(b), u"é")
def test_unicode__decode_errors(self):
"""
Test that the decode_errors attribute is respected.
"""
- renderer = Renderer()
+ renderer = self._renderer()
renderer.string_encoding = "ascii"
- s = "déf"
+ b = u"déf".encode('utf-8')
renderer.decode_errors = "ignore"
- self.assertEquals(renderer.unicode(s), "df")
+ self.assertEqual(renderer.unicode(b), "df")
renderer.decode_errors = "replace"
# 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.
@@ -191,10 +210,10 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
Test that _make_loader() returns a Loader.
"""
- renderer = Renderer()
+ renderer = self._renderer()
loader = renderer._make_loader()
- self.assertEquals(type(loader), Loader)
+ self.assertEqual(type(loader), Loader)
def test__make_loader__attributes(self):
"""
@@ -203,16 +222,16 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
"""
unicode_ = lambda x: x
- renderer = Renderer()
+ renderer = self._renderer()
renderer.file_encoding = 'enc'
renderer.file_extension = 'ext'
renderer.unicode = unicode_
loader = renderer._make_loader()
- self.assertEquals(loader.extension, 'ext')
- self.assertEquals(loader.file_encoding, 'enc')
- self.assertEquals(loader.to_unicode, unicode_)
+ self.assertEqual(loader.extension, 'ext')
+ self.assertEqual(loader.file_encoding, 'enc')
+ self.assertEqual(loader.to_unicode, unicode_)
## Test the render() method.
@@ -221,57 +240,57 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
Check that render() returns a string of type unicode.
"""
- renderer = Renderer()
+ renderer = self._renderer()
rendered = renderer.render('foo')
- self.assertEquals(type(rendered), unicode)
+ self.assertEqual(type(rendered), unicode)
def test_render__unicode(self):
- renderer = Renderer()
+ renderer = self._renderer()
actual = renderer.render(u'foo')
- self.assertEquals(actual, u'foo')
+ self.assertEqual(actual, u'foo')
def test_render__str(self):
- renderer = Renderer()
+ renderer = self._renderer()
actual = renderer.render('foo')
- self.assertEquals(actual, 'foo')
+ self.assertEqual(actual, 'foo')
def test_render__non_ascii_character(self):
- renderer = Renderer()
+ renderer = self._renderer()
actual = renderer.render(u'Poincaré')
- self.assertEquals(actual, u'Poincaré')
+ self.assertEqual(actual, u'Poincaré')
def test_render__context(self):
"""
Test render(): passing a context.
"""
- renderer = Renderer()
- self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom')
+ renderer = self._renderer()
+ self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom')
def test_render__context_and_kwargs(self):
"""
Test render(): passing a context and **kwargs.
"""
- renderer = Renderer()
+ renderer = self._renderer()
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):
"""
Test render(): passing **kwargs and no context.
"""
- renderer = Renderer()
- self.assertEquals(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom')
+ renderer = self._renderer()
+ self.assertEqual(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom')
def test_render__context_and_kwargs__precedence(self):
"""
Test render(): **kwargs takes precedence over context.
"""
- renderer = Renderer()
- self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad')
+ renderer = self._renderer()
+ self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad')
def test_render__kwargs_does_not_modify_context(self):
"""
@@ -279,25 +298,25 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
"""
context = {}
- renderer = Renderer()
+ renderer = self._renderer()
renderer.render('Hi {{person}}', context=context, foo="bar")
- self.assertEquals(context, {})
+ self.assertEqual(context, {})
def test_render__nonascii_template(self):
"""
Test passing a non-unicode template with non-ascii characters.
"""
- renderer = Renderer()
- template = "déf"
+ renderer = _make_renderer()
+ template = u"déf".encode("utf-8")
# Check that decode_errors and string_encoding are both respected.
renderer.decode_errors = 'ignore'
renderer.string_encoding = 'ascii'
- self.assertEquals(renderer.render(template), "df")
+ self.assertEqual(renderer.render(template), "df")
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):
"""
@@ -309,8 +328,8 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
load_partial = renderer._make_load_partial()
actual = load_partial('foo')
- self.assertEquals(actual, 'bar')
- self.assertEquals(type(actual), unicode, "RenderEngine requires that "
+ self.assertEqual(actual, 'bar')
+ self.assertEqual(type(actual), unicode, "RenderEngine requires that "
"load_partial return unicode strings.")
def test_make_load_partial__unicode(self):
@@ -322,14 +341,14 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer.partials = {'partial': 'foo'}
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.
renderer.partials = {'partial': u'foo'}
load_partial = renderer._make_load_partial()
# If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported
- self.assertEquals(load_partial("partial"), "foo")
+ self.assertEqual(load_partial("partial"), "foo")
def test_render_path(self):
"""
@@ -339,7 +358,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer = Renderer()
path = get_data_path('say_hello.mustache')
actual = renderer.render_path(path, to='foo')
- self.assertEquals(actual, "Hello, foo")
+ self.assertEqual(actual, "Hello, foo")
def test_render__object(self):
"""
@@ -350,10 +369,10 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
say_hello = SayHello()
actual = renderer.render(say_hello)
- self.assertEquals('Hello, World', actual)
+ self.assertEqual('Hello, World', actual)
actual = renderer.render(say_hello, to='Mars')
- self.assertEquals('Hello, Mars', actual)
+ self.assertEqual('Hello, Mars', actual)
def test_render__template_spec(self):
"""
@@ -379,7 +398,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
view = Simple()
actual = renderer.render(view)
- self.assertEquals('Hi pizza!', actual)
+ self.assertEqual('Hi pizza!', actual)
# By testing that Renderer.render() constructs the right RenderEngine,
@@ -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.
def test__load_partial__returns_unicode(self):
@@ -410,13 +436,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine()
actual = engine.load_partial('str')
- self.assertEquals(actual, "foo")
- self.assertEquals(type(actual), unicode)
+ self.assertEqual(actual, "foo")
+ self.assertEqual(type(actual), unicode)
# Check that unicode subclasses are not preserved.
actual = engine.load_partial('subclass')
- self.assertEquals(actual, "abc")
- self.assertEquals(type(actual), unicode)
+ self.assertEqual(actual, "abc")
+ self.assertEqual(type(actual), unicode)
def test__load_partial__not_found(self):
"""
@@ -433,7 +459,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
load_partial("foo")
raise Exception("Shouldn't get here")
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.
@@ -442,13 +468,14 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
Test that literal uses the renderer's unicode function.
"""
- renderer = Renderer()
- renderer.unicode = lambda s: s.upper()
+ renderer = self._make_renderer()
+ renderer.unicode = mock_unicode
engine = renderer._make_render_engine()
literal = engine.literal
- self.assertEquals(literal("foo"), "FOO")
+ b = u"foo".encode("ascii")
+ self.assertEqual(literal(b), "FOO")
def test__literal__handles_unicode(self):
"""
@@ -461,7 +488,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine()
literal = engine.literal
- self.assertEquals(literal(u"foo"), "foo")
+ self.assertEqual(literal(u"foo"), "foo")
def test__literal__returns_unicode(self):
"""
@@ -474,16 +501,16 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine()
literal = engine.literal
- self.assertEquals(type(literal("foo")), unicode)
+ self.assertEqual(type(literal("foo")), unicode)
class MyUnicode(unicode):
pass
s = MyUnicode("abc")
- self.assertEquals(type(s), MyUnicode)
+ self.assertEqual(type(s), MyUnicode)
self.assertTrue(isinstance(s, unicode))
- self.assertEquals(type(literal(s)), unicode)
+ self.assertEqual(type(literal(s)), unicode)
## Test the engine's escape attribute.
@@ -498,7 +525,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine()
escape = engine.escape
- self.assertEquals(escape("foo"), "**foo")
+ self.assertEqual(escape("foo"), "**foo")
def test__escape__uses_renderer_unicode(self):
"""
@@ -506,12 +533,13 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
"""
renderer = Renderer()
- renderer.unicode = lambda s: s.upper()
+ renderer.unicode = mock_unicode
engine = renderer._make_render_engine()
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):
"""
@@ -519,7 +547,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
"""
renderer = Renderer()
- renderer.escape = lambda s: type(s).__name__
+ renderer.escape = lambda s: unicode(type(s).__name__)
engine = renderer._make_render_engine()
escape = engine.escape
@@ -527,9 +555,9 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
class MyUnicode(unicode):
pass
- self.assertEquals(escape("foo"), "unicode")
- self.assertEquals(escape(u"foo"), "unicode")
- self.assertEquals(escape(MyUnicode("foo")), "MyUnicode")
+ self.assertEqual(escape(u"foo".encode('ascii')), unicode.__name__)
+ self.assertEqual(escape(u"foo"), unicode.__name__)
+ self.assertEqual(escape(MyUnicode("foo")), MyUnicode.__name__)
def test__escape__returns_unicode(self):
"""
@@ -542,7 +570,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine = renderer._make_render_engine()
escape = engine.escape
- self.assertEquals(type(escape("foo")), unicode)
+ self.assertEqual(type(escape("foo")), unicode)
# Check that literal doesn't preserve unicode subclasses.
class MyUnicode(unicode):
@@ -550,7 +578,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
s = MyUnicode("abc")
- self.assertEquals(type(s), MyUnicode)
+ self.assertEqual(type(s), MyUnicode)
self.assertTrue(isinstance(s, unicode))
- self.assertEquals(type(escape(s)), unicode)
+ self.assertEqual(type(escape(s)), unicode)
diff --git a/tests/test_simple.py b/pystache/tests/test_simple.py
index a8fc815..d3ed0b6 100644
--- a/tests/test_simple.py
+++ b/pystache/tests/test_simple.py
@@ -8,8 +8,8 @@ from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial
from examples.simple import Simple
-from tests.common import EXAMPLES_DIR
-from tests.common import AssertStringMixin
+from pystache.tests.common import EXAMPLES_DIR
+from pystache.tests.common import AssertStringMixin
class TestSimple(unittest.TestCase, AssertStringMixin):
@@ -28,11 +28,11 @@ class TestSimple(unittest.TestCase, AssertStringMixin):
renderer = Renderer()
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):
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):
view = Lambdas()
@@ -58,7 +58,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin):
def test_non_existent_value_renders_blank(self):
view = Simple()
template = '{{not_set}} {{blank}}'
- self.assertEquals(pystache.Renderer().render(template), ' ')
+ self.assertEqual(pystache.Renderer().render(template), ' ')
def test_template_partial_extension(self):
"""
diff --git a/tests/test_template_spec.py b/pystache/tests/test_specloader.py
index 9599c37..8332b28 100644
--- a/tests/test_template_spec.py
+++ b/pystache/tests/test_specloader.py
@@ -18,13 +18,11 @@ from pystache import Renderer
from pystache import TemplateSpec
from pystache.locator import Locator
from pystache.loader import Loader
-from pystache.spec_loader import SpecLoader
-from tests.common import DATA_DIR
-from tests.common import EXAMPLES_DIR
-from tests.common import AssertIsMixin
-from tests.common import AssertStringMixin
-from tests.data.views import SampleView
-from tests.data.views import NonAscii
+from pystache.specloader import SpecLoader
+from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
+from pystache.tests.common import AssertIsMixin, AssertStringMixin
+from pystache.tests.data.views import SampleView
+from pystache.tests.data.views import NonAscii
class Thing(object):
@@ -46,9 +44,10 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
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)
- self.assertEquals(actual, "No tags...")
+ self.assertEqual(actual, "No tags...")
def test_template_path_for_partials(self):
"""
@@ -64,7 +63,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(IOError, renderer1.render, spec)
actual = renderer2.render(spec)
- self.assertEquals(actual, "Partial: No tags...")
+ self.assertEqual(actual, "Partial: No tags...")
def test_basic_method_calls(self):
renderer = Renderer()
@@ -78,7 +77,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer = Renderer()
actual = renderer.render(view)
- self.assertEquals(actual, "Hi Chris!")
+ self.assertEqual(actual, "Hi Chris!")
def test_complex(self):
renderer = Renderer()
@@ -94,7 +93,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_higher_order_replace(self):
renderer = Renderer()
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):
view = Lambdas()
@@ -118,7 +117,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(view)
- self.assertEquals(actual, u'nopqrstuvwxyz')
+ self.assertEqual(actual, u'nopqrstuvwxyz')
def test_hierarchical_partials_with_lambdas(self):
view = Lambdas()
@@ -151,6 +150,28 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
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):
"""
@@ -158,13 +179,16 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
+ def _make_specloader(self):
+ return _make_specloader()
+
def test_init__defaults(self):
- custom = SpecLoader()
+ spec_loader = SpecLoader()
# Check the loader attribute.
- loader = custom.loader
- self.assertEquals(loader.extension, 'mustache')
- self.assertEquals(loader.file_encoding, sys.getdefaultencoding())
+ loader = spec_loader.loader
+ self.assertEqual(loader.extension, 'mustache')
+ self.assertEqual(loader.file_encoding, sys.getdefaultencoding())
# TODO: finish testing the other Loader attributes.
to_unicode = loader.to_unicode
@@ -186,7 +210,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
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):
"""
@@ -196,7 +221,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
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):
"""
@@ -206,7 +232,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
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):
"""
@@ -216,10 +243,12 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
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'
- self._assert_template(SpecLoader(), custom, u'é')
+ self._assert_template(spec_loader, custom, u'é')
# TODO: make this test complete.
def test_load__template__correct_loader(self):
@@ -254,8 +283,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# Check that our unicode() above was called.
self._assert_template(custom_loader, view, u'foo')
- self.assertEquals(loader.s, "template-foo")
- self.assertEquals(loader.encoding, "encoding-foo")
+ self.assertEqual(loader.s, "template-foo")
+ self.assertEqual(loader.encoding, "encoding-foo")
# TODO: migrate these tests into the SpecLoaderTests class.
@@ -265,14 +294,13 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# TemplateSpec attributes or something).
class TemplateSpecTests(unittest.TestCase):
- # TODO: rename this method to _make_loader().
- def _make_locator(self):
- return SpecLoader()
+ def _make_loader(self):
+ return _make_specloader()
def _assert_template_location(self, view, expected):
- locator = self._make_locator()
- actual = locator._find_relative(view)
- self.assertEquals(actual, expected)
+ loader = self._make_loader()
+ actual = loader._find_relative(view)
+ self.assertEqual(actual, expected)
def test_find_relative(self):
"""
@@ -328,43 +356,50 @@ class TemplateSpecTests(unittest.TestCase):
view.template_extension = '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):
"""
Test _find() with a view that has a directory specified.
"""
- locator = self._make_locator()
+ loader = self._make_loader()
view = SampleView()
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)
- expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt'))
+ actual = loader._find(view)
+ expected = os.path.join(DATA_DIR, 'foo/bar.txt')
- self.assertEquals(actual, expected)
+ self._assert_paths(actual, expected)
def test_find__without_directory(self):
"""
Test _find() with a view that doesn't have a directory specified.
"""
- locator = self._make_locator()
+ loader = self._make_loader()
view = SampleView()
- self.assertTrue(locator._find_relative(view)[0] is None)
+ self.assertTrue(loader._find_relative(view)[0] is None)
- actual = locator._find(view)
- expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
+ actual = loader._find(view)
+ 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):
- locator = self._make_locator()
- actual = locator.load(custom)
+ loader = self._make_loader()
+ actual = loader.load(custom)
- self.assertEquals(type(actual), unicode)
- self.assertEquals(actual, expected)
+ self.assertEqual(type(actual), unicode)
+ self.assertEqual(actual, expected)
def test_get_template(self):
"""
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index f91c44e..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-[nosetests]
-with-doctest=1
-doctest-extension=rst
diff --git a/setup.py b/setup.py
index 65d8d6f..356b51f 100644
--- a/setup.py
+++ b/setup.py
@@ -2,21 +2,23 @@
# 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
-version of pystache to PyPI--
+This docstring contains instructions to Pystache maintainers on how
+to release a new version of Pystache.
+
+(1) Push to PyPI. To release a new version of Pystache to PyPI--
http://pypi.python.org/pypi/pystache
-Create a PyPI user account. The user account will need permissions to push
-to PyPI. A current "Package Index Owner" of pystache can grant you those
-permissions.
+create a PyPI user account if you do not already have one. The user account
+will need permissions to push to PyPI. A current "Package Index Owner" of
+Pystache can grant you those permissions.
When you have permissions, run the following (after preparing the release,
-bumping the version number in setup.py, etc):
+merging to master, bumping the version number in setup.py, etc):
- > python setup.py publish
+ python setup.py publish
If you get an error like the following--
@@ -33,16 +35,78 @@ as described here, for example:
http://docs.python.org/release/2.5.2/dist/pypirc.html
+(2) Tag the release on GitHub. Here are some commands for tagging.
+
+List current tags:
+
+ git tag -l -n3
+
+Create an annotated tag:
+
+ git tag -a -m "Version 0.5.1" "v0.5.1"
+
+Push a tag to GitHub:
+
+ git push --tags defunkt v0.5.1
+
"""
import os
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
-except ImportError:
- from distutils.core import setup
+ """
+ # This function implementation was chosen to be compatible across Python 2/3.
+ f = open(path, 'rb')
+ # 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():
@@ -58,37 +122,96 @@ def make_long_description():
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':
publish()
sys.exit()
-long_description = make_long_description()
-
-setup(name='pystache',
- version='0.5.0-rc',
- 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',
- packages=['pystache'],
- license='MIT',
- entry_points = {
- 'console_scripts': ['pystache=pystache.commands:main'],
- },
- classifiers = (
- 'Development Status :: 4 - Beta',
- 'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2.4',
- 'Programming Language :: Python :: 2.5',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
- )
-)
+# We follow the guidance here for compatibility with using setuptools instead
+# of Distribute under Python 2 (on the subject of new, unrecognized keyword
+# arguments to setup()):
+#
+# http://packages.python.org/distribute/python3.html#note-on-compatibility-with-setuptools
+#
+if py_version < (3, ):
+ extra = {}
+else:
+ extra = {
+ # Causes 2to3 to be run during the build step.
+ 'use_2to3': True,
+ }
+
+# We use the package simplejson for older Python versions since Python
+# does not contain the module json before 2.6:
+#
+# http://docs.python.org/library/json.html
+#
+# Moreover, simplejson stopped officially support for Python 2.4 in version 2.1.0:
+#
+# https://github.com/simplejson/simplejson/blob/master/CHANGES.txt
+#
+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)
diff --git a/test_pystache.py b/test_pystache.py
new file mode 100644
index 0000000..9a1a3ca
--- /dev/null
+++ b/test_pystache.py
@@ -0,0 +1,30 @@
+#!/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()
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tests/__init__.py
+++ /dev/null
diff --git a/tests/common.py b/tests/common.py
index 6614f5b..402df11 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -52,22 +52,3 @@ class AssertIsMixin:
def assertIs(self, first, 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()))
diff --git a/tests/data/__init__.py b/tests/data/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tests/data/__init__.py
+++ /dev/null
diff --git a/tests/test_spec.py b/tests/test_spec.py
deleted file mode 100644
index 02f6080..0000000
--- a/tests/test_spec.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# 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()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..d1aef0d
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,34 @@
+# 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