summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2012-10-20 15:57:23 -0700
committerChris Jerdonek <chris.jerdonek@gmail.com>2012-10-20 15:57:23 -0700
commitba5ef6d31eb2705df846444b56893b555948fbf8 (patch)
treeadf04a7e2df34a7442b448573593cf316b3fca88
parentb8a3d0c6cea62874bac2414db2c06856293d8877 (diff)
parent5e9a226992ce9b3c9fa1ecd9de700b8d8ff4f136 (diff)
downloadpystache-ba5ef6d31eb2705df846444b56893b555948fbf8.tar.gz
Merge branch 'development' into 'master': staging v0.5.3-rc
-rw-r--r--.gitignore3
-rw-r--r--.travis.yml15
-rw-r--r--HISTORY.md159
-rw-r--r--HISTORY.rst117
-rw-r--r--LICENSE1
-rw-r--r--MANIFEST.in6
-rw-r--r--README.md273
-rw-r--r--README.rst234
-rw-r--r--TODO.md9
-rw-r--r--gh/images/logo_phillips.pngbin0 -> 173595 bytes
-rw-r--r--pystache/__init__.py6
-rw-r--r--pystache/common.py35
-rw-r--r--pystache/context.py61
-rw-r--r--pystache/defaults.py10
-rw-r--r--pystache/init.py1
-rw-r--r--pystache/loader.py28
-rw-r--r--pystache/locator.py25
-rw-r--r--pystache/parsed.py48
-rw-r--r--pystache/parser.py333
-rw-r--r--pystache/renderengine.py286
-rw-r--r--pystache/renderer.py265
-rw-r--r--pystache/specloader.py3
-rw-r--r--pystache/template_spec.py30
-rw-r--r--pystache/tests/common.py10
-rw-r--r--pystache/tests/data/locator/template.txt1
-rw-r--r--pystache/tests/doctesting.py6
-rw-r--r--pystache/tests/main.py104
-rw-r--r--pystache/tests/test___init__.py2
-rw-r--r--pystache/tests/test_context.py65
-rw-r--r--pystache/tests/test_defaults.py68
-rw-r--r--pystache/tests/test_loader.py17
-rw-r--r--pystache/tests/test_locator.py24
-rw-r--r--pystache/tests/test_parser.py3
-rw-r--r--pystache/tests/test_renderengine.py165
-rw-r--r--pystache/tests/test_renderer.py182
-rw-r--r--pystache/tests/test_specloader.py32
-rw-r--r--setup.py269
-rw-r--r--setup_description.rst501
-rw-r--r--tox.ini4
39 files changed, 2435 insertions, 966 deletions
diff --git a/.gitignore b/.gitignore
index c9b5217..758d62d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,9 @@
# Our tox runs convert the doctests in *.rst files to Python 3 prior to
# running tests. Ignore these temporary files.
*.temp2to3.rst
+# The setup.py "prep" command converts *.md to *.temp.rst (via *.temp.md).
+*.temp.md
+*.temp.rst
# TextMate project file
*.tmproj
# Distribution-related folders and files.
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a426b13
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,15 @@
+language: python
+
+# Travis CI has no plans to support Python 2.4 and Jython.
+python:
+ - 2.5
+ - 2.6
+ - 2.7
+ - 3.2
+ - pypy
+
+script:
+ - python setup.py install
+ # Include the spec tests directory for Mustache spec tests and the
+ # project directory for doctests.
+ - pystache-test ext/spec/specs .
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 0000000..01c9049
--- /dev/null
+++ b/HISTORY.md
@@ -0,0 +1,159 @@
+History
+=======
+
+0.5.3 (TBD)
+-----------
+
+- Added ability to customize string coercion (e.g. to have None render as
+ `''`) (issue \#130).
+- Added Renderer.render_name() to render a template by name (issue \#122).
+- Added TemplateSpec.template_path to specify an absolute path to a
+ template (issue \#41).
+- Added option of raising errors on missing tags/partials:
+ `Renderer(missing_tags='strict')` (issue \#110).
+- Added support for finding and loading templates by file name in
+ addition to by template name (issue \#127). [xgecko]
+- Added a `parse()` function that yields a printable, pre-compiled
+ parse tree.
+- Added support for rendering pre-compiled templates.
+- Added support for [PyPy](http://pypy.org/) (issue \#125).
+- Added support for [Travis CI](http://travis-ci.org) (issue \#124).
+ [msabramo]
+- Bugfix: `defaults.DELIMITERS` can now be changed at runtime (issue \#135).
+ [bennoleslie]
+- Bugfix: exceptions raised from a property are no longer swallowed
+ when getting a key from a context stack (issue \#110).
+- Bugfix: lambda section values can now return non-ascii, non-unicode
+ strings (issue \#118).
+- Convert HISTORY and README files from reST to Markdown.
+- More robust handling of byte strings in Python 3.
+- Added Creative Commons license for David Phillips's logo.
+
+0.5.2 (2012-05-03)
+------------------
+
+- Added support for dot notation and version 1.1.2 of the spec (issue
+ \#99). [rbp]
+- Missing partials now render as empty string per latest version of
+ spec (issue \#115).
+- Bugfix: falsey values now coerced to strings using str().
+- Bugfix: lambda return values for sections no longer pushed onto
+ context stack (issue \#113).
+- Bugfix: lists of lambdas for sections were not rendered (issue
+ \#114).
+
+0.5.1 (2012-04-24)
+------------------
+
+- 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)
+------------------
+
+This version represents a major rewrite and refactoring of the code base
+that also adds features and fixes many bugs. All functionality and
+nearly all unit tests have been preserved. However, some backwards
+incompatible changes to the API have been made.
+
+Below is a selection of some of the changes (not exhaustive).
+
+Highlights:
+
+- Pystache now passes all tests in version 1.0.3 of the [Mustache
+ spec](https://github.com/mustache/spec). [pvande]
+- Removed View class: it is no longer necessary to subclass from View
+ or from any other class to create a view.
+- Replaced Template with Renderer class: template rendering behavior
+ can be modified via the Renderer constructor or by setting
+ attributes on a Renderer instance.
+- Added TemplateSpec class: template rendering can be specified on a
+ per-view basis by subclassing from TemplateSpec.
+- Introduced separation of concerns and removed circular dependencies
+ (e.g. between Template and View classes, cf. [issue
+ \#13](https://github.com/defunkt/pystache/issues/13)).
+- Unicode now used consistently throughout the rendering process.
+- Expanded test coverage: nosetests now runs doctests and \~105 test
+ cases from the Mustache spec (increasing the number of tests from 56
+ to \~315).
+- Added a rudimentary benchmarking script to gauge performance while
+ refactoring.
+- Extensive documentation added (e.g. docstrings).
+
+Other changes:
+
+- Added a command-line interface. [vrde]
+- The main rendering class now accepts a custom partial loader (e.g. a
+ dictionary) and a custom escape function.
+- Non-ascii characters in str strings are now supported while
+ rendering.
+- Added string encoding, file encoding, and errors options for
+ decoding to unicode.
+- Removed the output encoding option.
+- Removed the use of markupsafe.
+
+Bug fixes:
+
+- Context values no longer processed as template strings.
+ [jakearchibald]
+- Whitespace surrounding sections is no longer altered, per the spec.
+ [heliodor]
+- Zeroes now render correctly when using PyPy. [alex]
+- Multline comments now permitted. [fczuardi]
+- Extensionless template files are now supported.
+- Passing `**kwargs` to `Template()` no longer modifies the context.
+- Passing `**kwargs` to `Template()` with no context no longer raises
+ an exception.
+
+0.4.1 (2012-03-25)
+------------------
+
+- Added support for Python 2.4. [wangtz, jvantuyl]
+
+0.4.0 (2011-01-12)
+------------------
+
+- Add support for nested contexts (within template and view)
+- Add support for inverted lists
+- Decoupled template loading
+
+0.3.1 (2010-05-07)
+------------------
+
+- Fix package
+
+0.3.0 (2010-05-03)
+------------------
+
+- View.template\_path can now hold a list of path
+- Add {{& blah}} as an alias for {{{ blah }}}
+- Higher Order Sections
+- Inverted sections
+
+0.2.0 (2010-02-15)
+------------------
+
+- Bugfix: Methods returning False or None are not rendered
+- Bugfix: Don't render an empty string when a tag's value is 0.
+ [enaeseth]
+- Add support for using non-callables as View attributes.
+ [joshthecoder]
+- Allow using View instances as attributes. [joshthecoder]
+- Support for Unicode and non-ASCII-encoded bytestring output.
+ [enaeseth]
+- Template file encoding awareness. [enaeseth]
+
+0.1.1 (2009-11-13)
+------------------
+
+- Ensure we're dealing with strings, always
+- Tests can be run by executing the test file directly
+
+0.1.0 (2009-11-12)
+------------------
+
+- First release
diff --git a/HISTORY.rst b/HISTORY.rst
deleted file mode 100644
index 9dab5cb..0000000
--- a/HISTORY.rst
+++ /dev/null
@@ -1,117 +0,0 @@
-History
-=======
-
-0.5.2 (2012-05-03)
-------------------
-
-* Added support for dot notation and version 1.1.2 of the spec (issue #99). [rbp]
-* Missing partials now render as empty string per latest version of spec (issue #115).
-* Bugfix: falsey values now coerced to strings using str().
-* Bugfix: lambda return values for sections no longer pushed onto context stack (issue #113).
-* Bugfix: lists of lambdas for sections were not rendered (issue #114).
-
-0.5.1 (2012-04-24)
-------------------
-
-* 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)
-------------------
-
-This version represents a major rewrite and refactoring of the code base
-that also adds features and fixes many bugs. All functionality and nearly
-all unit tests have been preserved. However, some backwards incompatible
-changes to the API have been made.
-
-Below is a selection of some of the changes (not exhaustive).
-
-Highlights:
-
-* Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande]
-* Removed View class: it is no longer necessary to subclass from View or
- from any other class to create a view.
-* Replaced Template with Renderer class: template rendering behavior can be
- modified via the Renderer constructor or by setting attributes on a Renderer instance.
-* Added TemplateSpec class: template rendering can be specified on a per-view
- basis by subclassing from TemplateSpec.
-* Introduced separation of concerns and removed circular dependencies (e.g.
- between Template and View classes, cf. `issue #13`_).
-* Unicode now used consistently throughout the rendering process.
-* Expanded test coverage: nosetests now runs doctests and ~105 test cases
- from the Mustache spec (increasing the number of tests from 56 to ~315).
-* Added a rudimentary benchmarking script to gauge performance while refactoring.
-* Extensive documentation added (e.g. docstrings).
-
-Other changes:
-
-* Added a command-line interface. [vrde]
-* The main rendering class now accepts a custom partial loader (e.g. a dictionary)
- and a custom escape function.
-* Non-ascii characters in str strings are now supported while rendering.
-* Added string encoding, file encoding, and errors options for decoding to unicode.
-* Removed the output encoding option.
-* Removed the use of markupsafe.
-
-Bug fixes:
-
-* Context values no longer processed as template strings. [jakearchibald]
-* Whitespace surrounding sections is no longer altered, per the spec. [heliodor]
-* Zeroes now render correctly when using PyPy. [alex]
-* Multline comments now permitted. [fczuardi]
-* Extensionless template files are now supported.
-* Passing ``**kwargs`` to ``Template()`` no longer modifies the context.
-* Passing ``**kwargs`` to ``Template()`` with no context no longer raises an exception.
-
-0.4.1 (2012-03-25)
-------------------
-* Added support for Python 2.4. [wangtz, jvantuyl]
-
-0.4.0 (2011-01-12)
-------------------
-* Add support for nested contexts (within template and view)
-* Add support for inverted lists
-* Decoupled template loading
-
-0.3.1 (2010-05-07)
-------------------
-
-* Fix package
-
-0.3.0 (2010-05-03)
-------------------
-
-* View.template_path can now hold a list of path
-* Add {{& blah}} as an alias for {{{ blah }}}
-* Higher Order Sections
-* Inverted sections
-
-0.2.0 (2010-02-15)
-------------------
-
-* Bugfix: Methods returning False or None are not rendered
-* Bugfix: Don't render an empty string when a tag's value is 0. [enaeseth]
-* Add support for using non-callables as View attributes. [joshthecoder]
-* Allow using View instances as attributes. [joshthecoder]
-* Support for Unicode and non-ASCII-encoded bytestring output. [enaeseth]
-* Template file encoding awareness. [enaeseth]
-
-0.1.1 (2009-11-13)
-------------------
-
-* Ensure we're dealing with strings, always
-* Tests can be run by executing the test file directly
-
-0.1.0 (2009-11-12)
-------------------
-
-* 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/LICENSE b/LICENSE
index 1943585..42be9d6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,5 @@
Copyright (C) 2012 Chris Jerdonek. All rights reserved.
+
Copyright (c) 2009 Chris Wanstrath
Permission is hereby granted, free of charge, to any person obtaining
diff --git a/MANIFEST.in b/MANIFEST.in
index 56a1d52..5a864fd 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,8 @@
+include README.md
+include HISTORY.md
include LICENSE
-include HISTORY.rst
-include README.rst
+include TODO.md
+include setup_description.rst
include tox.ini
include test_pystache.py
# You cannot use package_data, for example, to include data files in a
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8a4bf18
--- /dev/null
+++ b/README.md
@@ -0,0 +1,273 @@
+Pystache
+========
+
+<!-- Since PyPI rejects reST long descriptions that contain HTML, -->
+<!-- HTML comments must be removed when converting this file to reST. -->
+<!-- For more information on PyPI's behavior in this regard, see: -->
+<!-- http://docs.python.org/distutils/uploading.html#pypi-package-display -->
+<!-- The Pystache setup script strips 1-line HTML comments prior -->
+<!-- to converting to reST, so all HTML comments should be one line. -->
+<!-- -->
+<!-- We leave the leading brackets empty here. Otherwise, unwanted -->
+<!-- caption text shows up in the reST version converted by pandoc. -->
+![](https://s3.amazonaws.com/webdev_bucket/pystache.png "mustachioed, monocled snake by David Phillips")
+
+![](https://secure.travis-ci.org/defunkt/pystache.png?branch=master,development)
+
+[Pystache](https://github.com/defunkt/pystache) is a Python
+implementation of [Mustache](http://mustache.github.com/). Mustache is a
+framework-agnostic, logic-free templating system inspired by
+[ctemplate](http://code.google.com/p/google-ctemplate/) and
+[et](http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html).
+Like ctemplate, Mustache "emphasizes separating logic from presentation:
+it is impossible to embed application logic in this template language."
+
+The [mustache(5)](http://mustache.github.com/mustache.5.html) man page
+provides a good introduction to Mustache's syntax. For a more complete
+(and more current) description of Mustache's behavior, see the official
+[Mustache spec](https://github.com/mustache/spec).
+
+Pystache is [semantically versioned](http://semver.org) and can be found
+on [PyPI](http://pypi.python.org/pypi/pystache). This version of
+Pystache passes all tests in [version
+1.1.2](https://github.com/mustache/spec/tree/v1.1.2) of the spec.
+
+
+Requirements
+------------
+
+Pystache is tested with--
+
+- Python 2.4 (requires simplejson [version
+ 2.0.9](http://pypi.python.org/pypi/simplejson/2.0.9) or earlier)
+- Python 2.5 (requires
+ [simplejson](http://pypi.python.org/pypi/simplejson/))
+- Python 2.6
+- Python 2.7
+- Python 3.1
+- Python 3.2
+- [PyPy](http://pypy.org/)
+
+[Distribute](http://packages.python.org/distribute/) (the setuptools fork)
+is recommended over [setuptools](http://pypi.python.org/pypi/setuptools),
+and is required in some cases (e.g. for Python 3 support).
+If you use [pip](http://www.pip-installer.org/), you probably already satisfy
+this requirement.
+
+JSON support is needed only for the command-line interface and to run
+the spec tests. We require simplejson for earlier versions of Python
+since Python's [json](http://docs.python.org/library/json.html) 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
+----------
+
+ pip install pystache
+
+And test it--
+
+ pystache-test
+
+To install and test from source (e.g. from GitHub), see the Develop
+section.
+
+Use It
+------
+
+ >>> import pystache
+ >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
+ Hi Mom!
+
+You can also create dedicated view classes to hold your view logic.
+
+Here's your view class (in .../examples/readme.py):
+
+ class SayHello(object):
+ def to(self):
+ return "Pizza"
+
+Instantiating like so:
+
+ >>> from pystache.tests.examples.readme import SayHello
+ >>> hello = SayHello()
+
+Then your template, say\_hello.mustache (by default in the same
+directory as your class definition):
+
+ Hello, {{to}}!
+
+Pull it together:
+
+ >>> renderer = pystache.Renderer()
+ >>> print renderer.render(hello)
+ Hello, Pizza!
+
+For greater control over rendering (e.g. to specify a custom template
+directory), use the `Renderer` class like above. One can pass attributes
+to the Renderer class constructor or set them on a Renderer instance. To
+customize template loading on a per-view basis, subclass `TemplateSpec`.
+See the docstrings of the
+[Renderer](https://github.com/defunkt/pystache/blob/master/pystache/renderer.py)
+class and
+[TemplateSpec](https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py)
+class for more information.
+
+You can also pre-parse a template:
+
+ >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}")
+ >>> print parsed
+ [u'Hey ', _SectionNode(key=u'who', index_begin=12, index_end=18, parsed=[_EscapeNode(key=u'.'), u'!'])]
+
+And then:
+
+ >>> print renderer.render(parsed, {'who': 'Pops'})
+ Hey Pops!
+ >>> print renderer.render(parsed, {'who': 'you'})
+ Hey you!
+
+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. 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
+-------
+
+This section describes how Pystache handles unicode, strings, and
+encodings.
+
+Internally, Pystache uses [only unicode
+strings](http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs)
+(`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 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 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-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 docstrings for further details. In addition, the `file_encoding`
+attribute can be controlled on a per-view basis by subclassing the
+`TemplateSpec` class. When not specified explicitly, these attributes
+default to values set in Pystache's `defaults` module.
+
+Develop
+-------
+
+To test from a source distribution (without installing)--
+
+ python test_pystache.py
+
+To test Pystache with multiple versions of Python (with a single
+command!), you can use [tox](http://pypi.python.org/pypi/tox):
+
+ pip install 'virtualenv<1.8' # Version 1.8 dropped support for Python 2.4.
+ pip install 'tox<1.4' # Version 1.4 dropped support for Python 2.4.
+ tox
+
+If you do not have all Python versions listed in `tox.ini`--
+
+ tox -e py26,py32 # for example
+
+The source distribution tests also include doctests and tests from the
+Mustache spec. To include tests from the Mustache spec in your test
+runs:
+
+ git submodule init
+ git submodule update
+
+The test harness parses the spec's (more human-readable) yaml files if
+[PyYAML](http://pypi.python.org/pypi/PyYAML) is present. Otherwise, it
+parses the json files. To install PyYAML--
+
+ pip install pyyaml
+
+To run a subset of the tests, you can use
+[nose](http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html):
+
+ pip install nose
+ nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
+
+### Using Python 3 with Pystache from source
+
+Pystache is written in Python 2 and must be converted to Python 3 prior to
+using it with Python 3. The installation process (and tox) do this
+automatically.
+
+To convert the code to Python 3 manually (while using Python 3)--
+
+ python setup.py build
+
+This writes the converted code to a subdirectory called `build`.
+By design, Python 3 builds
+[cannot](https://bitbucket.org/tarek/distribute/issue/292/allow-use_2to3-with-python-2)
+be created from Python 2.
+
+To convert the code without using setup.py, you can use
+[2to3](http://docs.python.org/library/2to3.html) as follows (two steps)--
+
+ 2to3 --write --nobackups --no-diffs --doctests_only pystache
+ 2to3 --write --nobackups --no-diffs pystache
+
+This converts the code (and doctests) in place.
+
+To `import pystache` from a source distribution while using Python 3, be
+sure that you are importing from a directory containing a converted
+version of the code (e.g. from the `build` directory after converting),
+and not from the original (unconverted) source directory. Otherwise, you will
+get a syntax error. You can help prevent this by not running the Python
+IDE from the project directory when importing Pystache while using Python 3.
+
+
+Mailing List
+------------
+
+There is a [mailing list](http://librelist.com/browser/pystache/). Note
+that there is a bit of a delay between posting a message and seeing it
+appear in the mailing list archive.
+
+Credits
+-------
+
+ >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' }
+ >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context)
+ Author: Chris Wanstrath
+ Maintainer: Chris Jerdonek
+
+Pystache logo by [David Phillips](http://davidphillips.us/) and licensed
+under a [Creative Commons Attribution-ShareAlike 3.0 Unported
+License](http://creativecommons.org/licenses/by-sa/3.0/deed.en_US).
+![](http://i.creativecommons.org/l/by-sa/3.0/88x31.png "Creative
+Commons Attribution-ShareAlike 3.0 Unported License")
diff --git a/README.rst b/README.rst
deleted file mode 100644
index e1b5118..0000000
--- a/README.rst
+++ /dev/null
@@ -1,234 +0,0 @@
-========
-Pystache
-========
-
-.. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png
-
-Pystache_ is a Python implementation of Mustache_.
-Mustache is a framework-agnostic, logic-free templating system inspired
-by ctemplate_ and et_. Like ctemplate, Mustache "emphasizes
-separating logic from presentation: it is impossible to embed application
-logic in this template language."
-
-The `mustache(5)`_ man page provides a good introduction to Mustache's
-syntax. For a more complete (and more current) description of Mustache's
-behavior, see the official `Mustache spec`_.
-
-Pystache is `semantically versioned`_ and can be found on PyPI_. This
-version of Pystache passes all tests in `version 1.1.2`_ of the spec.
-
-Logo: `David Phillips`_
-
-
-Requirements
-============
-
-Pystache is tested with--
-
-* 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. 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
-==========
-
-::
-
- pip install pystache
- pystache-test
-
-To install and test from source (e.g. from GitHub), see the Develop section.
-
-
-Use It
-======
-
-::
-
- >>> import pystache
- >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
- Hi Mom!
-
-You can also create dedicated view classes to hold your view logic.
-
-Here's your view class (in examples/readme.py)::
-
- class SayHello(object):
-
- def to(self):
- return "Pizza"
-
-Like so::
-
- >>> from pystache.tests.examples.readme import SayHello
- >>> hello = SayHello()
-
-Then your template, say_hello.mustache (in the same directory by default
-as your class definition)::
-
- Hello, {{to}}!
-
-Pull it together::
-
- >>> renderer = pystache.Renderer()
- >>> 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
-=======
-
-This section describes how Pystache handles unicode, strings, and encodings.
-
-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 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 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-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
-docstrings for further details. In addition, the ``file_encoding``
-attribute can be controlled on a per-view basis by subclassing the
-``TemplateSpec`` class. When not specified explicitly, these attributes
-default to values set in Pystache's ``defaults`` module.
-
-
-Develop
-=======
-
-To test from a source distribution (without installing)-- ::
-
- python test_pystache.py
-
-To test Pystache with multiple versions of Python (with a single command!),
-you can use tox_: ::
-
- pip install tox
- tox
-
-If you do not have all Python versions listed in ``tox.ini``-- ::
-
- tox -e py26,py32 # for example
-
-The source distribution tests also include doctests and tests from the
-Mustache spec. To include tests from the Mustache spec in your test runs: ::
-
- git submodule init
- git submodule update
-
-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-- ::
-
- pip install pyyaml
-
-To run a subset of the tests, you can use nose_: ::
-
- pip install nose
- nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
-
-**Running Pystache from source with Python 3.** Pystache is written in
-Python 2 and must be converted with 2to3_ prior to running under Python 3.
-The installation process (and tox) do this conversion automatically.
-
-To ``import pystache`` from a source distribution while using Python 3,
-be sure that you are importing from a directory containing a converted
-version (e.g. from your site-packages directory after manually installing)
-and not from the original source directory. Otherwise, you will get a
-syntax error. You can help ensure this by not running the Python IDE
-from the project directory when importing Pystache.
-
-
-Mailing List
-============
-
-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.
-
-
-Authors
-=======
-
-::
-
- >>> 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
-.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
-.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
-.. _PyPI: http://pypi.python.org/pypi/pystache
-.. _Pystache: https://github.com/defunkt/pystache
-.. _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/
-.. _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.1.2: https://github.com/mustache/spec/tree/v1.1.2
-.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
diff --git a/TODO.md b/TODO.md
index 54f2851..00c675a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,6 +1,15 @@
TODO
====
+In master branch, after merging to master:
+
+* Enable web page after merging.
+* Change README to link to the repo version of the logo.
+
+In development branch:
+
+* End support for Python 2.4.
+* Add Python 3.3 to tox file (after deprecating 2.4).
* 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.
diff --git a/gh/images/logo_phillips.png b/gh/images/logo_phillips.png
new file mode 100644
index 0000000..7491901
--- /dev/null
+++ b/gh/images/logo_phillips.png
Binary files differ
diff --git a/pystache/__init__.py b/pystache/__init__.py
index b07eb65..59dac98 100644
--- a/pystache/__init__.py
+++ b/pystache/__init__.py
@@ -6,8 +6,8 @@ TODO: add a docstring.
# We keep all initialization code in a separate module.
-from pystache.init import render, Renderer, TemplateSpec
+from pystache.init import parse, render, Renderer, TemplateSpec
-__all__ = ['render', 'Renderer', 'TemplateSpec']
+__all__ = ['parse', 'render', 'Renderer', 'TemplateSpec']
-__version__ = '0.5.2' # Also change in setup.py.
+__version__ = '0.5.3-rc' # Also change in setup.py.
diff --git a/pystache/common.py b/pystache/common.py
index c1fd7a1..fb266dd 100644
--- a/pystache/common.py
+++ b/pystache/common.py
@@ -5,6 +5,33 @@ Exposes functionality needed throughout the project.
"""
+from sys import version_info
+
+def _get_string_types():
+ # 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 version_info < (3, ):
+ return basestring
+ # The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3.
+ return (unicode, type(u"a".encode('utf-8')))
+
+
+_STRING_TYPES = _get_string_types()
+
+
+def is_string(obj):
+ """
+ Return whether the given object is a byte string or unicode string.
+
+ This function is provided for compatibility with both Python 2 and 3
+ when using 2to3.
+
+ """
+ return isinstance(obj, _STRING_TYPES)
+
+
# This function was designed to be portable across Python versions -- both
# with older versions and with Python 3 after applying 2to3.
def read(path):
@@ -26,6 +53,14 @@ def read(path):
f.close()
+class MissingTags(object):
+
+ """Contains the valid values for Renderer.missing_tags."""
+
+ ignore = 'ignore'
+ strict = 'strict'
+
+
class PystacheError(Exception):
"""Base class for Pystache exceptions."""
pass
diff --git a/pystache/context.py b/pystache/context.py
index 8a95059..6715916 100644
--- a/pystache/context.py
+++ b/pystache/context.py
@@ -14,6 +14,9 @@ spec, we define these categories mutually exclusively as follows:
"""
+from pystache.common import PystacheError
+
+
# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
_BUILTIN_MODULE = type(0).__module__
@@ -55,8 +58,15 @@ def _get_value(context, key):
# types like integers and strings as objects (cf. issue #81).
# Instances of user-defined classes on the other hand, for example,
# are considered objects by the test above.
- if hasattr(context, key):
+ try:
attr = getattr(context, key)
+ except AttributeError:
+ # TODO: distinguish the case of the attribute not existing from
+ # an AttributeError being raised by the call to the attribute.
+ # See the following issue for implementation ideas:
+ # http://bugs.python.org/issue7559
+ pass
+ else:
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if callable(attr):
@@ -66,6 +76,21 @@ def _get_value(context, key):
return _NOT_FOUND
+class KeyNotFoundError(PystacheError):
+
+ """
+ An exception raised when a key is not found in a context stack.
+
+ """
+
+ def __init__(self, key, details):
+ self.key = key
+ self.details = details
+
+ def __str__(self):
+ return "Key %s not found: %s" % (repr(self.key), self.details)
+
+
class ContextStack(object):
"""
@@ -175,7 +200,7 @@ class ContextStack(object):
# TODO: add more unit tests for this.
# TODO: update the docstring for dotted names.
- def get(self, name, default=u''):
+ def get(self, name):
"""
Resolve a dotted name against the current context stack.
@@ -245,18 +270,19 @@ class ContextStack(object):
"""
if name == '.':
- # TODO: should we add a test case for an empty context stack?
- return self.top()
+ try:
+ return self.top()
+ except IndexError:
+ raise KeyNotFoundError(".", "empty context stack")
parts = name.split('.')
- result = self._get_simple(parts[0])
+ try:
+ result = self._get_simple(parts[0])
+ except KeyNotFoundError:
+ raise KeyNotFoundError(name, "first part")
for part in parts[1:]:
- # TODO: consider using EAFP here instead.
- # http://docs.python.org/glossary.html#term-eafp
- if result is _NOT_FOUND:
- break
# The full context stack is not used to resolve the remaining parts.
# From the spec--
#
@@ -268,9 +294,10 @@ class ContextStack(object):
#
# TODO: make sure we have a test case for the above point.
result = _get_value(result, part)
-
- if result is _NOT_FOUND:
- return default
+ # TODO: consider using EAFP here instead.
+ # http://docs.python.org/glossary.html#term-eafp
+ if result is _NOT_FOUND:
+ raise KeyNotFoundError(name, "missing %s" % repr(part))
return result
@@ -279,16 +306,12 @@ class ContextStack(object):
Query the stack for a non-dotted name.
"""
- result = _NOT_FOUND
-
for item in reversed(self._stack):
result = _get_value(item, name)
- if result is _NOT_FOUND:
- continue
- # Otherwise, the key was found.
- break
+ if result is not _NOT_FOUND:
+ return result
- return result
+ raise KeyNotFoundError(name, "part missing")
def push(self, item):
"""
diff --git a/pystache/defaults.py b/pystache/defaults.py
index fcd04c3..bcfdf4c 100644
--- a/pystache/defaults.py
+++ b/pystache/defaults.py
@@ -17,6 +17,8 @@ except ImportError:
import os
import sys
+from pystache.common import MissingTags
+
# How to handle encoding errors when decoding strings from str to unicode.
#
@@ -36,6 +38,12 @@ STRING_ENCODING = sys.getdefaultencoding()
# strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding()
+# The delimiters to start with when parsing.
+DELIMITERS = (u'{{', u'}}')
+
+# How to handle missing tags when rendering a template.
+MISSING_TAGS = MissingTags.ignore
+
# The starting list of directories in which to search for templates when
# loading a template by file name.
SEARCH_DIRS = [os.curdir] # i.e. ['.']
@@ -53,5 +61,5 @@ SEARCH_DIRS = [os.curdir] # i.e. ['.']
#
TAG_ESCAPE = lambda u: escape(u, quote=True)
-# The default template extension.
+# The default template extension, without the leading dot.
TEMPLATE_EXTENSION = 'mustache'
diff --git a/pystache/init.py b/pystache/init.py
index e9d854d..38bb1f5 100644
--- a/pystache/init.py
+++ b/pystache/init.py
@@ -5,6 +5,7 @@ This module contains the initialization logic called by __init__.py.
"""
+from pystache.parser import parse
from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
diff --git a/pystache/loader.py b/pystache/loader.py
index 0fdadc5..d4a7e53 100644
--- a/pystache/loader.py
+++ b/pystache/loader.py
@@ -33,6 +33,8 @@ class Loader(object):
"""
Loads the template associated to a name or user-defined object.
+ All load_*() methods return the template as a unicode string.
+
"""
def __init__(self, file_encoding=None, extension=None, to_unicode=None,
@@ -42,9 +44,9 @@ class Loader(object):
Arguments:
- extension: the template file extension. Pass False for no
- extension (i.e. to use extensionless template files).
- Defaults to the package default.
+ extension: the template file extension, without the leading dot.
+ Pass False for no extension (e.g. to use extensionless template
+ files). Defaults to the package default.
file_encoding: the name of the encoding to use when converting file
contents to unicode. Defaults to the package default.
@@ -119,17 +121,29 @@ class Loader(object):
return self.unicode(b, encoding)
- # TODO: unit-test this method.
+ def load_file(self, file_name):
+ """
+ Find and return the template with the given file name.
+
+ Arguments:
+
+ file_name: the file name of the template.
+
+ """
+ locator = self._make_locator()
+
+ path = locator.find_file(file_name, self.search_dirs)
+
+ return self.read(path)
+
def load_name(self, name):
"""
- Find and return the template with the given name.
+ Find and return the template with the given template name.
Arguments:
name: the name of the template.
- search_dirs: the list of directories in which to search.
-
"""
locator = self._make_locator()
diff --git a/pystache/locator.py b/pystache/locator.py
index 2189cf2..30c5b01 100644
--- a/pystache/locator.py
+++ b/pystache/locator.py
@@ -21,9 +21,9 @@ class Locator(object):
Arguments:
- extension: the template file extension. Pass False for no
- extension (i.e. to use extensionless template files).
- Defaults to the package default.
+ extension: the template file extension, without the leading dot.
+ Pass False for no extension (e.g. to use extensionless template
+ files). Defaults to the package default.
"""
if extension is None:
@@ -123,10 +123,29 @@ class Locator(object):
return path
+ def find_file(self, file_name, search_dirs):
+ """
+ Return the path to a template with the given file name.
+
+ Arguments:
+
+ file_name: the file name of the template.
+
+ search_dirs: the list of directories in which to search.
+
+ """
+ return self._find_path_required(search_dirs, file_name)
+
def find_name(self, template_name, search_dirs):
"""
Return the path to a template with the given name.
+ Arguments:
+
+ template_name: the name of the template.
+
+ search_dirs: the list of directories in which to search.
+
"""
file_name = self.make_file_name(template_name)
diff --git a/pystache/parsed.py b/pystache/parsed.py
index a37565b..372d96c 100644
--- a/pystache/parsed.py
+++ b/pystache/parsed.py
@@ -3,50 +3,48 @@
"""
Exposes a class that represents a parsed (or compiled) template.
-This module is meant only for internal use.
-
"""
class ParsedTemplate(object):
- def __init__(self, parse_tree):
- """
- Arguments:
+ """
+ Represents a parsed or compiled template.
- parse_tree: a list, each element of which is either--
+ An instance wraps a list of unicode strings and node objects. A node
+ object must have a `render(engine, stack)` method that accepts a
+ RenderEngine instance and a ContextStack instance and returns a unicode
+ string.
- (1) a unicode string, or
- (2) a "rendering" callable that accepts a ContextStack instance
- and returns a unicode string.
+ """
- The possible rendering callables are the return values of the
- following functions:
+ def __init__(self):
+ self._parse_tree = []
- * RenderEngine._make_get_escaped()
- * RenderEngine._make_get_inverse()
- * RenderEngine._make_get_literal()
- * RenderEngine._make_get_partial()
- * RenderEngine._make_get_section()
+ def __repr__(self):
+ return repr(self._parse_tree)
+ def add(self, node):
"""
- self._parse_tree = parse_tree
+ Arguments:
- def __repr__(self):
- return "[%s]" % (", ".join([repr(part) for part in self._parse_tree]))
+ node: a unicode string or node object instance. See the class
+ docstring for information.
+
+ """
+ self._parse_tree.append(node)
- def render(self, context):
+ def render(self, engine, context):
"""
Returns: a string of type unicode.
"""
# We avoid use of the ternary operator for Python 2.4 support.
- def get_unicode(val):
- if callable(val):
- return val(context)
- return val
+ def get_unicode(node):
+ if type(node) is unicode:
+ return node
+ return node.render(engine, context)
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
return unicode(s)
-
diff --git a/pystache/parser.py b/pystache/parser.py
index 4e05f3b..c6a171f 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -1,31 +1,51 @@
# coding: utf-8
"""
-Provides a class for parsing template strings.
-
-This module is only meant for internal use by the renderengine module.
+Exposes a parse() function to parse template strings.
"""
import re
-from pystache.common import TemplateNotFoundError
+from pystache import defaults
from pystache.parsed import ParsedTemplate
-DEFAULT_DELIMITERS = (u'{{', u'}}')
END_OF_LINE_CHARACTERS = [u'\r', u'\n']
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
-def _compile_template_re(delimiters=None):
+# TODO: add some unit tests for this.
+# TODO: add a test case that checks for spurious spaces.
+# TODO: add test cases for delimiters.
+def parse(template, delimiters=None):
"""
- Return a regular expresssion object (re.RegexObject) instance.
+ Parse a unicode template string and return a ParsedTemplate instance.
+
+ Arguments:
+
+ template: a unicode template string.
+
+ delimiters: a 2-tuple of delimiters. Defaults to the package default.
+
+ Examples:
+
+ >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}")
+ >>> print str(parsed).replace('u', '') # This is a hack to get the test to pass both in Python 2 and 3.
+ ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])]
"""
- if delimiters is None:
- delimiters = DEFAULT_DELIMITERS
+ if type(template) is not unicode:
+ raise Exception("Template is not unicode: %s" % type(template))
+ parser = _Parser(delimiters)
+ return parser.parse(template)
+
+
+def _compile_template_re(delimiters):
+ """
+ Return a regular expresssion object (re.RegexObject) instance.
+ """
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
tag_types = "!>&/#^"
@@ -54,34 +74,172 @@ class ParsingError(Exception):
pass
-class Parser(object):
+## Node types
- _delimiters = None
- _template_re = None
+def _format(obj, exclude=None):
+ if exclude is None:
+ exclude = []
+ exclude.append('key')
+ attrs = obj.__dict__
+ names = list(set(attrs.keys()) - set(exclude))
+ names.sort()
+ names.insert(0, 'key')
+ args = ["%s=%s" % (name, repr(attrs[name])) for name in names]
+ return "%s(%s)" % (obj.__class__.__name__, ", ".join(args))
- def __init__(self, engine, delimiters=None):
- """
- Construct an instance.
- Arguments:
+class _CommentNode(object):
- engine: a RenderEngine instance.
+ def __repr__(self):
+ return _format(self)
- """
+ def render(self, engine, context):
+ return u''
+
+
+class _ChangeNode(object):
+
+ def __init__(self, delimiters):
+ self.delimiters = delimiters
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ return u''
+
+
+class _EscapeNode(object):
+
+ def __init__(self, key):
+ self.key = key
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ s = engine.fetch_string(context, self.key)
+ return engine.escape(s)
+
+
+class _LiteralNode(object):
+
+ def __init__(self, key):
+ self.key = key
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ s = engine.fetch_string(context, self.key)
+ return engine.literal(s)
+
+
+class _PartialNode(object):
+
+ def __init__(self, key, indent):
+ self.key = key
+ self.indent = indent
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ template = engine.resolve_partial(self.key)
+ # Indent before rendering.
+ template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template)
+
+ return engine.render(template, context)
+
+
+class _InvertedNode(object):
+
+ def __init__(self, key, parsed_section):
+ self.key = key
+ self.parsed_section = parsed_section
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ # TODO: is there a bug because we are not using the same
+ # logic as in fetch_string()?
+ data = engine.resolve_context(context, self.key)
+ # Note that lambdas are considered truthy for inverted sections
+ # per the spec.
+ if data:
+ return u''
+ return self.parsed_section.render(engine, context)
+
+
+class _SectionNode(object):
+
+ # TODO: the template_ and parsed_template_ arguments don't both seem
+ # to be necessary. Can we remove one of them? For example, if
+ # callable(data) is True, then the initial parsed_template isn't used.
+ def __init__(self, key, parsed, delimiters, template, index_begin, index_end):
+ self.delimiters = delimiters
+ self.key = key
+ self.parsed = parsed
+ self.template = template
+ self.index_begin = index_begin
+ self.index_end = index_end
+
+ def __repr__(self):
+ return _format(self, exclude=['delimiters', 'template'])
+
+ def render(self, engine, context):
+ values = engine.fetch_section_data(context, self.key)
+
+ parts = []
+ for val in values:
+ if callable(val):
+ # Lambdas special case section rendering and bypass pushing
+ # the data value onto the context stack. From the spec--
+ #
+ # When used as the data value for a Section tag, the
+ # lambda MUST be treatable as an arity 1 function, and
+ # invoked as such (passing a String containing the
+ # unprocessed section contents). The returned value
+ # MUST be rendered against the current delimiters, then
+ # interpolated in place of the section.
+ #
+ # Also see--
+ #
+ # https://github.com/defunkt/pystache/issues/113
+ #
+ # TODO: should we check the arity?
+ val = val(self.template[self.index_begin:self.index_end])
+ val = engine._render_value(val, context, delimiters=self.delimiters)
+ parts.append(val)
+ continue
+
+ context.push(val)
+ parts.append(self.parsed.render(engine, context))
+ context.pop()
+
+ return unicode(''.join(parts))
+
+
+class _Parser(object):
+
+ _delimiters = None
+ _template_re = None
+
+ def __init__(self, delimiters=None):
if delimiters is None:
- delimiters = DEFAULT_DELIMITERS
+ delimiters = defaults.DELIMITERS
self._delimiters = delimiters
- self.engine = engine
- def compile_template_re(self):
+ def _compile_delimiters(self):
self._template_re = _compile_template_re(self._delimiters)
def _change_delimiters(self, delimiters):
self._delimiters = delimiters
- self.compile_template_re()
+ self._compile_delimiters()
- def parse(self, template, start_index=0, section_key=None):
+ def parse(self, template):
"""
Parse a template string starting at some index.
@@ -98,11 +256,16 @@ class Parser(object):
a ParsedTemplate instance.
"""
- parse_tree = []
- index = start_index
+ self._compile_delimiters()
+
+ start_index = 0
+ content_end_index, parsed_section, section_key = None, None, None
+ parsed_template = ParsedTemplate()
+
+ states = []
while True:
- match = self._template_re.search(template, index)
+ match = self._template_re.search(template, start_index)
if match is None:
break
@@ -110,10 +273,6 @@ class Parser(object):
match_index = match.start()
end_index = match.end()
- before_tag = template[index : match_index]
-
- parse_tree.append(before_tag)
-
matches = match.groupdict()
# Normalize the matches dictionary.
@@ -138,100 +297,82 @@ class Parser(object):
if end_index < len(template):
end_index += template[end_index] == '\n' and 1 or 0
elif leading_whitespace:
- parse_tree.append(leading_whitespace)
match_index += len(leading_whitespace)
leading_whitespace = ''
- if tag_type == '/':
- if tag_key != section_key:
- raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != match_index:
+ parsed_template.add(template[start_index:match_index])
- return ParsedTemplate(parse_tree), match_index, end_index
+ start_index = end_index
- index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
+ if tag_type in ('#', '^'):
+ # Cache current state.
+ state = (tag_type, end_index, section_key, parsed_template)
+ states.append(state)
- # Save the rest of the template.
- parse_tree.append(template[index:])
+ # Initialize new state
+ section_key, parsed_template = tag_key, ParsedTemplate()
+ continue
- return ParsedTemplate(parse_tree)
-
- def _parse_section(self, template, start_index, section_key):
- """
- Parse the contents of a template section.
-
- Arguments:
-
- template: a unicode template string.
+ if tag_type == '/':
+ if tag_key != section_key:
+ raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
- start_index: the string index at which the section contents begin.
+ # Restore previous state with newly found section data.
+ parsed_section = parsed_template
- section_key: the tag key of the section.
+ (tag_type, section_start_index, section_key, parsed_template) = states.pop()
+ node = self._make_section_node(template, tag_type, tag_key, parsed_section,
+ section_start_index, match_index)
- Returns: a 3-tuple:
+ else:
+ node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace)
- parsed_section: the section contents parsed as a ParsedTemplate
- instance.
+ parsed_template.add(node)
- content_end_index: the string index after the section contents.
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != len(template):
+ parsed_template.add(template[start_index:])
- end_index: the string index after the closing section tag (and
- including any trailing newlines).
+ return parsed_template
+ def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
"""
- parsed_section, content_end_index, end_index = \
- self.parse(template=template, start_index=start_index, section_key=section_key)
-
- 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):
+ Create and return a non-section node for the parse tree.
+ """
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!':
- return end_index
+ return _CommentNode()
if tag_type == '=':
delimiters = tag_key.split()
self._change_delimiters(delimiters)
- return end_index
-
- engine = self.engine
+ return _ChangeNode(delimiters)
if tag_type == '':
+ return _EscapeNode(tag_key)
- func = engine._make_get_escaped(tag_key)
-
- elif tag_type == '&':
+ if tag_type == '&':
+ return _LiteralNode(tag_key)
- func = engine._make_get_literal(tag_key)
+ if tag_type == '>':
+ return _PartialNode(tag_key, leading_whitespace)
- elif tag_type == '#':
+ raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))
- 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, section_contents, end_index = self._parse_section(template, end_index, tag_key)
- func = engine._make_get_inverse(tag_key, parsed_section)
-
- elif tag_type == '>':
-
- try:
- # TODO: make engine.load() and test it separately.
- template = engine.load_partial(tag_key)
- except TemplateNotFoundError:
- template = u''
-
- # Indent before rendering.
- template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
-
- func = engine._make_get_partial(template)
-
- else:
-
- raise Exception("Unrecognized tag type: %s" % repr(tag_type))
+ def _make_section_node(self, template, tag_type, tag_key, parsed_section,
+ section_start_index, section_end_index):
+ """
+ Create and return a section node for the parse tree.
- parse_tree.append(func)
+ """
+ if tag_type == '#':
+ return _SectionNode(tag_key, parsed_section, self._delimiters,
+ template, section_start_index, section_end_index)
- return end_index
+ if tag_type == '^':
+ return _InvertedNode(tag_key, parsed_section)
+ raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index bdbb30a..c797b17 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -7,7 +7,16 @@ Defines a class responsible for rendering logic.
import re
-from pystache.parser import Parser
+from pystache.common import is_string
+from pystache.parser import parse
+
+
+def context_get(stack, name):
+ """
+ Find and return a name from a ContextStack instance.
+
+ """
+ return stack.get(name)
class RenderEngine(object):
@@ -29,15 +38,15 @@ class RenderEngine(object):
"""
- def __init__(self, load_partial=None, literal=None, escape=None):
+ # TODO: it would probably be better for the constructor to accept
+ # and set as an attribute a single RenderResolver instance
+ # that encapsulates the customizable aspects of converting
+ # strings and resolving partials and names from context.
+ def __init__(self, literal=None, escape=None, resolve_context=None,
+ resolve_partial=None, to_str=None):
"""
Arguments:
- load_partial: the function to call when loading a partial. The
- function should accept a string template name and return a
- template string of type unicode (not a subclass). If the
- template is not found, it should raise a TemplateNotFoundError.
-
literal: the function used to convert unescaped variable tag
values to unicode, e.g. the value corresponding to a tag
"{{{name}}}". The function should accept a string of type
@@ -59,217 +68,114 @@ class RenderEngine(object):
incoming strings of type markupsafe.Markup differently
from plain unicode strings.
+ resolve_context: the function to call to resolve a name against
+ a context stack. The function should accept two positional
+ arguments: a ContextStack instance and a name to resolve.
+
+ resolve_partial: the function to call when loading a partial.
+ The function should accept a template name string and return a
+ template string of type unicode (not a subclass).
+
+ to_str: a function that accepts an object and returns a string (e.g.
+ the built-in function str). This function is used for string
+ coercion whenever a string is required (e.g. for converting None
+ or 0 to a string).
+
"""
self.escape = escape
self.literal = literal
- self.load_partial = load_partial
-
- # TODO: rename context to stack throughout this module.
- def _get_string_value(self, context, tag_name):
+ self.resolve_context = resolve_context
+ self.resolve_partial = resolve_partial
+ self.to_str = to_str
+
+ # TODO: Rename context to stack throughout this module.
+
+ # From the spec:
+ #
+ # When used as the data value for an Interpolation tag, the lambda
+ # MUST be treatable as an arity 0 function, and invoked as such.
+ # The returned value MUST be rendered against the default delimiters,
+ # then interpolated in place of the lambda.
+ #
+ def fetch_string(self, context, name):
"""
Get a value from the given context as a basestring instance.
"""
- val = context.get(tag_name)
+ val = self.resolve_context(context, name)
if callable(val):
- # According to the spec:
- #
- # When used as the data value for an Interpolation tag,
- # the lambda MUST be treatable as an arity 0 function,
- # and invoked as such. The returned value MUST be
- # rendered against the default delimiters, then
- # interpolated in place of the lambda.
- template = val()
- if not isinstance(template, basestring):
- # In case the template is an integer, for example.
- template = str(template)
- if type(template) is not unicode:
- template = self.literal(template)
- val = self._render(template, context)
-
- if not isinstance(val, basestring):
- val = str(val)
+ # Return because _render_value() is already a string.
+ return self._render_value(val(), context)
+
+ if not is_string(val):
+ return self.to_str(val)
return val
- def _make_get_literal(self, name):
- def get_literal(context):
- """
- Returns: a string of type unicode.
-
- """
- s = self._get_string_value(context, name)
- s = self.literal(s)
- return s
-
- return get_literal
-
- def _make_get_escaped(self, name):
- get_literal = self._make_get_literal(name)
-
- def get_escaped(context):
- """
- Returns: a string of type unicode.
-
- """
- s = self._get_string_value(context, name)
- s = self.escape(s)
- return s
-
- return get_escaped
-
- def _make_get_partial(self, template):
- def get_partial(context):
- """
- Returns: a string of type unicode.
-
- """
- # TODO: the parsing should be done before calling this function.
- return self._render(template, context)
-
- return get_partial
-
- def _make_get_inverse(self, name, parsed_template):
- def get_inverse(context):
- """
- Returns a string with type unicode.
-
- """
- # TODO: is there a bug because we are not using the same
- # logic as in _get_string_value()?
- data = context.get(name)
- # Per the spec, lambdas in inverted sections are considered truthy.
- if data:
- return u''
- return parsed_template.render(context)
-
- return get_inverse
-
- # TODO: the template_ and parsed_template_ arguments don't both seem
- # to be necessary. Can we remove one of them? For example, if
- # callable(data) is True, then the initial parsed_template isn't used.
- def _make_get_section(self, name, parsed_template_, template_, delims):
- def get_section(context):
- """
- Returns: a string of type unicode.
-
- """
- template = template_
- parsed_template = parsed_template_
- data = context.get(name)
-
- # From the spec:
+ def fetch_section_data(self, context, name):
+ """
+ Fetch the value of a section as a list.
+
+ """
+ data = self.resolve_context(context, name)
+
+ # From the spec:
+ #
+ # If the data is not of a list type, it is coerced into a list
+ # as follows: if the data is truthy (e.g. `!!data == true`),
+ # use a single-element list containing the data, otherwise use
+ # an empty list.
+ #
+ if not data:
+ data = []
+ else:
+ # The least brittle way to determine whether something
+ # supports iteration is by trying to call iter() on it:
#
- # If the data is not of a list type, it is coerced into a list
- # as follows: if the data is truthy (e.g. `!!data == true`),
- # use a single-element list containing the data, otherwise use
- # an empty list.
+ # http://docs.python.org/library/functions.html#iter
#
- if not data:
- data = []
+ # 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:
- # The least brittle way to determine whether something
- # supports iteration is by trying to call iter() on it:
- #
- # http://docs.python.org/library/functions.html#iter
- #
- # 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.
+ if is_string(data) or isinstance(data, dict):
+ # Do not treat strings and dicts (which are iterable) as lists.
data = [data]
- else:
- if isinstance(data, (basestring, dict)):
- # Do not treat strings and dicts (which are iterable) as lists.
- data = [data]
- # Otherwise, treat the value as a list.
-
- parts = []
- for element in data:
- if callable(element):
- # Lambdas special case section rendering and bypass pushing
- # the data value onto the context stack. From the spec--
- #
- # When used as the data value for a Section tag, the
- # lambda MUST be treatable as an arity 1 function, and
- # invoked as such (passing a String containing the
- # unprocessed section contents). The returned value
- # MUST be rendered against the current delimiters, then
- # interpolated in place of the section.
- #
- # Also see--
- #
- # https://github.com/defunkt/pystache/issues/113
- #
- # TODO: should we check the arity?
- new_template = element(template)
- new_parsed_template = self._parse(new_template, delimiters=delims)
- parts.append(new_parsed_template.render(context))
- continue
-
- context.push(element)
- parts.append(parsed_template.render(context))
- context.pop()
-
- return unicode(''.join(parts))
-
- return get_section
-
- def _parse(self, template, delimiters=None):
- """
- Parse the given template, and return a ParsedTemplate instance.
-
- Arguments:
+ # Otherwise, treat the value as a list.
- template: a template string of type unicode.
+ return data
+ def _render_value(self, val, context, delimiters=None):
"""
- parser = Parser(self, delimiters=delimiters)
- parser.compile_template_re()
+ Render an arbitrary value.
- return parser.parse(template=template)
-
- def _render(self, template, context):
"""
- Returns: a string of type unicode.
-
- Arguments:
-
- template: a template string of type unicode.
- context: a ContextStack instance.
-
- """
- # We keep this type-check as an added check because this method is
- # called with template strings coming from potentially externally-
- # supplied functions like self.literal, self.load_partial, etc.
- # Beyond this point, we have much better control over the type.
- if type(template) is not unicode:
- raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template)))
-
- parsed_template = self._parse(template)
-
- return parsed_template.render(context)
-
- def render(self, template, context):
+ if not is_string(val):
+ # In case the template is an integer, for example.
+ val = self.to_str(val)
+ if type(val) is not unicode:
+ val = self.literal(val)
+ return self.render(val, context, delimiters)
+
+ def render(self, template, context_stack, delimiters=None):
"""
- Return a template rendered as a string with type unicode.
+ Render a unicode template string, and return as unicode.
Arguments:
template: a template string of type unicode (but not a proper
subclass of unicode).
- context: a ContextStack instance.
+ context_stack: a ContextStack instance.
"""
- # Be strict but not too strict. In other words, accept str instead
- # of unicode, but don't assume anything about the encoding (e.g.
- # don't use self.literal).
- template = unicode(template)
+ parsed_template = parse(template, delimiters)
- return self._render(template, context)
+ return parsed_template.render(self, context_stack)
diff --git a/pystache/renderer.py b/pystache/renderer.py
index a3d4c57..ff6a90c 100644
--- a/pystache/renderer.py
+++ b/pystache/renderer.py
@@ -8,36 +8,26 @@ This module provides a Renderer class to render templates.
import sys
from pystache import defaults
-from pystache.common import TemplateNotFoundError
-from pystache.context import ContextStack
+from pystache.common import TemplateNotFoundError, MissingTags, is_string
+from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader
-from pystache.renderengine import RenderEngine
+from pystache.parsed import ParsedTemplate
+from pystache.renderengine import context_get, RenderEngine
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):
"""
A class for rendering mustache templates.
This class supports several rendering options which are described in
- the constructor's docstring. Among these, the constructor supports
- passing a custom partial loader.
+ the constructor's docstring. Other behavior can be customized by
+ subclassing this class.
- Here is an example of rendering a template using a custom partial loader
- that loads partials from a string-string dictionary.
+ For example, one can pass a string-string dictionary to the constructor
+ to bypass loading partials from the file system:
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials)
@@ -45,16 +35,49 @@ class Renderer(object):
>>> print renderer.render('{{>partial}}', {'thing': 'world'})
Hello, world!
+ To customize string coercion (e.g. to render False values as ''), one can
+ subclass this class. For example:
+
+ class MyRenderer(Renderer):
+ def str_coerce(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
"""
def __init__(self, file_encoding=None, string_encoding=None,
decode_errors=None, search_dirs=None, file_extension=None,
- escape=None, partials=None):
+ escape=None, partials=None, missing_tags=None):
"""
Construct an instance.
Arguments:
+ file_encoding: the name of the encoding to use by default when
+ reading template files. All templates are converted to unicode
+ prior to parsing. Defaults to the package default.
+
+ string_encoding: the name of the encoding to use when converting
+ to unicode any byte strings (type str in Python 2) encountered
+ during the rendering process. This name will be passed as the
+ encoding argument to the built-in function unicode().
+ Defaults to the package default.
+
+ decode_errors: the string to pass as the errors argument to the
+ built-in function unicode() when converting byte strings to
+ unicode. Defaults to the package default.
+
+ search_dirs: the list of directories in which to search when
+ loading a template by name or file name. If given a string,
+ the method interprets the string as a single directory.
+ Defaults to the package default.
+
+ file_extension: the template file extension. Pass False for no
+ extension (i.e. to use extensionless template files).
+ Defaults to the package default.
+
partials: an object (e.g. a dictionary) for custom partial loading
during the rendering process.
The object should have a get() method that accepts a string
@@ -67,10 +90,6 @@ class Renderer(object):
the file system -- using relevant instance attributes like
search_dirs, file_encoding, etc.
- decode_errors: the string to pass as the errors argument to the
- built-in function unicode() when converting str strings to
- unicode. Defaults to the package default.
-
escape: the function used to escape variable tag values when
rendering a template. The function should accept a unicode
string (or subclass of unicode) and return an escaped string
@@ -84,24 +103,9 @@ class Renderer(object):
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
- to parsing. This encoding is used when reading template files
- and converting them to unicode. Defaults to the package default.
-
- file_extension: the template file extension. Pass False for no
- extension (i.e. to use extensionless template files).
- Defaults to the package default.
-
- search_dirs: the list of directories in which to search when
- loading a template by name or file name. If given a string,
- the method interprets the string as a single directory.
- Defaults to the package default.
-
- string_encoding: the name of the encoding to use when converting
- to unicode any strings of type str encountered during the
- rendering process. The name will be passed as the encoding
- argument to the built-in function unicode(). Defaults to the
+ missing_tags: a string specifying how to handle missing tags.
+ If 'strict', an error is raised on a missing tag. If 'ignore',
+ the value of the tag is the empty string. Defaults to the
package default.
"""
@@ -117,6 +121,9 @@ class Renderer(object):
if file_extension is None:
file_extension = defaults.TEMPLATE_EXTENSION
+ if missing_tags is None:
+ missing_tags = defaults.MISSING_TAGS
+
if search_dirs is None:
search_dirs = defaults.SEARCH_DIRS
@@ -131,6 +138,7 @@ class Renderer(object):
self.escape = escape
self.file_encoding = file_encoding
self.file_extension = file_extension
+ self.missing_tags = missing_tags
self.partials = partials
self.search_dirs = search_dirs
self.string_encoding = string_encoding
@@ -148,6 +156,20 @@ class Renderer(object):
"""
return self._context
+ # We could not choose str() as the name because 2to3 renames the unicode()
+ # method of this class to str().
+ def str_coerce(self, val):
+ """
+ Coerce a non-string value to a string.
+
+ This method is called whenever a non-string is encountered during the
+ rendering process when a string is needed (e.g. if a context value
+ for string interpolation is not a string). To customize string
+ coercion, you can override this method.
+
+ """
+ return str(val)
+
def _to_unicode_soft(self, s):
"""
Convert a basestring to unicode, preserving any unicode subclass.
@@ -224,21 +246,21 @@ class Renderer(object):
def _make_load_partial(self):
"""
- Return the load_partial function to pass to RenderEngine.__init__().
+ Return a function that loads a partial by name.
"""
if self.partials is None:
- load_template = self._make_load_template()
- return load_template
+ return self._make_load_template()
- # Otherwise, create a load_partial function from the custom partial
- # loader that satisfies RenderEngine requirements (and that provides
- # a nicer exception, etc).
+ # Otherwise, create a function from the custom partial loader.
partials = self.partials
def load_partial(name):
+ # TODO: consider using EAFP here instead.
+ # http://docs.python.org/glossary.html#term-eafp
+ # This would mean requiring that the custom partial loader
+ # raise a KeyError on name not found.
template = partials.get(name)
-
if template is None:
raise TemplateNotFoundError("Name %s not found in partials: %s" %
(repr(name), type(partials)))
@@ -248,42 +270,79 @@ class Renderer(object):
return load_partial
- def _make_render_engine(self):
+ def _is_missing_tags_strict(self):
"""
- Return a RenderEngine instance for rendering.
+ Return whether missing_tags is set to strict.
"""
- load_partial = self._make_load_partial()
+ val = self.missing_tags
- engine = RenderEngine(load_partial=load_partial,
- literal=self._to_unicode_hard,
- escape=self._escape_to_unicode)
- return engine
+ if val == MissingTags.strict:
+ return True
+ elif val == MissingTags.ignore:
+ return False
- # TODO: add unit tests for this method.
- def load_template(self, template_name):
+ raise Exception("Unsupported 'missing_tags' value: %s" % repr(val))
+
+ def _make_resolve_partial(self):
"""
- Load a template by name from the file system.
+ Return the resolve_partial function to pass to RenderEngine.__init__().
"""
- load_template = self._make_load_template()
- return load_template(template_name)
+ load_partial = self._make_load_partial()
- def _render_string(self, template, *context, **kwargs):
+ if self._is_missing_tags_strict():
+ return load_partial
+ # Otherwise, ignore missing tags.
+
+ def resolve_partial(name):
+ try:
+ return load_partial(name)
+ except TemplateNotFoundError:
+ return u''
+
+ return resolve_partial
+
+ def _make_resolve_context(self):
"""
- Render the given template string using the given context.
+ Return the resolve_context function to pass to RenderEngine.__init__().
"""
- # RenderEngine.render() requires that the template string be unicode.
- template = self._to_unicode_hard(template)
+ if self._is_missing_tags_strict():
+ return context_get
+ # Otherwise, ignore missing tags.
- context = ContextStack.create(*context, **kwargs)
- self._context = context
+ def resolve_context(stack, name):
+ try:
+ return context_get(stack, name)
+ except KeyNotFoundError:
+ return u''
- engine = self._make_render_engine()
- rendered = engine.render(template, context)
+ return resolve_context
- return unicode(rendered)
+ def _make_render_engine(self):
+ """
+ Return a RenderEngine instance for rendering.
+
+ """
+ resolve_context = self._make_resolve_context()
+ resolve_partial = self._make_resolve_partial()
+
+ engine = RenderEngine(literal=self._to_unicode_hard,
+ escape=self._escape_to_unicode,
+ resolve_context=resolve_context,
+ resolve_partial=resolve_partial,
+ to_str=self.str_coerce)
+ return engine
+
+ # TODO: add unit tests for this method.
+ def load_template(self, template_name):
+ """
+ Load a template by name from the file system.
+
+ """
+ load_template = self._make_load_template()
+ return load_template(template_name)
def _render_object(self, obj, *context, **kwargs):
"""
@@ -307,6 +366,17 @@ class Renderer(object):
return self._render_string(template, *context, **kwargs)
+ def render_name(self, template_name, *context, **kwargs):
+ """
+ Render the template with the given name using the given context.
+
+ See the render() docstring for more information.
+
+ """
+ loader = self._make_loader()
+ template = loader.load_name(template_name)
+ return self._render_string(template, *context, **kwargs)
+
def render_path(self, template_path, *context, **kwargs):
"""
Render the template at the given path using the given context.
@@ -319,24 +389,54 @@ class Renderer(object):
return self._render_string(template, *context, **kwargs)
+ def _render_string(self, template, *context, **kwargs):
+ """
+ Render the given template string using the given context.
+
+ """
+ # RenderEngine.render() requires that the template string be unicode.
+ template = self._to_unicode_hard(template)
+
+ render_func = lambda engine, stack: engine.render(template, stack)
+
+ return self._render_final(render_func, *context, **kwargs)
+
+ # All calls to render() should end here because it prepares the
+ # context stack correctly.
+ def _render_final(self, render_func, *context, **kwargs):
+ """
+ Arguments:
+
+ render_func: a function that accepts a RenderEngine and ContextStack
+ instance and returns a template rendering as a unicode string.
+
+ """
+ stack = ContextStack.create(*context, **kwargs)
+ self._context = stack
+
+ engine = self._make_render_engine()
+
+ return render_func(engine, stack)
+
def render(self, template, *context, **kwargs):
"""
- Render the given template (or template object) using the given context.
+ Render the given template string, view template, or parsed template.
- Returns the rendering as a unicode string.
+ Returns a unicode string.
- Prior to rendering, templates of type str are converted to unicode
- using the string_encoding and decode_errors attributes. See the
- constructor docstring for more information.
+ Prior to rendering, this method will convert a template that is a
+ byte string (type str in Python 2) to unicode using the string_encoding
+ and decode_errors attributes. See the constructor docstring for
+ more information.
Arguments:
- template: a template string of type unicode or str, or an object
- instance. If the argument is an object, the function first looks
- for the template associated to the object by calling this class's
- get_associated_template() method. The rendering process also
- uses the passed object as the first element of the context stack
- when rendering.
+ template: a template string that is unicode or a byte string,
+ a ParsedTemplate instance, or another object instance. In the
+ final case, the function first looks for the template associated
+ to the object by calling this class's get_associated_template()
+ method. The rendering process also uses the passed object as
+ the first element of the context stack when rendering.
*context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
@@ -350,8 +450,11 @@ class Renderer(object):
all items in the *context list.
"""
- if isinstance(template, _STRING_TYPES):
+ if is_string(template):
return self._render_string(template, *context, **kwargs)
+ if isinstance(template, ParsedTemplate):
+ render_func = lambda engine, stack: template.render(engine, stack)
+ return self._render_final(render_func, *context, **kwargs)
# Otherwise, we assume the template is an object.
return self._render_object(template, *context, **kwargs)
diff --git a/pystache/specloader.py b/pystache/specloader.py
index 3cb0f1a..3a77d4c 100644
--- a/pystache/specloader.py
+++ b/pystache/specloader.py
@@ -55,6 +55,9 @@ class SpecLoader(object):
Find and return the path to the template associated to the instance.
"""
+ if spec.template_path is not None:
+ return spec.template_path
+
dir_path, file_name = self._find_relative(spec)
locator = self.loader._make_locator()
diff --git a/pystache/template_spec.py b/pystache/template_spec.py
index 76ce784..9e9f454 100644
--- a/pystache/template_spec.py
+++ b/pystache/template_spec.py
@@ -4,12 +4,11 @@
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".
+from a class that subclasses TemplateSpec. The "spec" in TemplateSpec
+stands for "special" or "specified" template information.
"""
-# TODO: finish the class docstring.
class TemplateSpec(object):
"""
@@ -28,20 +27,27 @@ class TemplateSpec(object):
template: the template as a string.
- template_rel_path: the path to the template file, relative to the
- directory containing the module defining the class.
-
- template_rel_directory: the directory containing the template file, relative
- to the directory containing the module defining the class.
+ template_encoding: the encoding used by the template.
template_extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. extensionless template files).
+ template_name: the name of the template.
+
+ template_path: absolute path to the template.
+
+ template_rel_directory: the directory containing the template file,
+ relative to the directory containing the module defining the class.
+
+ template_rel_path: the path to the template file, relative to the
+ directory containing the module defining the class.
+
"""
template = None
- template_rel_path = None
- template_rel_directory = None
- template_name = None
- template_extension = None
template_encoding = None
+ template_extension = None
+ template_name = None
+ template_path = None
+ template_rel_directory = None
+ template_rel_path = None
diff --git a/pystache/tests/common.py b/pystache/tests/common.py
index 24b24dc..99be4c8 100644
--- a/pystache/tests/common.py
+++ b/pystache/tests/common.py
@@ -22,7 +22,7 @@ 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']
+TEXT_DOCTEST_PATHS = ['README.md']
UNITTEST_FILE_PREFIX = "test_"
@@ -43,7 +43,10 @@ def html_escape(u):
return u.replace("'", '&#x27;')
-def get_data_path(file_name):
+def get_data_path(file_name=None):
+ """Return the path to a file in the test data directory."""
+ if file_name is None:
+ file_name = ""
return os.path.join(DATA_DIR, file_name)
@@ -139,8 +142,7 @@ class AssertStringMixin:
format = "%s"
# Show both friendly and literal versions.
- details = """String mismatch: %%s\
-
+ details = """String mismatch: %%s
Expected: \"""%s\"""
Actual: \"""%s\"""
diff --git a/pystache/tests/data/locator/template.txt b/pystache/tests/data/locator/template.txt
new file mode 100644
index 0000000..bef8160
--- /dev/null
+++ b/pystache/tests/data/locator/template.txt
@@ -0,0 +1 @@
+Test template file
diff --git a/pystache/tests/doctesting.py b/pystache/tests/doctesting.py
index 469c81e..1102b78 100644
--- a/pystache/tests/doctesting.py
+++ b/pystache/tests/doctesting.py
@@ -44,7 +44,11 @@ def get_doctests(text_file_dir):
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)
+ # Skip the README doctests in Python 3 for now because examples
+ # rendering to unicode do not give consistent results
+ # (e.g. 'foo' vs u'foo').
+ # paths = _convert_paths(paths)
+ paths = []
suites = []
diff --git a/pystache/tests/main.py b/pystache/tests/main.py
index de56c44..184122d 100644
--- a/pystache/tests/main.py
+++ b/pystache/tests/main.py
@@ -1,7 +1,7 @@
# coding: utf-8
"""
-Exposes a run_tests() function that runs all tests in the project.
+Exposes a main() function that runs all tests in the project.
This module is for our test console script.
@@ -10,7 +10,7 @@ This module is for our test console script.
import os
import sys
import unittest
-from unittest import TestProgram
+from unittest import TestCase, TestProgram
import pystache
from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, SPEC_TEST_DIR, UNITTEST_FILE_PREFIX
@@ -24,6 +24,58 @@ from pystache.tests.spectesting import get_spec_tests
FROM_SOURCE_OPTION = "--from-source"
+def make_extra_tests(text_doctest_dir, spec_test_dir):
+ tests = []
+
+ if text_doctest_dir is not None:
+ doctest_suites = get_doctests(text_doctest_dir)
+ tests.extend(doctest_suites)
+
+ if spec_test_dir is not None:
+ spec_testcases = get_spec_tests(spec_test_dir)
+ tests.extend(spec_testcases)
+
+ return unittest.TestSuite(tests)
+
+
+def make_test_program_class(extra_tests):
+ """
+ Return a subclass of unittest.TestProgram.
+
+ """
+ # The function unittest.main() is an alias for unittest.TestProgram's
+ # constructor. TestProgram's constructor does the following:
+ #
+ # 1. calls self.parseArgs(argv),
+ # 2. which in turn calls self.createTests().
+ # 3. then the constructor calls self.runTests().
+ #
+ # The createTests() method sets the self.test attribute by calling one
+ # of self.testLoader's "loadTests" methods. 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 createTests(self):
+ """
+ Load tests and set self.test to a unittest.TestSuite instance
+
+ Compare--
+
+ http://docs.python.org/library/unittest.html#unittest.TestSuite
+
+ """
+ super(PystacheTestProgram, self).createTests()
+ self.test.addTests(extra_tests)
+
+ return PystacheTestProgram
+
+
# Do not include "test" in this function's name to avoid it getting
# picked up by nosetests.
def main(sys_argv):
@@ -52,7 +104,14 @@ def main(sys_argv):
sys_argv.pop(1)
except IndexError:
if should_source_exist:
- spec_test_dir = SPEC_TEST_DIR
+ if not os.path.exists(SPEC_TEST_DIR):
+ # Then the user is probably using a downloaded sdist rather
+ # than a repository clone (since the sdist does not include
+ # the spec test directory).
+ print("pystache: skipping spec tests: spec test directory "
+ "not found")
+ else:
+ spec_test_dir = SPEC_TEST_DIR
try:
# TODO: use optparse command options instead.
@@ -71,16 +130,17 @@ def main(sys_argv):
# 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
+ extra_tests = make_extra_tests(project_dir, spec_test_dir)
+ test_program_class = make_test_program_class(extra_tests)
+
# 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)
+ test_program_class(argv=sys_argv, module=None)
# No need to return since unitttest.main() exits.
@@ -103,7 +163,7 @@ def _discover_test_modules(package_dir):
return names
-class SetupTests(unittest.TestCase):
+class SetupTests(TestCase):
"""Tests about setup.py."""
@@ -123,33 +183,3 @@ class SetupTests(unittest.TestCase):
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/test___init__.py b/pystache/tests/test___init__.py
index d4f3526..eae42c1 100644
--- a/pystache/tests/test___init__.py
+++ b/pystache/tests/test___init__.py
@@ -23,7 +23,7 @@ class InitTests(unittest.TestCase):
"""
actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL)
- expected = set(['render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
+ expected = set(['parse', 'render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
self.assertEqual(actual, expected)
diff --git a/pystache/tests/test_context.py b/pystache/tests/test_context.py
index d432428..238e4b0 100644
--- a/pystache/tests/test_context.py
+++ b/pystache/tests/test_context.py
@@ -8,10 +8,8 @@ Unit tests of context.py.
from datetime import datetime
import unittest
-from pystache.context import _NOT_FOUND
-from pystache.context import _get_value
-from pystache.context import ContextStack
-from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable
+from pystache.context import _NOT_FOUND, _get_value, KeyNotFoundError, ContextStack
+from pystache.tests.common import AssertIsMixin, AssertStringMixin, AssertExceptionMixin, Attachable
class SimpleObject(object):
@@ -39,7 +37,7 @@ class DictLike(object):
return self._dict[key]
-class GetValueTests(unittest.TestCase, AssertIsMixin):
+class GetValueTestCase(unittest.TestCase, AssertIsMixin):
"""Test context._get_value()."""
@@ -147,6 +145,26 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertEqual(item["foo"], "bar")
self.assertNotFound(item, "foo")
+ def test_object__property__raising_exception(self):
+ """
+ Test getting a property that raises an exception.
+
+ """
+ class Foo(object):
+
+ @property
+ def bar(self):
+ return 1
+
+ @property
+ def baz(self):
+ raise ValueError("test")
+
+ foo = Foo()
+ self.assertEqual(_get_value(foo, 'bar'), 1)
+ self.assertNotFound(foo, 'missing')
+ self.assertRaises(ValueError, _get_value, foo, 'baz')
+
### Case: the item is an instance of a built-in type.
def test_built_in_type__integer(self):
@@ -204,7 +222,8 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertNotFound(item2, 'pop')
-class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+class ContextStackTestCase(unittest.TestCase, AssertIsMixin, AssertStringMixin,
+ AssertExceptionMixin):
"""
Test the ContextStack class.
@@ -306,6 +325,24 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
context = ContextStack.create({'foo': 'bar'}, foo='buzz')
self.assertEqual(context.get('foo'), 'buzz')
+ ## Test the get() method.
+
+ def test_get__single_dot(self):
+ """
+ Test getting a single dot (".").
+
+ """
+ context = ContextStack("a", "b")
+ self.assertEqual(context.get("."), "b")
+
+ def test_get__single_dot__missing(self):
+ """
+ Test getting a single dot (".") with an empty context stack.
+
+ """
+ context = ContextStack()
+ self.assertException(KeyNotFoundError, "Key '.' not found: empty context stack", context.get, ".")
+
def test_get__key_present(self):
"""
Test getting a key.
@@ -320,15 +357,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
context = ContextStack()
- self.assertString(context.get("foo"), u'')
-
- def test_get__default(self):
- """
- Test that get() respects the default value.
-
- """
- context = ContextStack()
- self.assertEqual(context.get("foo", "bar"), "bar")
+ self.assertException(KeyNotFoundError, "Key 'foo' not found: first part", context.get, "foo")
def test_get__precedence(self):
"""
@@ -424,10 +453,10 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
def test_dot_notation__missing_attr_or_key(self):
name = "foo.bar.baz.bak"
stack = ContextStack({"foo": {"bar": {}}})
- self.assertString(stack.get(name), u'')
+ self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name)
stack = ContextStack({"foo": Attachable(bar=Attachable())})
- self.assertString(stack.get(name), u'')
+ self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name)
def test_dot_notation__missing_part_terminates_search(self):
"""
@@ -451,7 +480,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'})
self.assertEqual(stack.get('a'), 'A')
- self.assertString(stack.get('a.b'), u'')
+ self.assertException(KeyNotFoundError, "Key 'a.b' not found: missing 'b'", stack.get, "a.b")
stack.pop()
self.assertEqual(stack.get('a.b'), 'A.B')
diff --git a/pystache/tests/test_defaults.py b/pystache/tests/test_defaults.py
new file mode 100644
index 0000000..c78ea7c
--- /dev/null
+++ b/pystache/tests/test_defaults.py
@@ -0,0 +1,68 @@
+# coding: utf-8
+
+"""
+Unit tests for defaults.py.
+
+"""
+
+import unittest
+
+import pystache
+
+from pystache.tests.common import AssertStringMixin
+
+
+# TODO: make sure each default has at least one test.
+class DefaultsConfigurableTestCase(unittest.TestCase, AssertStringMixin):
+
+ """Tests that the user can change the defaults at runtime."""
+
+ # TODO: switch to using a context manager after 2.4 is deprecated.
+ def setUp(self):
+ """Save the defaults."""
+ defaults = [
+ 'DECODE_ERRORS', 'DELIMITERS',
+ 'FILE_ENCODING', 'MISSING_TAGS',
+ 'SEARCH_DIRS', 'STRING_ENCODING',
+ 'TAG_ESCAPE', 'TEMPLATE_EXTENSION'
+ ]
+ self.saved = {}
+ for e in defaults:
+ self.saved[e] = getattr(pystache.defaults, e)
+
+ def tearDown(self):
+ for key, value in self.saved.items():
+ setattr(pystache.defaults, key, value)
+
+ def test_tag_escape(self):
+ """Test that changes to defaults.TAG_ESCAPE take effect."""
+ template = u"{{foo}}"
+ context = {'foo': '<'}
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"&lt;")
+
+ pystache.defaults.TAG_ESCAPE = lambda u: u
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"<")
+
+ def test_delimiters(self):
+ """Test that changes to defaults.DELIMITERS take effect."""
+ template = u"[[foo]]{{foo}}"
+ context = {'foo': 'FOO'}
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"[[foo]]FOO")
+
+ pystache.defaults.DELIMITERS = ('[[', ']]')
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"FOO{{foo}}")
+
+ def test_missing_tags(self):
+ """Test that changes to defaults.MISSING_TAGS take effect."""
+ template = u"{{foo}}"
+ context = {}
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"")
+
+ pystache.defaults.MISSING_TAGS = 'strict'
+ self.assertRaises(pystache.context.KeyNotFoundError,
+ pystache.render, template, context)
diff --git a/pystache/tests/test_loader.py b/pystache/tests/test_loader.py
index c47239c..f2c2187 100644
--- a/pystache/tests/test_loader.py
+++ b/pystache/tests/test_loader.py
@@ -14,6 +14,10 @@ from pystache import defaults
from pystache.loader import Loader
+# We use the same directory as the locator tests for now.
+LOADER_DATA_DIR = os.path.join(DATA_DIR, 'locator')
+
+
class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
def setUp(self):
@@ -178,7 +182,7 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
actual = loader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é')
- def test_loader__to_unicode__attribute(self):
+ def test_read__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
@@ -192,3 +196,14 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
#actual = loader.read(path)
#self.assertString(actual, u'non-ascii: ')
+ def test_load_file(self):
+ loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR])
+ template = loader.load_file('template.txt')
+ self.assertEqual(template, 'Test template file\n')
+
+ def test_load_name(self):
+ loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR],
+ extension='txt')
+ template = loader.load_name('template')
+ self.assertEqual(template, 'Test template file\n')
+
diff --git a/pystache/tests/test_locator.py b/pystache/tests/test_locator.py
index f17a289..ee1c2ff 100644
--- a/pystache/tests/test_locator.py
+++ b/pystache/tests/test_locator.py
@@ -19,6 +19,9 @@ from pystache.tests.common import DATA_DIR, EXAMPLES_DIR, AssertExceptionMixin
from pystache.tests.data.views import SayHello
+LOCATOR_DATA_DIR = os.path.join(DATA_DIR, 'locator')
+
+
class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def _locator(self):
@@ -53,7 +56,17 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def test_get_object_directory__not_hasattr_module(self):
locator = Locator()
- obj = datetime(2000, 1, 1)
+ # Previously, we used a genuine object -- a datetime instance --
+ # because datetime instances did not have the __module__ attribute
+ # in CPython. See, for example--
+ #
+ # http://bugs.python.org/issue15223
+ #
+ # However, since datetime instances do have the __module__ attribute
+ # in PyPy, we needed to switch to something else once we added
+ # support for PyPi. This was so that our test runs would pass
+ # in all systems.
+ obj = "abc"
self.assertFalse(hasattr(obj, '__module__'))
self.assertEqual(locator.get_object_directory(obj), None)
@@ -77,6 +90,13 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
+ def test_find_file(self):
+ locator = Locator()
+ path = locator.find_file('template.txt', [LOCATOR_DATA_DIR])
+
+ expected_path = os.path.join(LOCATOR_DATA_DIR, 'template.txt')
+ self.assertEqual(path, expected_path)
+
def test_find_name(self):
locator = Locator()
path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
@@ -97,7 +117,7 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
locator = Locator()
dir1 = DATA_DIR
- dir2 = os.path.join(DATA_DIR, 'locator')
+ dir2 = LOCATOR_DATA_DIR
self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate'))
self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate'))
diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py
index 4aa0959..92248ea 100644
--- a/pystache/tests/test_parser.py
+++ b/pystache/tests/test_parser.py
@@ -7,6 +7,7 @@ Unit tests of parser.py.
import unittest
+from pystache.defaults import DELIMITERS
from pystache.parser import _compile_template_re as make_re
@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
Test getting a key from a dictionary.
"""
- re = make_re()
+ re = make_re(DELIMITERS)
match = re.search("b {{test}}")
self.assertEqual(match.start(), 1)
diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py
index b13e246..db916f7 100644
--- a/pystache/tests/test_renderengine.py
+++ b/pystache/tests/test_renderengine.py
@@ -5,13 +5,23 @@ Unit tests of renderengine.py.
"""
+import sys
import unittest
-from pystache.context import ContextStack
+from pystache.context import ContextStack, KeyNotFoundError
from pystache import defaults
from pystache.parser import ParsingError
-from pystache.renderengine import RenderEngine
-from pystache.tests.common import AssertStringMixin, Attachable
+from pystache.renderer import Renderer
+from pystache.renderengine import context_get, RenderEngine
+from pystache.tests.common import AssertStringMixin, AssertExceptionMixin, Attachable
+
+
+def _get_unicode_char():
+ if sys.version_info < (3, ):
+ return 'u'
+ return ''
+
+_UNICODE_CHAR = _get_unicode_char()
def mock_literal(s):
@@ -45,14 +55,16 @@ class RenderEngineTestCase(unittest.TestCase):
"""
# In real-life, these arguments would be functions
- engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
+ engine = RenderEngine(resolve_partial="foo", literal="literal",
+ escape="escape", to_str="str")
self.assertEqual(engine.escape, "escape")
self.assertEqual(engine.literal, "literal")
- self.assertEqual(engine.load_partial, "foo")
+ self.assertEqual(engine.resolve_partial, "foo")
+ self.assertEqual(engine.to_str, "str")
-class RenderTests(unittest.TestCase, AssertStringMixin):
+class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
"""
Tests RenderEngine.render().
@@ -68,8 +80,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Create and return a default RenderEngine for testing.
"""
- escape = defaults.TAG_ESCAPE
- engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
+ renderer = Renderer(string_encoding='utf-8', missing_tags='strict')
+ engine = renderer._make_render_engine()
+
return engine
def _assert_render(self, expected, template, *context, **kwargs):
@@ -81,25 +94,26 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
engine = kwargs.get('engine', self._engine())
if partials is not None:
- engine.load_partial = lambda key: unicode(partials[key])
+ engine.resolve_partial = lambda key: unicode(partials[key])
context = ContextStack(*context)
- actual = engine.render(template, context)
+ # RenderEngine.render() only accepts unicode template strings.
+ actual = engine.render(unicode(template), context)
self.assertString(actual=actual, expected=expected)
def test_render(self):
self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'})
- def test__load_partial(self):
+ def test__resolve_partial(self):
"""
Test that render() uses the load_template attribute.
"""
engine = self._engine()
partials = {'partial': u"{{person}}"}
- engine.load_partial = lambda key: partials[key]
+ engine.resolve_partial = lambda key: partials[key]
self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
@@ -170,6 +184,47 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
self._assert_render(u'**bar bar**', template, context, engine=engine)
+ # Custom to_str for testing purposes.
+ def _to_str(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
+ def test_to_str(self):
+ """Test the to_str attribute."""
+ engine = self._engine()
+ template = '{{value}}'
+ context = {'value': None}
+
+ self._assert_render(u'None', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__lambda(self):
+ """Test the to_str attribute for a lambda."""
+ engine = self._engine()
+ template = '{{value}}'
+ context = {'value': lambda: None}
+
+ self._assert_render(u'None', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__section_list(self):
+ """Test the to_str attribute for a section list."""
+ engine = self._engine()
+ template = '{{#list}}{{.}}{{/list}}'
+ context = {'list': [None, None]}
+
+ self._assert_render(u'NoneNone', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__section_lambda(self):
+ # TODO: add a test for a "method with an arity of 1".
+ pass
+
def test__non_basestring__literal_and_escaped(self):
"""
Test a context value that is not a basestring instance.
@@ -285,6 +340,16 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'section': item, attr_name: 7}
self._assert_render(u'7', template, context)
+ # This test is also important for testing 2to3.
+ def test_interpolation__nonascii_nonunicode(self):
+ """
+ Test a tag whose value is a non-ascii, non-unicode string.
+
+ """
+ template = '{{nonascii}}'
+ context = {'nonascii': u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
def test_implicit_iterator__literal(self):
"""
Test an implicit iterator in a literal tag.
@@ -343,6 +408,28 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
+ ## Test cases related specifically to lambdas.
+
+ # This test is also important for testing 2to3.
+ def test_section__nonascii_nonunicode(self):
+ """
+ Test a section whose value is a non-ascii, non-unicode string.
+
+ """
+ template = '{{#nonascii}}{{.}}{{/nonascii}}'
+ context = {'nonascii': u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
+ # This test is also important for testing 2to3.
+ def test_lambda__returning_nonascii_nonunicode(self):
+ """
+ Test a lambda tag value returning a non-ascii, non-unicode string.
+
+ """
+ template = '{{lambda}}'
+ context = {'lambda': lambda: u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
## Test cases related specifically to sections.
def test_section__end_tag_with_no_start_tag(self):
@@ -461,6 +548,25 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': (lambda text: 'Hi %s' % text)}
self._assert_render(u'Hi Mom', template, context)
+ # This test is also important for testing 2to3.
+ def test_section__lambda__returning_nonascii_nonunicode(self):
+ """
+ Test a lambda section value returning a non-ascii, non-unicode string.
+
+ """
+ template = '{{#lambda}}{{/lambda}}'
+ context = {'lambda': lambda text: u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
+ def test_section__lambda__returning_nonstring(self):
+ """
+ Test a lambda section value returning a non-string.
+
+ """
+ template = '{{#lambda}}foo{{/lambda}}'
+ context = {'lambda': lambda text: len(text)}
+ self._assert_render(u'3', template, context)
+
def test_section__iterable(self):
"""
Check that objects supporting iteration (aside from dicts) behave like lists.
@@ -609,33 +715,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'person': person}
self._assert_render(u'Hello, Biggles. I see you are 42.', template, context)
- def test_dot_notation__missing_attributes_or_keys(self):
- """
- Test dot notation with missing keys or attributes.
-
- Check that if a key or attribute in a dotted name does not exist, then
- the tag renders as the empty string.
-
- """
- template = """I cannot see {{person.name}}'s age: {{person.age}}.
- Nor {{other_person.name}}'s: ."""
- expected = u"""I cannot see Biggles's age: .
- Nor Mr. Bradshaw's: ."""
- context = {'person': {'name': 'Biggles'},
- 'other_person': Attachable(name='Mr. Bradshaw')}
- self._assert_render(expected, template, context)
-
def test_dot_notation__multiple_levels(self):
"""
Test dot notation with multiple levels.
"""
template = """Hello, Mr. {{person.name.lastname}}.
- I see you're back from {{person.travels.last.country.city}}.
- I'm missing some of your details: {{person.details.private.editor}}."""
+ I see you're back from {{person.travels.last.country.city}}."""
expected = u"""Hello, Mr. Pither.
- I see you're back from Cornwall.
- I'm missing some of your details: ."""
+ I see you're back from Cornwall."""
context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'},
'travels': {'last': {'country': {'city': 'Cornwall'}}},
'details': {'public': 'likes cycling'}}}
@@ -667,6 +755,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
https://github.com/mustache/spec/pull/48
"""
- template = '{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})'
context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} }
- self._assert_render(u'A.B :: (A :: )', template, context)
+
+ template = '{{a.b}}'
+ self._assert_render(u'A.B', template, context)
+
+ template = '{{#c}}{{a}}{{/c}}'
+ self._assert_render(u'A', template, context)
+
+ template = '{{#c}}{{a.b}}{{/c}}'
+ self.assertException(KeyNotFoundError, "Key %(unicode)s'a.b' not found: missing %(unicode)s'b'" %
+ {'unicode': _UNICODE_CHAR},
+ self._assert_render, 'A.B :: (A :: )', template, context)
diff --git a/pystache/tests/test_renderer.py b/pystache/tests/test_renderer.py
index f04c799..0dbe0d9 100644
--- a/pystache/tests/test_renderer.py
+++ b/pystache/tests/test_renderer.py
@@ -14,6 +14,7 @@ from examples.simple import Simple
from pystache import Renderer
from pystache import TemplateSpec
from pystache.common import TemplateNotFoundError
+from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader
from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin
@@ -124,6 +125,22 @@ class RendererInitTestCase(unittest.TestCase):
renderer = Renderer(file_extension='foo')
self.assertEqual(renderer.file_extension, 'foo')
+ def test_missing_tags(self):
+ """
+ Check that the missing_tags attribute is set correctly.
+
+ """
+ renderer = Renderer(missing_tags='foo')
+ self.assertEqual(renderer.missing_tags, 'foo')
+
+ def test_missing_tags__default(self):
+ """
+ Check the missing_tags default.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.missing_tags, 'ignore')
+
def test_search_dirs__default(self):
"""
Check the search_dirs default.
@@ -319,37 +336,44 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer.string_encoding = 'utf_8'
self.assertEqual(renderer.render(template), u"déf")
- def test_make_load_partial(self):
+ def test_make_resolve_partial(self):
"""
- Test the _make_load_partial() method.
+ Test the _make_resolve_partial() method.
"""
renderer = Renderer()
renderer.partials = {'foo': 'bar'}
- load_partial = renderer._make_load_partial()
+ resolve_partial = renderer._make_resolve_partial()
- actual = load_partial('foo')
+ actual = resolve_partial('foo')
self.assertEqual(actual, 'bar')
self.assertEqual(type(actual), unicode, "RenderEngine requires that "
- "load_partial return unicode strings.")
+ "resolve_partial return unicode strings.")
- def test_make_load_partial__unicode(self):
+ def test_make_resolve_partial__unicode(self):
"""
- Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode.
+ Test _make_resolve_partial(): that resolve_partial doesn't "double-decode" Unicode.
"""
renderer = Renderer()
renderer.partials = {'partial': 'foo'}
- load_partial = renderer._make_load_partial()
- self.assertEqual(load_partial("partial"), "foo")
+ resolve_partial = renderer._make_resolve_partial()
+ self.assertEqual(resolve_partial("partial"), "foo")
# Now with a value that is already unicode.
renderer.partials = {'partial': u'foo'}
- load_partial = renderer._make_load_partial()
+ resolve_partial = renderer._make_resolve_partial()
# If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported
- self.assertEqual(load_partial("partial"), "foo")
+ self.assertEqual(resolve_partial("partial"), "foo")
+
+ def test_render_name(self):
+ """Test the render_name() method."""
+ data_dir = get_data_path()
+ renderer = Renderer(search_dirs=data_dir)
+ actual = renderer.render_name("say_hello", to='foo')
+ self.assertString(actual, u"Hello, foo")
def test_render_path(self):
"""
@@ -401,12 +425,45 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
actual = renderer.render(view)
self.assertEqual('Hi pizza!', actual)
+ def test_custom_string_coercion_via_assignment(self):
+ """
+ Test that string coercion can be customized via attribute assignment.
+
+ """
+ renderer = self._renderer()
+ def to_str(val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
+ self.assertEqual(renderer.render('{{value}}', value=None), 'None')
+ renderer.str_coerce = to_str
+ self.assertEqual(renderer.render('{{value}}', value=None), '')
+
+ def test_custom_string_coercion_via_subclassing(self):
+ """
+ Test that string coercion can be customized via subclassing.
+
+ """
+ class MyRenderer(Renderer):
+ def str_coerce(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+ renderer1 = Renderer()
+ renderer2 = MyRenderer()
+
+ self.assertEqual(renderer1.render('{{value}}', value=None), 'None')
+ self.assertEqual(renderer2.render('{{value}}', value=None), '')
+
# By testing that Renderer.render() constructs the right RenderEngine,
# we no longer need to exercise all rendering code paths through
# the Renderer. It suffices to test rendering paths through the
# RenderEngine for the same amount of code coverage.
-class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
+class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
"""
Check the RenderEngine returned by Renderer._make_render_engine().
@@ -420,11 +477,11 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
"""
return _make_renderer()
- ## Test the engine's load_partial attribute.
+ ## Test the engine's resolve_partial attribute.
- def test__load_partial__returns_unicode(self):
+ def test__resolve_partial__returns_unicode(self):
"""
- Check that load_partial returns unicode (and not a subclass).
+ Check that resolve_partial returns unicode (and not a subclass).
"""
class MyUnicode(unicode):
@@ -436,43 +493,70 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
engine = renderer._make_render_engine()
- actual = engine.load_partial('str')
+ actual = engine.resolve_partial('str')
self.assertEqual(actual, "foo")
self.assertEqual(type(actual), unicode)
# Check that unicode subclasses are not preserved.
- actual = engine.load_partial('subclass')
+ actual = engine.resolve_partial('subclass')
self.assertEqual(actual, "abc")
self.assertEqual(type(actual), unicode)
- def test__load_partial__not_found__default(self):
+ def test__resolve_partial__not_found(self):
+ """
+ Check that resolve_partial returns the empty string when a template is not found.
+
+ """
+ renderer = Renderer()
+
+ engine = renderer._make_render_engine()
+ resolve_partial = engine.resolve_partial
+
+ self.assertString(resolve_partial('foo'), u'')
+
+ def test__resolve_partial__not_found__missing_tags_strict(self):
"""
- Check that load_partial provides a nice message when a template is not found.
+ Check that resolve_partial provides a nice message when a template is not found.
"""
renderer = Renderer()
+ renderer.missing_tags = 'strict'
engine = renderer._make_render_engine()
- load_partial = engine.load_partial
+ resolve_partial = engine.resolve_partial
self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']",
- load_partial, "foo")
+ resolve_partial, "foo")
- def test__load_partial__not_found__dict(self):
+ def test__resolve_partial__not_found__partials_dict(self):
"""
- Check that load_partial provides a nice message when a template is not found.
+ Check that resolve_partial returns the empty string when a template is not found.
"""
renderer = Renderer()
renderer.partials = {}
engine = renderer._make_render_engine()
- load_partial = engine.load_partial
+ resolve_partial = engine.resolve_partial
+
+ self.assertString(resolve_partial('foo'), u'')
+
+ def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self):
+ """
+ Check that resolve_partial provides a nice message when a template is not found.
- # Include dict directly since str(dict) is different in Python 2 and 3:
- # <type 'dict'> versus <class 'dict'>, respectively.
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'strict'
+ renderer.partials = {}
+
+ engine = renderer._make_render_engine()
+ resolve_partial = engine.resolve_partial
+
+ # Include dict directly since str(dict) is different in Python 2 and 3:
+ # <type 'dict'> versus <class 'dict'>, respectively.
self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict,
- load_partial, "foo")
+ resolve_partial, "foo")
## Test the engine's literal attribute.
@@ -595,3 +679,47 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
self.assertTrue(isinstance(s, unicode))
self.assertEqual(type(escape(s)), unicode)
+ ## Test the missing_tags attribute.
+
+ def test__missing_tags__unknown_value(self):
+ """
+ Check missing_tags attribute: setting an unknown value.
+
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'foo'
+
+ self.assertException(Exception, "Unsupported 'missing_tags' value: 'foo'",
+ renderer._make_render_engine)
+
+ ## Test the engine's resolve_context attribute.
+
+ def test__resolve_context(self):
+ """
+ Check resolve_context(): default arguments.
+
+ """
+ renderer = Renderer()
+
+ engine = renderer._make_render_engine()
+
+ stack = ContextStack({'foo': 'bar'})
+
+ self.assertEqual('bar', engine.resolve_context(stack, 'foo'))
+ self.assertString(u'', engine.resolve_context(stack, 'missing'))
+
+ def test__resolve_context__missing_tags_strict(self):
+ """
+ Check resolve_context(): missing_tags 'strict'.
+
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'strict'
+
+ engine = renderer._make_render_engine()
+
+ stack = ContextStack({'foo': 'bar'})
+
+ self.assertEqual('bar', engine.resolve_context(stack, 'foo'))
+ self.assertException(KeyNotFoundError, "Key 'missing' not found: first part",
+ engine.resolve_context, stack, 'missing')
diff --git a/pystache/tests/test_specloader.py b/pystache/tests/test_specloader.py
index 24fb34d..d934987 100644
--- a/pystache/tests/test_specloader.py
+++ b/pystache/tests/test_specloader.py
@@ -30,6 +30,14 @@ class Thing(object):
pass
+class AssertPathsMixin:
+
+ """A unittest.TestCase mixin to check path equality."""
+
+ def assertPaths(self, actual, expected):
+ self.assertEqual(actual, expected)
+
+
class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_template_rel_directory(self):
@@ -174,7 +182,8 @@ def _make_specloader():
return SpecLoader(loader=loader)
-class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin,
+ AssertPathsMixin):
"""
Tests template_spec.SpecLoader.
@@ -288,13 +297,21 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
self.assertEqual(loader.s, "template-foo")
self.assertEqual(loader.encoding, "encoding-foo")
+ def test_find__template_path(self):
+ """Test _find() with TemplateSpec.template_path."""
+ loader = self._make_specloader()
+ custom = TemplateSpec()
+ custom.template_path = "path/foo"
+ actual = loader._find(custom)
+ self.assertPaths(actual, "path/foo")
+
# TODO: migrate these tests into the SpecLoaderTests class.
# TODO: rename the get_template() tests to test load().
# TODO: condense, reorganize, and rename the tests so that it is
# clear whether we have full test coverage (e.g. organized by
# TemplateSpec attributes or something).
-class TemplateSpecTests(unittest.TestCase):
+class TemplateSpecTests(unittest.TestCase, AssertPathsMixin):
def _make_loader(self):
return _make_specloader()
@@ -358,13 +375,6 @@ 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.
@@ -379,7 +389,7 @@ class TemplateSpecTests(unittest.TestCase):
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'foo/bar.txt')
- self._assert_paths(actual, expected)
+ self.assertPaths(actual, expected)
def test_find__without_directory(self):
"""
@@ -394,7 +404,7 @@ class TemplateSpecTests(unittest.TestCase):
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'sample_view.mustache')
- self._assert_paths(actual, expected)
+ self.assertPaths(actual, expected)
def _assert_get_template(self, custom, expected):
loader = self._make_loader()
diff --git a/setup.py b/setup.py
index 548a923..54780e0 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,42 @@ This script supports publishing 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--
+(1) Prepare the release.
+
+Make sure the code is finalized and merged to master. Bump the version
+number in setup.py, etc.
+
+Generate the reStructuredText long_description using--
+
+ $ python setup.py prep
+
+and be sure this new version is checked in. You must have pandoc installed
+to do this step:
+
+ http://johnmacfarlane.net/pandoc/
+
+It helps to review this auto-generated file on GitHub prior to uploading
+because the long description will be sent to PyPI and appear there after
+publishing. PyPI attempts to convert this string to HTML before displaying
+it on the PyPI project page. If PyPI finds any issues, it will render it
+instead as plain-text, which we do not want.
+
+To check in advance that PyPI will accept and parse the reST file as HTML,
+you can use the rst2html program installed by the docutils package
+(http://docutils.sourceforge.net/). To install docutils:
+
+ $ pip install docutils
+
+To check the file, run the following command and confirm that it reports
+no warnings:
+
+ $ python setup.py --long-description | rst2html.py -v --no-raw > out.html
+
+See here for more information:
+
+ http://docs.python.org/distutils/uploading.html#pypi-package-display
+
+(2) Push to PyPI. To release a new version of Pystache to PyPI--
http://pypi.python.org/pypi/pystache
@@ -15,8 +50,7 @@ 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,
-merging to master, bumping the version number in setup.py, etc):
+When you have permissions, run the following:
python setup.py publish
@@ -35,7 +69,7 @@ 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.
+(3) Tag the release on GitHub. Here are some commands for tagging.
List current tags:
@@ -52,12 +86,22 @@ Push a tag to GitHub:
"""
import os
+import shutil
import sys
+
py_version = sys.version_info
-# Distribute works with Python 2.3.5 and above:
+# distutils does not seem to support the following setup() arguments.
+# It displays a UserWarning when setup() is passed those options:
+#
+# * entry_points
+# * install_requires
+#
+# 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
@@ -67,16 +111,20 @@ 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.3-rc' # Also change in pystache/__init__.py.
-VERSION = '0.5.2' # Also change in pystache/__init__.py.
+FILE_ENCODING = 'utf-8'
-HISTORY_PATH = 'HISTORY.rst'
+README_PATH = 'README.md'
+HISTORY_PATH = 'HISTORY.md'
LICENSE_PATH = 'LICENSE'
-README_PATH = 'README.rst'
+
+RST_DESCRIPTION_PATH = 'setup_description.rst'
+
+TEMP_EXTENSION = '.temp'
+
+PREP_COMMAND = 'prep'
CLASSIFIERS = (
'Development Status :: 4 - Beta',
@@ -90,8 +138,15 @@ CLASSIFIERS = (
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: Implementation :: PyPy',
)
+# Comments in reST begin with two dots.
+RST_LONG_DESCRIPTION_INTRO = """\
+.. Do not edit this file. This file is auto-generated for PyPI by setup.py
+.. using pandoc, so edits should go in the source files rather than here.
+"""
+
def read(path):
"""
@@ -106,49 +161,151 @@ def read(path):
finally:
f.close()
- return b.decode('utf-8')
+ return b.decode(FILE_ENCODING)
-def publish():
+def write(u, path):
"""
- Publish this package to PyPI (aka "the Cheeseshop").
+ Write a unicode string to a file (as utf-8).
"""
- os.system('python setup.py sdist upload')
+ print("writing to: %s" % path)
+ # This function implementation was chosen to be compatible across Python 2/3.
+ f = open(path, "wb")
+ try:
+ b = u.encode(FILE_ENCODING)
+ f.write(b)
+ finally:
+ f.close()
+
+
+def make_temp_path(path, new_ext=None):
+ """
+ Arguments:
+
+ new_ext: the new file extension, including the leading dot.
+ Defaults to preserving the existing file extension.
+
+ """
+ root, ext = os.path.splitext(path)
+ if new_ext is None:
+ new_ext = ext
+ temp_path = root + TEMP_EXTENSION + new_ext
+ return temp_path
+
+
+def strip_html_comments(text):
+ """Strip HTML comments from a unicode string."""
+ lines = text.splitlines(True) # preserve line endings.
+
+ # Remove HTML comments (which we only allow to take a special form).
+ new_lines = filter(lambda line: not line.startswith("<!--"), lines)
+
+ return "".join(new_lines)
+
+
+# We write the converted file to a temp file to simplify debugging and
+# to avoid removing a valid pre-existing file on failure.
+def convert_md_to_rst(md_path, rst_temp_path):
+ """
+ Convert the contents of a file from Markdown to reStructuredText.
+
+ Returns the converted text as a Unicode string.
+
+ Arguments:
+
+ md_path: a path to a UTF-8 encoded Markdown file to convert.
+
+ rst_temp_path: a temporary path to which to write the converted contents.
+
+ """
+ # Pandoc uses the UTF-8 character encoding for both input and output.
+ command = "pandoc --write=rst --output=%s %s" % (rst_temp_path, md_path)
+ print("converting with pandoc: %s to %s\n-->%s" % (md_path, rst_temp_path,
+ command))
+
+ if os.path.exists(rst_temp_path):
+ os.remove(rst_temp_path)
+
+ os.system(command)
+
+ if not os.path.exists(rst_temp_path):
+ s = ("Error running: %s\n"
+ " Did you install pandoc per the %s docstring?" % (command,
+ __file__))
+ sys.exit(s)
+
+ return read(rst_temp_path)
+# The long_description needs to be formatted as reStructuredText.
+# See the following for more information:
+#
+# http://docs.python.org/distutils/setupscript.html#additional-meta-data
+# http://docs.python.org/distutils/uploading.html#pypi-package-display
+#
def make_long_description():
"""
- Return the long description for the package.
+ Generate the reST long_description for setup() from source files.
+
+ Returns the generated long_description as a unicode string.
"""
- license = """\
+ readme_path = README_PATH
+
+ # Remove our HTML comments because PyPI does not allow it.
+ # See the setup.py docstring for more info on this.
+ readme_md = strip_html_comments(read(readme_path))
+ history_md = strip_html_comments(read(HISTORY_PATH))
+ license_md = """\
License
=======
""" + read(LICENSE_PATH)
- sections = [read(README_PATH), read(HISTORY_PATH), license]
- return '\n\n'.join(sections)
+ sections = [readme_md, history_md, license_md]
+ md_description = '\n\n'.join(sections)
+ # Write the combined Markdown file to a temp path.
+ md_ext = os.path.splitext(readme_path)[1]
+ md_description_path = make_temp_path(RST_DESCRIPTION_PATH, new_ext=md_ext)
+ write(md_description, md_description_path)
-if sys.argv[-1] == 'publish':
- publish()
- sys.exit()
+ rst_temp_path = make_temp_path(RST_DESCRIPTION_PATH)
+ long_description = convert_md_to_rst(md_path=md_description_path,
+ rst_temp_path=rst_temp_path)
+
+ return "\n".join([RST_LONG_DESCRIPTION_INTRO, long_description])
+
+
+def prep():
+ """Update the reST long_description file."""
+ long_description = make_long_description()
+ write(long_description, RST_DESCRIPTION_PATH)
+
+
+def publish():
+ """Publish this package to PyPI (aka "the Cheeseshop")."""
+ long_description = make_long_description()
+
+ if long_description != read(RST_DESCRIPTION_PATH):
+ print("""\
+Description file not up-to-date: %s
+Run the following command and commit the changes--
+
+ python setup.py %s
+""" % (RST_DESCRIPTION_PATH, PREP_COMMAND))
+ sys.exit()
+
+ print("Description up-to-date: %s" % RST_DESCRIPTION_PATH)
+
+ answer = raw_input("Are you sure you want to publish to PyPI (yes/no)?")
+
+ if answer != "yes":
+ exit("Aborted: nothing published")
+
+ os.system('python setup.py sdist upload')
-# 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:
@@ -180,10 +337,47 @@ PACKAGES = [
]
+# The purpose of this function is to follow the guidance suggested here:
+#
+# http://packages.python.org/distribute/python3.html#note-on-compatibility-with-setuptools
+#
+# The guidance is for better compatibility when using setuptools (e.g. with
+# earlier versions of Python 2) instead of Distribute, because of new
+# keyword arguments to setup() that setuptools may not recognize.
+def get_extra_args():
+ """
+ Return a dictionary of extra args to pass to setup().
+
+ """
+ extra = {}
+ # TODO: it might be more correct to check whether we are using
+ # Distribute instead of setuptools, since use_2to3 doesn't take
+ # effect when using Python 2, even when using Distribute.
+ if py_version >= (3, ):
+ # Causes 2to3 to be run during the build step.
+ extra['use_2to3'] = True
+
+ return extra
+
+
def main(sys_argv):
- long_description = make_long_description()
+ # TODO: use the logging module instead of printing.
+ # TODO: include the following in a verbose mode.
+ print("pystache: using: version %s of %s" % (repr(dist.__version__), repr(dist)))
+
+ command = sys_argv[-1]
+
+ if command == 'publish':
+ publish()
+ sys.exit()
+ elif command == PREP_COMMAND:
+ prep()
+ sys.exit()
+
+ long_description = read(RST_DESCRIPTION_PATH)
template_files = ['*.mustache', '*.txt']
+ extra_args = get_extra_args()
setup(name='pystache',
version=VERSION,
@@ -193,6 +387,7 @@ def main(sys_argv):
author='Chris Wanstrath',
author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
+ maintainer_email='chris.jerdonek@gmail.com',
url='http://github.com/defunkt/pystache',
install_requires=INSTALL_REQUIRES,
packages=PACKAGES,
@@ -209,7 +404,7 @@ def main(sys_argv):
],
},
classifiers = CLASSIFIERS,
- **extra
+ **extra_args
)
diff --git a/setup_description.rst b/setup_description.rst
new file mode 100644
index 0000000..0f1f86c
--- /dev/null
+++ b/setup_description.rst
@@ -0,0 +1,501 @@
+.. Do not edit this file. This file is auto-generated for PyPI by setup.py
+.. using pandoc, so edits should go in the source files rather than here.
+
+Pystache
+========
+
+.. figure:: https://s3.amazonaws.com/webdev_bucket/pystache.png
+ :align: center
+ :alt: mustachioed, monocled snake by David Phillips
+
+.. figure:: https://secure.travis-ci.org/defunkt/pystache.png?branch=master,development
+ :align: center
+ :alt:
+
+`Pystache <https://github.com/defunkt/pystache>`_ is a Python
+implementation of `Mustache <http://mustache.github.com/>`_. Mustache is
+a framework-agnostic, logic-free templating system inspired by
+`ctemplate <http://code.google.com/p/google-ctemplate/>`_ and
+`et <http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html>`_.
+Like ctemplate, Mustache "emphasizes separating logic from presentation:
+it is impossible to embed application logic in this template language."
+
+The `mustache(5) <http://mustache.github.com/mustache.5.html>`_ man page
+provides a good introduction to Mustache's syntax. For a more complete
+(and more current) description of Mustache's behavior, see the official
+`Mustache spec <https://github.com/mustache/spec>`_.
+
+Pystache is `semantically versioned <http://semver.org>`_ and can be
+found on `PyPI <http://pypi.python.org/pypi/pystache>`_. This version of
+Pystache passes all tests in `version
+1.1.2 <https://github.com/mustache/spec/tree/v1.1.2>`_ of the spec.
+
+Requirements
+------------
+
+Pystache is tested with--
+
+- Python 2.4 (requires simplejson `version
+ 2.0.9 <http://pypi.python.org/pypi/simplejson/2.0.9>`_ or earlier)
+- Python 2.5 (requires
+ `simplejson <http://pypi.python.org/pypi/simplejson/>`_)
+- Python 2.6
+- Python 2.7
+- Python 3.1
+- Python 3.2
+- `PyPy <http://pypy.org/>`_
+
+`Distribute <http://packages.python.org/distribute/>`_ (the setuptools
+fork) is recommended over
+`setuptools <http://pypi.python.org/pypi/setuptools>`_, and is required
+in some cases (e.g. for Python 3 support). If you use
+`pip <http://www.pip-installer.org/>`_, you probably already satisfy
+this requirement.
+
+JSON support is needed only for the command-line interface and to run
+the spec tests. We require simplejson for earlier versions of Python
+since Python's `json <http://docs.python.org/library/json.html>`_ 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
+----------
+
+::
+
+ pip install pystache
+
+And test it--
+
+::
+
+ pystache-test
+
+To install and test from source (e.g. from GitHub), see the Develop
+section.
+
+Use It
+------
+
+::
+
+ >>> import pystache
+ >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
+ Hi Mom!
+
+You can also create dedicated view classes to hold your view logic.
+
+Here's your view class (in .../examples/readme.py):
+
+::
+
+ class SayHello(object):
+ def to(self):
+ return "Pizza"
+
+Instantiating like so:
+
+::
+
+ >>> from pystache.tests.examples.readme import SayHello
+ >>> hello = SayHello()
+
+Then your template, say\_hello.mustache (by default in the same
+directory as your class definition):
+
+::
+
+ Hello, {{to}}!
+
+Pull it together:
+
+::
+
+ >>> renderer = pystache.Renderer()
+ >>> print renderer.render(hello)
+ Hello, Pizza!
+
+For greater control over rendering (e.g. to specify a custom template
+directory), use the ``Renderer`` class like above. One can pass
+attributes to the Renderer class constructor or set them on a Renderer
+instance. To customize template loading on a per-view basis, subclass
+``TemplateSpec``. See the docstrings of the
+`Renderer <https://github.com/defunkt/pystache/blob/master/pystache/renderer.py>`_
+class and
+`TemplateSpec <https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py>`_
+class for more information.
+
+You can also pre-parse a template:
+
+::
+
+ >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}")
+ >>> print parsed
+ [u'Hey ', _SectionNode(key=u'who', index_begin=12, index_end=18, parsed=[_EscapeNode(key=u'.'), u'!'])]
+
+And then:
+
+::
+
+ >>> print renderer.render(parsed, {'who': 'Pops'})
+ Hey Pops!
+ >>> print renderer.render(parsed, {'who': 'you'})
+ Hey you!
+
+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. 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
+-------
+
+This section describes how Pystache handles unicode, strings, and
+encodings.
+
+Internally, Pystache uses `only unicode
+strings <http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs>`_
+(``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 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 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-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 docstrings for further details. In addition, the
+``file_encoding`` attribute can be controlled on a per-view basis by
+subclassing the ``TemplateSpec`` class. When not specified explicitly,
+these attributes default to values set in Pystache's ``defaults``
+module.
+
+Develop
+-------
+
+To test from a source distribution (without installing)--
+
+::
+
+ python test_pystache.py
+
+To test Pystache with multiple versions of Python (with a single
+command!), you can use `tox <http://pypi.python.org/pypi/tox>`_:
+
+::
+
+ pip install 'virtualenv<1.8' # Version 1.8 dropped support for Python 2.4.
+ pip install 'tox<1.4' # Version 1.4 dropped support for Python 2.4.
+ tox
+
+If you do not have all Python versions listed in ``tox.ini``--
+
+::
+
+ tox -e py26,py32 # for example
+
+The source distribution tests also include doctests and tests from the
+Mustache spec. To include tests from the Mustache spec in your test
+runs:
+
+::
+
+ git submodule init
+ git submodule update
+
+The test harness parses the spec's (more human-readable) yaml files if
+`PyYAML <http://pypi.python.org/pypi/PyYAML>`_ is present. Otherwise, it
+parses the json files. To install PyYAML--
+
+::
+
+ pip install pyyaml
+
+To run a subset of the tests, you can use
+`nose <http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html>`_:
+
+::
+
+ pip install nose
+ nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
+
+Using Python 3 with Pystache from source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Pystache is written in Python 2 and must be converted to Python 3 prior
+to using it with Python 3. The installation process (and tox) do this
+automatically.
+
+To convert the code to Python 3 manually (while using Python 3)--
+
+::
+
+ python setup.py build
+
+This writes the converted code to a subdirectory called ``build``. By
+design, Python 3 builds
+`cannot <https://bitbucket.org/tarek/distribute/issue/292/allow-use_2to3-with-python-2>`_
+be created from Python 2.
+
+To convert the code without using setup.py, you can use
+`2to3 <http://docs.python.org/library/2to3.html>`_ as follows (two
+steps)--
+
+::
+
+ 2to3 --write --nobackups --no-diffs --doctests_only pystache
+ 2to3 --write --nobackups --no-diffs pystache
+
+This converts the code (and doctests) in place.
+
+To ``import pystache`` from a source distribution while using Python 3,
+be sure that you are importing from a directory containing a converted
+version of the code (e.g. from the ``build`` directory after
+converting), and not from the original (unconverted) source directory.
+Otherwise, you will get a syntax error. You can help prevent this by not
+running the Python IDE from the project directory when importing
+Pystache while using Python 3.
+
+Mailing List
+------------
+
+There is a `mailing list <http://librelist.com/browser/pystache/>`_.
+Note that there is a bit of a delay between posting a message and seeing
+it appear in the mailing list archive.
+
+Credits
+-------
+
+::
+
+ >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' }
+ >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context)
+ Author: Chris Wanstrath
+ Maintainer: Chris Jerdonek
+
+Pystache logo by `David Phillips <http://davidphillips.us/>`_ and
+licensed under a `Creative Commons Attribution-ShareAlike 3.0 Unported
+License <http://creativecommons.org/licenses/by-sa/3.0/deed.en_US>`_.
+|image0|
+
+History
+=======
+
+0.5.3 (TBD)
+-----------
+
+- Added ability to customize string coercion (e.g. to have None render
+ as ``''``) (issue #130).
+- Added Renderer.render\_name() to render a template by name (issue
+ #122).
+- Added TemplateSpec.template\_path to specify an absolute path to a
+ template (issue #41).
+- Added option of raising errors on missing tags/partials:
+ ``Renderer(missing_tags='strict')`` (issue #110).
+- Added support for finding and loading templates by file name in
+ addition to by template name (issue #127). [xgecko]
+- Added a ``parse()`` function that yields a printable, pre-compiled
+ parse tree.
+- Added support for rendering pre-compiled templates.
+- Added support for `PyPy <http://pypy.org/>`_ (issue #125).
+- Added support for `Travis CI <http://travis-ci.org>`_ (issue #124).
+ [msabramo]
+- Bugfix: ``defaults.DELIMITERS`` can now be changed at runtime (issue
+ #135). [bennoleslie]
+- Bugfix: exceptions raised from a property are no longer swallowed
+ when getting a key from a context stack (issue #110).
+- Bugfix: lambda section values can now return non-ascii, non-unicode
+ strings (issue #118).
+- Convert HISTORY and README files from reST to Markdown.
+- More robust handling of byte strings in Python 3.
+- Added Creative Commons license for David Phillips's logo.
+
+0.5.2 (2012-05-03)
+------------------
+
+- Added support for dot notation and version 1.1.2 of the spec (issue
+ #99). [rbp]
+- Missing partials now render as empty string per latest version of
+ spec (issue #115).
+- Bugfix: falsey values now coerced to strings using str().
+- Bugfix: lambda return values for sections no longer pushed onto
+ context stack (issue #113).
+- Bugfix: lists of lambdas for sections were not rendered (issue #114).
+
+0.5.1 (2012-04-24)
+------------------
+
+- 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)
+------------------
+
+This version represents a major rewrite and refactoring of the code base
+that also adds features and fixes many bugs. All functionality and
+nearly all unit tests have been preserved. However, some backwards
+incompatible changes to the API have been made.
+
+Below is a selection of some of the changes (not exhaustive).
+
+Highlights:
+
+- Pystache now passes all tests in version 1.0.3 of the `Mustache
+ spec <https://github.com/mustache/spec>`_. [pvande]
+- Removed View class: it is no longer necessary to subclass from View
+ or from any other class to create a view.
+- Replaced Template with Renderer class: template rendering behavior
+ can be modified via the Renderer constructor or by setting attributes
+ on a Renderer instance.
+- Added TemplateSpec class: template rendering can be specified on a
+ per-view basis by subclassing from TemplateSpec.
+- Introduced separation of concerns and removed circular dependencies
+ (e.g. between Template and View classes, cf. `issue
+ #13 <https://github.com/defunkt/pystache/issues/13>`_).
+- Unicode now used consistently throughout the rendering process.
+- Expanded test coverage: nosetests now runs doctests and ~105 test
+ cases from the Mustache spec (increasing the number of tests from 56
+ to ~315).
+- Added a rudimentary benchmarking script to gauge performance while
+ refactoring.
+- Extensive documentation added (e.g. docstrings).
+
+Other changes:
+
+- Added a command-line interface. [vrde]
+- The main rendering class now accepts a custom partial loader (e.g. a
+ dictionary) and a custom escape function.
+- Non-ascii characters in str strings are now supported while
+ rendering.
+- Added string encoding, file encoding, and errors options for decoding
+ to unicode.
+- Removed the output encoding option.
+- Removed the use of markupsafe.
+
+Bug fixes:
+
+- Context values no longer processed as template strings.
+ [jakearchibald]
+- Whitespace surrounding sections is no longer altered, per the spec.
+ [heliodor]
+- Zeroes now render correctly when using PyPy. [alex]
+- Multline comments now permitted. [fczuardi]
+- Extensionless template files are now supported.
+- Passing ``**kwargs`` to ``Template()`` no longer modifies the
+ context.
+- Passing ``**kwargs`` to ``Template()`` with no context no longer
+ raises an exception.
+
+0.4.1 (2012-03-25)
+------------------
+
+- Added support for Python 2.4. [wangtz, jvantuyl]
+
+0.4.0 (2011-01-12)
+------------------
+
+- Add support for nested contexts (within template and view)
+- Add support for inverted lists
+- Decoupled template loading
+
+0.3.1 (2010-05-07)
+------------------
+
+- Fix package
+
+0.3.0 (2010-05-03)
+------------------
+
+- View.template\_path can now hold a list of path
+- Add {{& blah}} as an alias for {{{ blah }}}
+- Higher Order Sections
+- Inverted sections
+
+0.2.0 (2010-02-15)
+------------------
+
+- Bugfix: Methods returning False or None are not rendered
+- Bugfix: Don't render an empty string when a tag's value is 0.
+ [enaeseth]
+- Add support for using non-callables as View attributes.
+ [joshthecoder]
+- Allow using View instances as attributes. [joshthecoder]
+- Support for Unicode and non-ASCII-encoded bytestring output.
+ [enaeseth]
+- Template file encoding awareness. [enaeseth]
+
+0.1.1 (2009-11-13)
+------------------
+
+- Ensure we're dealing with strings, always
+- Tests can be run by executing the test file directly
+
+0.1.0 (2009-11-12)
+------------------
+
+- First release
+
+License
+=======
+
+Copyright (C) 2012 Chris Jerdonek. All rights reserved.
+
+Copyright (c) 2009 Chris Wanstrath
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+.. |image0| image:: http://i.creativecommons.org/l/by-sa/3.0/88x31.png
diff --git a/tox.ini b/tox.ini
index d1aef0d..b5a9394 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@
# http://pypi.python.org/pypi/tox
#
[tox]
-envlist = py24,py25,py26,py27,py27-yaml,py27-noargs,py31,py32
+envlist = py24,py25,py26,py27,py27-yaml,py27-noargs,py31,py32,pypy
[testenv]
# Change the working directory so that we don't import the pystache located
@@ -11,7 +11,7 @@ envlist = py24,py25,py26,py27,py27-yaml,py27-noargs,py31,py32
changedir =
{envbindir}
commands =
- pystache-test {toxinidir}/ext/spec/specs {toxinidir}
+ pystache-test {toxinidir}/ext/spec/specs {toxinidir}
# Check that the spec tests work with PyYAML.
[testenv:py27-yaml]