diff options
-rw-r--r-- | .gitignore | 14 | ||||
-rw-r--r-- | HISTORY.rst | 17 | ||||
-rw-r--r-- | MANIFEST.in | 8 | ||||
-rw-r--r-- | README.rst | 162 | ||||
-rw-r--r-- | TODO.md | 8 | ||||
-rw-r--r-- | examples/__init__.py | 0 | ||||
-rw-r--r-- | examples/partials_with_lambdas.py | 6 | ||||
-rw-r--r-- | pystache/__init__.py | 13 | ||||
-rw-r--r-- | pystache/commands/__init__.py | 4 | ||||
-rw-r--r-- | pystache/commands/render.py (renamed from pystache/commands.py) | 21 | ||||
-rw-r--r-- | pystache/commands/test.py | 18 | ||||
-rw-r--r-- | pystache/common.py | 26 | ||||
-rw-r--r-- | pystache/context.py | 68 | ||||
-rw-r--r-- | pystache/defaults.py | 15 | ||||
-rw-r--r-- | pystache/init.py | 3 | ||||
-rw-r--r-- | pystache/loader.py | 31 | ||||
-rw-r--r-- | pystache/parsed.py | 2 | ||||
-rw-r--r-- | pystache/parser.py | 63 | ||||
-rw-r--r-- | pystache/renderengine.py | 52 | ||||
-rw-r--r-- | pystache/renderer.py | 49 | ||||
-rw-r--r-- | pystache/specloader.py (renamed from pystache/spec_loader.py) | 0 | ||||
-rw-r--r-- | pystache/template_spec.py | 6 | ||||
-rw-r--r-- | pystache/tests/__init__.py | 4 | ||||
-rwxr-xr-x | pystache/tests/benchmark.py (renamed from tests/benchmark.py) | 0 | ||||
-rw-r--r-- | pystache/tests/common.py | 193 | ||||
-rw-r--r-- | pystache/tests/data/__init__.py | 4 | ||||
-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__.py | 4 | ||||
-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.py | 90 | ||||
-rw-r--r-- | pystache/tests/examples/__init__.py | 4 | ||||
-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.py | 12 | ||||
-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.py | 155 | ||||
-rw-r--r-- | pystache/tests/spectesting.py | 285 | ||||
-rw-r--r-- | pystache/tests/test___init__.py | 36 | ||||
-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.py | 26 | ||||
-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.cfg | 3 | ||||
-rw-r--r-- | setup.py | 201 | ||||
-rw-r--r-- | test_pystache.py | 30 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/common.py | 19 | ||||
-rw-r--r-- | tests/data/__init__.py | 0 | ||||
-rw-r--r-- | tests/test_spec.py | 99 | ||||
-rw-r--r-- | tox.ini | 34 |
96 files changed, 2094 insertions, 669 deletions
@@ -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 @@ -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 @@ -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 '''), 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("'", ''') + + +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 & ['something'])(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(">"), ">") - self.assertEquals(escape('"'), """) - # Single quotes are not escaped. - self.assertEquals(escape("'"), "'") + self.assertEqual(escape(">"), ">") + self.assertEqual(escape('"'), """) + # Single quotes are escaped only in Python 3.2 and later. + if sys.version_info < (3, 2): + expected = "'" + else: + expected = ''' + 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 @@ -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() @@ -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 |