summaryrefslogtreecommitdiffstats
path: root/python/pystache
diff options
context:
space:
mode:
Diffstat (limited to 'python/pystache')
-rw-r--r--python/pystache/.gitignore17
-rw-r--r--python/pystache/.gitmodules3
-rw-r--r--python/pystache/.travis.yml14
-rw-r--r--python/pystache/HISTORY.md169
-rw-r--r--python/pystache/LICENSE22
-rw-r--r--python/pystache/MANIFEST.in13
-rw-r--r--python/pystache/README.md276
-rw-r--r--python/pystache/TODO.md16
-rw-r--r--python/pystache/gh/images/logo_phillips.pngbin0 -> 173595 bytes
-rw-r--r--python/pystache/pystache/__init__.py13
-rw-r--r--python/pystache/pystache/commands/__init__.py4
-rw-r--r--python/pystache/pystache/commands/render.py95
-rw-r--r--python/pystache/pystache/commands/test.py18
-rw-r--r--python/pystache/pystache/common.py71
-rw-r--r--python/pystache/pystache/context.py342
-rw-r--r--python/pystache/pystache/defaults.py65
-rw-r--r--python/pystache/pystache/init.py19
-rw-r--r--python/pystache/pystache/loader.py170
-rw-r--r--python/pystache/pystache/locator.py171
-rw-r--r--python/pystache/pystache/parsed.py50
-rw-r--r--python/pystache/pystache/parser.py378
-rw-r--r--python/pystache/pystache/renderengine.py181
-rw-r--r--python/pystache/pystache/renderer.py460
-rw-r--r--python/pystache/pystache/specloader.py90
-rw-r--r--python/pystache/pystache/template_spec.py53
-rw-r--r--python/pystache/setup.py413
-rw-r--r--python/pystache/setup_description.rst513
-rw-r--r--python/pystache/test_pystache.py30
-rw-r--r--python/pystache/tox.ini36
29 files changed, 3702 insertions, 0 deletions
diff --git a/python/pystache/.gitignore b/python/pystache/.gitignore
new file mode 100644
index 000000000..758d62df9
--- /dev/null
+++ b/python/pystache/.gitignore
@@ -0,0 +1,17 @@
+*.pyc
+.DS_Store
+# Tox support. See: http://pypi.python.org/pypi/tox
+.tox
+# Our tox runs convert the doctests in *.rst files to Python 3 prior to
+# running tests. Ignore these temporary files.
+*.temp2to3.rst
+# 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.
+build
+dist
+MANIFEST
+pystache.egg-info
diff --git a/python/pystache/.gitmodules b/python/pystache/.gitmodules
new file mode 100644
index 000000000..c55c8e5e3
--- /dev/null
+++ b/python/pystache/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ext/spec"]
+ path = ext/spec
+ url = http://github.com/mustache/spec.git
diff --git a/python/pystache/.travis.yml b/python/pystache/.travis.yml
new file mode 100644
index 000000000..00227053a
--- /dev/null
+++ b/python/pystache/.travis.yml
@@ -0,0 +1,14 @@
+language: python
+
+# Travis CI has no plans to support Jython and no longer supports Python 2.5.
+python:
+ - 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/python/pystache/HISTORY.md b/python/pystache/HISTORY.md
new file mode 100644
index 000000000..e5b7638ae
--- /dev/null
+++ b/python/pystache/HISTORY.md
@@ -0,0 +1,169 @@
+History
+=======
+
+**Note:** Official support for Python 2.4 will end with Pystache version 0.6.0.
+
+0.5.4 (2014-07-11)
+------------------
+
+- Bugfix: made test with filenames OS agnostic (issue \#162).
+
+0.5.3 (2012-11-03)
+------------------
+
+- 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 Python 3.3 to the list of supported versions.
+- 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).
+- Bugfix: allow `test_pystache.py` and `tox` to pass when run from a
+ downloaded sdist (i.e. without the spec test directory).
+- 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/python/pystache/LICENSE b/python/pystache/LICENSE
new file mode 100644
index 000000000..42be9d646
--- /dev/null
+++ b/python/pystache/LICENSE
@@ -0,0 +1,22 @@
+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.
diff --git a/python/pystache/MANIFEST.in b/python/pystache/MANIFEST.in
new file mode 100644
index 000000000..bdc64bf71
--- /dev/null
+++ b/python/pystache/MANIFEST.in
@@ -0,0 +1,13 @@
+include README.md
+include HISTORY.md
+include LICENSE
+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
+# source distribution when using Distribute.
+recursive-include pystache/tests *.mustache *.txt
+# We deliberately exclude the gh/ directory because it contains copies
+# of resources needed only for the web page hosted on GitHub (via the
+# gh-pages branch).
diff --git a/python/pystache/README.md b/python/pystache/README.md
new file mode 100644
index 000000000..54a96088b
--- /dev/null
+++ b/python/pystache/README.md
@@ -0,0 +1,276 @@
+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. -->
+![](http://defunkt.github.com/pystache/images/logo_phillips.png "mustachioed, monocled snake by David Phillips")
+
+![](https://secure.travis-ci.org/defunkt/pystache.png "Travis CI current build status")
+
+[Pystache](http://defunkt.github.com/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
+- Python 3.3
+- [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'
+
+Official support for Python 2.4 will end with Pystache version 0.6.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/) is 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/python/pystache/TODO.md b/python/pystache/TODO.md
new file mode 100644
index 000000000..cd8241765
--- /dev/null
+++ b/python/pystache/TODO.md
@@ -0,0 +1,16 @@
+TODO
+====
+
+In development branch:
+
+* Figure out a way to suppress center alignment of images in reST output.
+* Add a unit test for the change made in 7ea8e7180c41. This is with regard
+ to not requiring spec tests when running tests from a downloaded sdist.
+* 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.
+* Make sure command parsing to pystache-test doesn't break with Python 2.4 and earlier.
+* Combine pystache-test with the main command.
diff --git a/python/pystache/gh/images/logo_phillips.png b/python/pystache/gh/images/logo_phillips.png
new file mode 100644
index 000000000..749190136
--- /dev/null
+++ b/python/pystache/gh/images/logo_phillips.png
Binary files differ
diff --git a/python/pystache/pystache/__init__.py b/python/pystache/pystache/__init__.py
new file mode 100644
index 000000000..4cf24344e
--- /dev/null
+++ b/python/pystache/pystache/__init__.py
@@ -0,0 +1,13 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+# We keep all initialization code in a separate module.
+
+from pystache.init import parse, render, Renderer, TemplateSpec
+
+__all__ = ['parse', 'render', 'Renderer', 'TemplateSpec']
+
+__version__ = '0.5.4' # Also change in setup.py.
diff --git a/python/pystache/pystache/commands/__init__.py b/python/pystache/pystache/commands/__init__.py
new file mode 100644
index 000000000..a0d386a38
--- /dev/null
+++ b/python/pystache/pystache/commands/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/python/pystache/pystache/commands/render.py b/python/pystache/pystache/commands/render.py
new file mode 100644
index 000000000..1a9c309d5
--- /dev/null
+++ b/python/pystache/pystache/commands/render.py
@@ -0,0 +1,95 @@
+# coding: utf-8
+
+"""
+This module provides command-line access to pystache.
+
+Run this script using the -h option for command-line help.
+
+"""
+
+
+try:
+ import json
+except:
+ # The json module is new in Python 2.6, whereas simplejson is
+ # compatible with earlier versions.
+ try:
+ import simplejson as json
+ except ImportError:
+ # Raise an error with a type different from ImportError as a hack around
+ # this issue:
+ # http://bugs.python.org/issue7559
+ from sys import exc_info
+ ex_type, ex_value, tb = exc_info()
+ new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
+ raise new_ex.__class__, new_ex, tb
+
+# The optparse module is deprecated in Python 2.7 in favor of argparse.
+# However, argparse is not available in Python 2.6 and earlier.
+from optparse import OptionParser
+import sys
+
+# We use absolute imports here to allow use of this script from its
+# location in source control (e.g. for development purposes).
+# Otherwise, the following error occurs:
+#
+# ValueError: Attempted relative import in non-package
+#
+from pystache.common import TemplateNotFoundError
+from pystache.renderer import Renderer
+
+
+USAGE = """\
+%prog [-h] template context
+
+Render a mustache template with the given context.
+
+positional arguments:
+ template A filename or template string.
+ context A filename or JSON string."""
+
+
+def parse_args(sys_argv, usage):
+ """
+ Return an OptionParser for the script.
+
+ """
+ args = sys_argv[1:]
+
+ parser = OptionParser(usage=usage)
+ options, args = parser.parse_args(args)
+
+ template, context = args
+
+ return template, context
+
+
+# TODO: verify whether the setup() method's entry_points argument
+# supports passing arguments to main:
+#
+# http://packages.python.org/distribute/setuptools.html#automatic-script-creation
+#
+def main(sys_argv=sys.argv):
+ template, context = parse_args(sys_argv, USAGE)
+
+ if template.endswith('.mustache'):
+ template = template[:-9]
+
+ renderer = Renderer()
+
+ try:
+ template = renderer.load_template(template)
+ except TemplateNotFoundError:
+ pass
+
+ try:
+ context = json.load(open(context))
+ except IOError:
+ context = json.loads(context)
+
+ rendered = renderer.render(template, context)
+ print rendered
+
+
+if __name__=='__main__':
+ main()
diff --git a/python/pystache/pystache/commands/test.py b/python/pystache/pystache/commands/test.py
new file mode 100644
index 000000000..087245338
--- /dev/null
+++ b/python/pystache/pystache/commands/test.py
@@ -0,0 +1,18 @@
+# coding: utf-8
+
+"""
+This module provides a command to test pystache (unit tests, doctests, etc).
+
+"""
+
+import sys
+
+from pystache.tests.main import main as run_tests
+
+
+def main(sys_argv=sys.argv):
+ run_tests(sys_argv=sys_argv)
+
+
+if __name__=='__main__':
+ main()
diff --git a/python/pystache/pystache/common.py b/python/pystache/pystache/common.py
new file mode 100644
index 000000000..fb266dd8b
--- /dev/null
+++ b/python/pystache/pystache/common.py
@@ -0,0 +1,71 @@
+# coding: utf-8
+
+"""
+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):
+ """
+ Return the contents of a text file as a byte string.
+
+ """
+ # Opening in binary mode is necessary for compatibility across Python
+ # 2 and 3. In both Python 2 and 3, open() defaults to opening files in
+ # text mode. However, in Python 2, open() returns file objects whose
+ # read() method returns byte strings (strings of type `str` in Python 2),
+ # whereas in Python 3, the file object returns unicode strings (strings
+ # of type `str` in Python 3).
+ f = open(path, 'rb')
+ # We avoid use of the with keyword for Python 2.4 support.
+ try:
+ return f.read()
+ finally:
+ f.close()
+
+
+class MissingTags(object):
+
+ """Contains the valid values for Renderer.missing_tags."""
+
+ ignore = 'ignore'
+ strict = 'strict'
+
+
+class PystacheError(Exception):
+ """Base class for Pystache exceptions."""
+ pass
+
+
+class TemplateNotFoundError(PystacheError):
+ """An exception raised when a template is not found."""
+ pass
diff --git a/python/pystache/pystache/context.py b/python/pystache/pystache/context.py
new file mode 100644
index 000000000..671591609
--- /dev/null
+++ b/python/pystache/pystache/context.py
@@ -0,0 +1,342 @@
+# coding: utf-8
+
+"""
+Exposes a ContextStack class.
+
+The Mustache spec makes a special distinction between two types of context
+stack elements: hashes and objects. For the purposes of interpreting the
+spec, we define these categories mutually exclusively as follows:
+
+ (1) Hash: an item whose type is a subclass of dict.
+
+ (2) Object: an item that is neither a hash nor an instance of a
+ built-in type.
+
+"""
+
+from pystache.common import PystacheError
+
+
+# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
+_BUILTIN_MODULE = type(0).__module__
+
+
+# We use this private global variable as a return value to represent a key
+# not being found on lookup. This lets us distinguish between the case
+# of a key's value being None with the case of a key not being found --
+# without having to rely on exceptions (e.g. KeyError) for flow control.
+#
+# TODO: eliminate the need for a private global variable, e.g. by using the
+# preferred Python approach of "easier to ask for forgiveness than permission":
+# http://docs.python.org/glossary.html#term-eafp
+class NotFound(object):
+ pass
+_NOT_FOUND = NotFound()
+
+
+def _get_value(context, key):
+ """
+ Retrieve a key's value from a context item.
+
+ Returns _NOT_FOUND if the key does not exist.
+
+ The ContextStack.get() docstring documents this function's intended behavior.
+
+ """
+ if isinstance(context, dict):
+ # Then we consider the argument a "hash" for the purposes of the spec.
+ #
+ # We do a membership test to avoid using exceptions for flow control
+ # (e.g. catching KeyError).
+ if key in context:
+ return context[key]
+ elif type(context).__module__ != _BUILTIN_MODULE:
+ # Then we consider the argument an "object" for the purposes of
+ # the spec.
+ #
+ # The elif test above lets us avoid treating instances of built-in
+ # 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.
+ 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):
+ return attr()
+ return attr
+
+ 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):
+
+ """
+ Provides dictionary-like access to a stack of zero or more items.
+
+ Instances of this class are meant to act as the rendering context
+ when rendering Mustache templates in accordance with mustache(5)
+ and the Mustache spec.
+
+ Instances encapsulate a private stack of hashes, objects, and built-in
+ type instances. Querying the stack for the value of a key queries
+ the items in the stack in order from last-added objects to first
+ (last in, first out).
+
+ Caution: this class does not currently support recursive nesting in
+ that items in the stack cannot themselves be ContextStack instances.
+
+ See the docstrings of the methods of this class for more details.
+
+ """
+
+ # We reserve keyword arguments for future options (e.g. a "strict=True"
+ # option for enabling a strict mode).
+ def __init__(self, *items):
+ """
+ Construct an instance, and initialize the private stack.
+
+ The *items arguments are the items with which to populate the
+ initial stack. Items in the argument list are added to the
+ stack in order so that, in particular, items at the end of
+ the argument list are queried first when querying the stack.
+
+ Caution: items should not themselves be ContextStack instances, as
+ recursive nesting does not behave as one might expect.
+
+ """
+ self._stack = list(items)
+
+ def __repr__(self):
+ """
+ Return a string representation of the instance.
+
+ For example--
+
+ >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
+ >>> repr(context)
+ "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
+
+ """
+ return "%s%s" % (self.__class__.__name__, tuple(self._stack))
+
+ @staticmethod
+ def create(*context, **kwargs):
+ """
+ Build a ContextStack instance from a sequence of context-like items.
+
+ This factory-style method is more general than the ContextStack class's
+ constructor in that, unlike the constructor, the argument list
+ can itself contain ContextStack instances.
+
+ Here is an example illustrating various aspects of this method:
+
+ >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
+ >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
+ >>>
+ >>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
+ >>>
+ >>> context.get('animal')
+ 'cat'
+ >>> context.get('vegetable')
+ 'spinach'
+ >>> context.get('mineral')
+ 'gold'
+
+ Arguments:
+
+ *context: zero or more dictionaries, ContextStack instances, or objects
+ with which to populate the initial context stack. None
+ arguments will be skipped. Items in the *context list are
+ added to the stack in order so that later items in the argument
+ list take precedence over earlier items. This behavior is the
+ same as the constructor's.
+
+ **kwargs: additional key-value data to add to the context stack.
+ As these arguments appear after all items in the *context list,
+ in the case of key conflicts these values take precedence over
+ all items in the *context list. This behavior is the same as
+ the constructor's.
+
+ """
+ items = context
+
+ context = ContextStack()
+
+ for item in items:
+ if item is None:
+ continue
+ if isinstance(item, ContextStack):
+ context._stack.extend(item._stack)
+ else:
+ context.push(item)
+
+ if kwargs:
+ context.push(kwargs)
+
+ return context
+
+ # TODO: add more unit tests for this.
+ # TODO: update the docstring for dotted names.
+ def get(self, name):
+ """
+ Resolve a dotted name against the current context stack.
+
+ This function follows the rules outlined in the section of the
+ spec regarding tag interpolation. This function returns the value
+ as is and does not coerce the return value to a string.
+
+ Arguments:
+
+ name: a dotted or non-dotted name.
+
+ default: the value to return if name resolution fails at any point.
+ Defaults to the empty string per the Mustache spec.
+
+ This method queries items in the stack in order from last-added
+ objects to first (last in, first out). The value returned is
+ the value of the key in the first item that contains the key.
+ If the key is not found in any item in the stack, then the default
+ value is returned. The default value defaults to None.
+
+ In accordance with the spec, this method queries items in the
+ stack for a key differently depending on whether the item is a
+ hash, object, or neither (as defined in the module docstring):
+
+ (1) Hash: if the item is a hash, then the key's value is the
+ dictionary value of the key. If the dictionary doesn't contain
+ the key, then the key is considered not found.
+
+ (2) Object: if the item is an an object, then the method looks for
+ an attribute with the same name as the key. If an attribute
+ with that name exists, the value of the attribute is returned.
+ If the attribute is callable, however (i.e. if the attribute
+ is a method), then the attribute is called with no arguments
+ and that value is returned. If there is no attribute with
+ the same name as the key, then the key is considered not found.
+
+ (3) Neither: if the item is neither a hash nor an object, then
+ the key is considered not found.
+
+ *Caution*:
+
+ Callables are handled differently depending on whether they are
+ dictionary values, as in (1) above, or attributes, as in (2).
+ The former are returned as-is, while the latter are first
+ called and that value returned.
+
+ Here is an example to illustrate:
+
+ >>> def greet():
+ ... return "Hi Bob!"
+ >>>
+ >>> class Greeter(object):
+ ... greet = None
+ >>>
+ >>> dct = {'greet': greet}
+ >>> obj = Greeter()
+ >>> obj.greet = greet
+ >>>
+ >>> dct['greet'] is obj.greet
+ True
+ >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
+ <function greet at 0x...>
+ >>> ContextStack(obj).get('greet')
+ 'Hi Bob!'
+
+ TODO: explain the rationale for this difference in treatment.
+
+ """
+ if name == '.':
+ try:
+ return self.top()
+ except IndexError:
+ raise KeyNotFoundError(".", "empty context stack")
+
+ parts = name.split('.')
+
+ try:
+ result = self._get_simple(parts[0])
+ except KeyNotFoundError:
+ raise KeyNotFoundError(name, "first part")
+
+ for part in parts[1:]:
+ # The full context stack is not used to resolve the remaining parts.
+ # From the spec--
+ #
+ # 5) If any name parts were retained in step 1, each should be
+ # resolved against a context stack containing only the result
+ # from the former resolution. If any part fails resolution, the
+ # result should be considered falsey, and should interpolate as
+ # the empty string.
+ #
+ # TODO: make sure we have a test case for the above point.
+ result = _get_value(result, part)
+ # 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
+
+ def _get_simple(self, name):
+ """
+ Query the stack for a non-dotted name.
+
+ """
+ for item in reversed(self._stack):
+ result = _get_value(item, name)
+ if result is not _NOT_FOUND:
+ return result
+
+ raise KeyNotFoundError(name, "part missing")
+
+ def push(self, item):
+ """
+ Push an item onto the stack.
+
+ """
+ self._stack.append(item)
+
+ def pop(self):
+ """
+ Pop an item off of the stack, and return it.
+
+ """
+ return self._stack.pop()
+
+ def top(self):
+ """
+ Return the item last added to the stack.
+
+ """
+ return self._stack[-1]
+
+ def copy(self):
+ """
+ Return a copy of this instance.
+
+ """
+ return ContextStack(*self._stack)
diff --git a/python/pystache/pystache/defaults.py b/python/pystache/pystache/defaults.py
new file mode 100644
index 000000000..bcfdf4cd3
--- /dev/null
+++ b/python/pystache/pystache/defaults.py
@@ -0,0 +1,65 @@
+# coding: utf-8
+
+"""
+This module provides a central location for defining default behavior.
+
+Throughout the package, these defaults take effect only when the user
+does not otherwise specify a value.
+
+"""
+
+try:
+ # Python 3.2 adds html.escape() and deprecates cgi.escape().
+ from html import escape
+except ImportError:
+ from cgi import escape
+
+import os
+import sys
+
+from pystache.common import MissingTags
+
+
+# How to handle encoding errors when decoding strings from str to unicode.
+#
+# This value is passed as the "errors" argument to Python's built-in
+# unicode() function:
+#
+# http://docs.python.org/library/functions.html#unicode
+#
+DECODE_ERRORS = 'strict'
+
+# The name of the encoding to use when converting to unicode any strings of
+# type str encountered during the rendering process.
+STRING_ENCODING = sys.getdefaultencoding()
+
+# The name of the encoding to use when converting file contents to unicode.
+# This default takes precedence over the STRING_ENCODING default for
+# 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. ['.']
+
+# The escape function to apply to strings that require escaping when
+# rendering templates (e.g. for tags enclosed in double braces).
+# Only unicode strings will be passed to this function.
+#
+# The quote=True argument causes double but not single quotes to be escaped
+# in Python 3.1 and earlier, and both double and single quotes to be
+# escaped in Python 3.2 and later:
+#
+# http://docs.python.org/library/cgi.html#cgi.escape
+# http://docs.python.org/dev/library/html.html#html.escape
+#
+TAG_ESCAPE = lambda u: escape(u, quote=True)
+
+# The default template extension, without the leading dot.
+TEMPLATE_EXTENSION = 'mustache'
diff --git a/python/pystache/pystache/init.py b/python/pystache/pystache/init.py
new file mode 100644
index 000000000..38bb1f5a0
--- /dev/null
+++ b/python/pystache/pystache/init.py
@@ -0,0 +1,19 @@
+# encoding: utf-8
+
+"""
+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
+
+
+def render(template, context=None, **kwargs):
+ """
+ Return the given template string rendered using the given context.
+
+ """
+ renderer = Renderer()
+ return renderer.render(template, context, **kwargs)
diff --git a/python/pystache/pystache/loader.py b/python/pystache/pystache/loader.py
new file mode 100644
index 000000000..d4a7e5310
--- /dev/null
+++ b/python/pystache/pystache/loader.py
@@ -0,0 +1,170 @@
+# coding: utf-8
+
+"""
+This module provides a Loader class for locating and reading templates.
+
+"""
+
+import os
+import sys
+
+from pystache import common
+from pystache import defaults
+from pystache.locator import Locator
+
+
+# We make a function so that the current defaults take effect.
+# TODO: revisit whether this is necessary.
+
+def _make_to_unicode():
+ def to_unicode(s, encoding=None):
+ """
+ Raises a TypeError exception if the given string is already unicode.
+
+ """
+ if encoding is None:
+ encoding = defaults.STRING_ENCODING
+ return unicode(s, encoding, defaults.DECODE_ERRORS)
+ return to_unicode
+
+
+class Loader(object):
+
+ """
+ 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,
+ search_dirs=None):
+ """
+ Construct a template loader instance.
+
+ Arguments:
+
+ 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.
+
+ search_dirs: the list of directories in which to search when loading
+ a template by name or file name. Defaults to the package default.
+
+ to_unicode: the function to use when converting strings of type
+ str to unicode. The function should have the signature:
+
+ to_unicode(s, encoding=None)
+
+ It should accept a string of type str and an optional encoding
+ name and return a string of type unicode. Defaults to calling
+ Python's built-in function unicode() using the package string
+ encoding and decode errors defaults.
+
+ """
+ if extension is None:
+ extension = defaults.TEMPLATE_EXTENSION
+
+ if file_encoding is None:
+ file_encoding = defaults.FILE_ENCODING
+
+ if search_dirs is None:
+ search_dirs = defaults.SEARCH_DIRS
+
+ if to_unicode is None:
+ to_unicode = _make_to_unicode()
+
+ self.extension = extension
+ self.file_encoding = file_encoding
+ # TODO: unit test setting this attribute.
+ self.search_dirs = search_dirs
+ self.to_unicode = to_unicode
+
+ def _make_locator(self):
+ return Locator(extension=self.extension)
+
+ def unicode(self, s, encoding=None):
+ """
+ Convert a string to unicode using the given encoding, and return it.
+
+ This function uses the underlying to_unicode attribute.
+
+ Arguments:
+
+ s: a basestring instance to convert to unicode. Unlike Python's
+ built-in unicode() function, it is okay to pass unicode strings
+ to this function. (Passing a unicode string to Python's unicode()
+ with the encoding argument throws the error, "TypeError: decoding
+ Unicode is not supported.")
+
+ encoding: the encoding to pass to the to_unicode attribute.
+ Defaults to None.
+
+ """
+ if isinstance(s, unicode):
+ return unicode(s)
+
+ return self.to_unicode(s, encoding)
+
+ def read(self, path, encoding=None):
+ """
+ Read the template at the given path, and return it as a unicode string.
+
+ """
+ b = common.read(path)
+
+ if encoding is None:
+ encoding = self.file_encoding
+
+ return self.unicode(b, encoding)
+
+ 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 template name.
+
+ Arguments:
+
+ name: the name of the template.
+
+ """
+ locator = self._make_locator()
+
+ path = locator.find_name(name, self.search_dirs)
+
+ return self.read(path)
+
+ # TODO: unit-test this method.
+ def load_object(self, obj):
+ """
+ Find and return the template associated to the given object.
+
+ Arguments:
+
+ obj: an instance of a user-defined class.
+
+ search_dirs: the list of directories in which to search.
+
+ """
+ locator = self._make_locator()
+
+ path = locator.find_object(obj, self.search_dirs)
+
+ return self.read(path)
diff --git a/python/pystache/pystache/locator.py b/python/pystache/pystache/locator.py
new file mode 100644
index 000000000..30c5b01e0
--- /dev/null
+++ b/python/pystache/pystache/locator.py
@@ -0,0 +1,171 @@
+# coding: utf-8
+
+"""
+This module provides a Locator class for finding template files.
+
+"""
+
+import os
+import re
+import sys
+
+from pystache.common import TemplateNotFoundError
+from pystache import defaults
+
+
+class Locator(object):
+
+ def __init__(self, extension=None):
+ """
+ Construct a template locator.
+
+ Arguments:
+
+ 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:
+ extension = defaults.TEMPLATE_EXTENSION
+
+ self.template_extension = extension
+
+ def get_object_directory(self, obj):
+ """
+ Return the directory containing an object's defining class.
+
+ Returns None if there is no such directory, for example if the
+ class was defined in an interactive Python session, or in a
+ doctest that appears in a text file (rather than a Python file).
+
+ """
+ if not hasattr(obj, '__module__'):
+ return None
+
+ module = sys.modules[obj.__module__]
+
+ if not hasattr(module, '__file__'):
+ # TODO: add a unit test for this case.
+ return None
+
+ path = module.__file__
+
+ return os.path.dirname(path)
+
+ def make_template_name(self, obj):
+ """
+ Return the canonical template name for an object instance.
+
+ This method converts Python-style class names (PEP 8's recommended
+ CamelCase, aka CapWords) to lower_case_with_underscords. Here
+ is an example with code:
+
+ >>> class HelloWorld(object):
+ ... pass
+ >>> hi = HelloWorld()
+ >>>
+ >>> locator = Locator()
+ >>> locator.make_template_name(hi)
+ 'hello_world'
+
+ """
+ template_name = obj.__class__.__name__
+
+ def repl(match):
+ return '_' + match.group(0).lower()
+
+ return re.sub('[A-Z]', repl, template_name)[1:]
+
+ def make_file_name(self, template_name, template_extension=None):
+ """
+ Generate and return the file name for the given template name.
+
+ Arguments:
+
+ template_extension: defaults to the instance's extension.
+
+ """
+ file_name = template_name
+
+ if template_extension is None:
+ template_extension = self.template_extension
+
+ if template_extension is not False:
+ file_name += os.path.extsep + template_extension
+
+ return file_name
+
+ def _find_path(self, search_dirs, file_name):
+ """
+ Search for the given file, and return the path.
+
+ Returns None if the file is not found.
+
+ """
+ for dir_path in search_dirs:
+ file_path = os.path.join(dir_path, file_name)
+ if os.path.exists(file_path):
+ return file_path
+
+ return None
+
+ def _find_path_required(self, search_dirs, file_name):
+ """
+ Return the path to a template with the given file name.
+
+ """
+ path = self._find_path(search_dirs, file_name)
+
+ if path is None:
+ raise TemplateNotFoundError('File %s not found in dirs: %s' %
+ (repr(file_name), repr(search_dirs)))
+
+ 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)
+
+ return self._find_path_required(search_dirs, file_name)
+
+ def find_object(self, obj, search_dirs, file_name=None):
+ """
+ Return the path to a template associated with the given object.
+
+ """
+ if file_name is None:
+ # TODO: should we define a make_file_name() method?
+ template_name = self.make_template_name(obj)
+ file_name = self.make_file_name(template_name)
+
+ dir_path = self.get_object_directory(obj)
+
+ if dir_path is not None:
+ search_dirs = [dir_path] + search_dirs
+
+ path = self._find_path_required(search_dirs, file_name)
+
+ return path
diff --git a/python/pystache/pystache/parsed.py b/python/pystache/pystache/parsed.py
new file mode 100644
index 000000000..372d96c66
--- /dev/null
+++ b/python/pystache/pystache/parsed.py
@@ -0,0 +1,50 @@
+# coding: utf-8
+
+"""
+Exposes a class that represents a parsed (or compiled) template.
+
+"""
+
+
+class ParsedTemplate(object):
+
+ """
+ Represents a parsed or compiled template.
+
+ 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.
+
+ """
+
+ def __init__(self):
+ self._parse_tree = []
+
+ def __repr__(self):
+ return repr(self._parse_tree)
+
+ def add(self, node):
+ """
+ Arguments:
+
+ node: a unicode string or node object instance. See the class
+ docstring for information.
+
+ """
+ self._parse_tree.append(node)
+
+ 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(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/python/pystache/pystache/parser.py b/python/pystache/pystache/parser.py
new file mode 100644
index 000000000..9a4fba235
--- /dev/null
+++ b/python/pystache/pystache/parser.py
@@ -0,0 +1,378 @@
+# coding: utf-8
+
+"""
+Exposes a parse() function to parse template strings.
+
+"""
+
+import re
+
+from pystache import defaults
+from pystache.parsed import ParsedTemplate
+
+
+END_OF_LINE_CHARACTERS = [u'\r', u'\n']
+NON_BLANK_RE = re.compile(ur'^(.)', re.M)
+
+
+# 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):
+ """
+ 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 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 expression object (re.RegexObject) instance.
+
+ """
+ # The possible tag type characters following the opening tag,
+ # excluding "=" and "{".
+ tag_types = "!>&/#^"
+
+ # TODO: are we following this in the spec?
+ #
+ # The tag's content MUST be a non-whitespace character sequence
+ # NOT containing the current closing delimiter.
+ #
+ tag = r"""
+ (?P<whitespace>[\ \t]*)
+ %(otag)s \s*
+ (?:
+ (?P<change>=) \s* (?P<delims>.+?) \s* = |
+ (?P<raw>{) \s* (?P<raw_name>.+?) \s* } |
+ (?P<tag>[%(tag_types)s]?) \s* (?P<tag_key>[\s\S]+?)
+ )
+ \s* %(ctag)s
+ """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])}
+
+ return re.compile(tag, re.VERBOSE)
+
+
+class ParsingError(Exception):
+
+ pass
+
+
+## Node types
+
+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))
+
+
+class _CommentNode(object):
+
+ 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 = defaults.DELIMITERS
+
+ self._delimiters = delimiters
+
+ def _compile_delimiters(self):
+ self._template_re = _compile_template_re(self._delimiters)
+
+ def _change_delimiters(self, delimiters):
+ self._delimiters = delimiters
+ self._compile_delimiters()
+
+ def parse(self, template):
+ """
+ Parse a template string starting at some index.
+
+ This method uses the current tag delimiter.
+
+ Arguments:
+
+ template: a unicode string that is the template to parse.
+
+ index: the index at which to start parsing.
+
+ Returns:
+
+ a ParsedTemplate instance.
+
+ """
+ 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, start_index)
+
+ if match is None:
+ break
+
+ match_index = match.start()
+ end_index = match.end()
+
+ matches = match.groupdict()
+
+ # Normalize the matches dictionary.
+ if matches['change'] is not None:
+ matches.update(tag='=', tag_key=matches['delims'])
+ elif matches['raw'] is not None:
+ matches.update(tag='&', tag_key=matches['raw_name'])
+
+ tag_type = matches['tag']
+ tag_key = matches['tag_key']
+ leading_whitespace = matches['whitespace']
+
+ # Standalone (non-interpolation) tags consume the entire line,
+ # both leading whitespace and trailing newline.
+ did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS
+ did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
+ is_tag_interpolating = tag_type in ['', '&']
+
+ if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating:
+ if end_index < len(template):
+ end_index += template[end_index] == '\r' and 1 or 0
+ if end_index < len(template):
+ end_index += template[end_index] == '\n' and 1 or 0
+ elif leading_whitespace:
+ match_index += len(leading_whitespace)
+ leading_whitespace = ''
+
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != match_index:
+ parsed_template.add(template[start_index:match_index])
+
+ start_index = end_index
+
+ if tag_type in ('#', '^'):
+ # Cache current state.
+ state = (tag_type, end_index, section_key, parsed_template)
+ states.append(state)
+
+ # Initialize new state
+ section_key, parsed_template = tag_key, ParsedTemplate()
+ continue
+
+ if tag_type == '/':
+ if tag_key != section_key:
+ raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
+
+ # Restore previous state with newly found section data.
+ parsed_section = parsed_template
+
+ (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)
+
+ else:
+ node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace)
+
+ parsed_template.add(node)
+
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != len(template):
+ parsed_template.add(template[start_index:])
+
+ return parsed_template
+
+ def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
+ """
+ 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 _CommentNode()
+
+ if tag_type == '=':
+ delimiters = tag_key.split()
+ self._change_delimiters(delimiters)
+ return _ChangeNode(delimiters)
+
+ if tag_type == '':
+ return _EscapeNode(tag_key)
+
+ if tag_type == '&':
+ return _LiteralNode(tag_key)
+
+ if tag_type == '>':
+ return _PartialNode(tag_key, leading_whitespace)
+
+ raise Exception("Invalid symbol for interpolation tag: %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.
+
+ """
+ if tag_type == '#':
+ return _SectionNode(tag_key, parsed_section, self._delimiters,
+ template, section_start_index, section_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/python/pystache/pystache/renderengine.py b/python/pystache/pystache/renderengine.py
new file mode 100644
index 000000000..c797b1765
--- /dev/null
+++ b/python/pystache/pystache/renderengine.py
@@ -0,0 +1,181 @@
+# coding: utf-8
+
+"""
+Defines a class responsible for rendering logic.
+
+"""
+
+import re
+
+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):
+
+ """
+ Provides a render() method.
+
+ This class is meant only for internal use.
+
+ As a rule, the code in this class operates on unicode strings where
+ possible rather than, say, strings of type str or markupsafe.Markup.
+ This means that strings obtained from "external" sources like partials
+ and variable tag values are immediately converted to unicode (or
+ escaped and converted to unicode) before being operated on further.
+ This makes maintaining, reasoning about, and testing the correctness
+ of the code much simpler. In particular, it keeps the implementation
+ of this class independent of the API details of one (or possibly more)
+ unicode subclasses (e.g. markupsafe.Markup).
+
+ """
+
+ # 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:
+
+ 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
+ str or unicode (or a subclass) and return a string of type
+ unicode (but not a proper subclass of unicode).
+ This class will only pass basestring instances to this
+ function. For example, it will call str() on integer variable
+ values prior to passing them to this function.
+
+ escape: the function used to escape and convert variable tag
+ values to unicode, e.g. the value corresponding to a tag
+ "{{name}}". The function should obey the same properties
+ described above for the "literal" function argument.
+ This function should take care to convert any str
+ arguments to unicode just as the literal function should, as
+ this class will not pass tag values to literal prior to passing
+ them to this function. This allows for more flexibility,
+ for example using a custom escape function that handles
+ incoming strings of type 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.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 = self.resolve_context(context, name)
+
+ if callable(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 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:
+ #
+ # http://docs.python.org/library/functions.html#iter
+ #
+ # It is not sufficient, for example, to check whether the item
+ # implements __iter__ () (the iteration protocol). There is
+ # also __getitem__() (the sequence protocol). In Python 2,
+ # strings do not implement __iter__(), but in Python 3 they do.
+ try:
+ iter(data)
+ except TypeError:
+ # Then the value does not support iteration.
+ data = [data]
+ else:
+ if is_string(data) or isinstance(data, dict):
+ # Do not treat strings and dicts (which are iterable) as lists.
+ data = [data]
+ # Otherwise, treat the value as a list.
+
+ return data
+
+ def _render_value(self, val, context, delimiters=None):
+ """
+ Render an arbitrary value.
+
+ """
+ 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):
+ """
+ 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_stack: a ContextStack instance.
+
+ """
+ parsed_template = parse(template, delimiters)
+
+ return parsed_template.render(self, context_stack)
diff --git a/python/pystache/pystache/renderer.py b/python/pystache/pystache/renderer.py
new file mode 100644
index 000000000..ff6a90c64
--- /dev/null
+++ b/python/pystache/pystache/renderer.py
@@ -0,0 +1,460 @@
+# coding: utf-8
+
+"""
+This module provides a Renderer class to render templates.
+
+"""
+
+import sys
+
+from pystache import defaults
+from pystache.common import TemplateNotFoundError, MissingTags, is_string
+from pystache.context import ContextStack, KeyNotFoundError
+from pystache.loader import Loader
+from pystache.parsed import ParsedTemplate
+from pystache.renderengine import context_get, RenderEngine
+from pystache.specloader import SpecLoader
+from pystache.template_spec import TemplateSpec
+
+
+class Renderer(object):
+
+ """
+ A class for rendering mustache templates.
+
+ This class supports several rendering options which are described in
+ the constructor's docstring. Other behavior can be customized by
+ subclassing this class.
+
+ 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)
+ >>> # We apply print to make the test work in Python 3 after 2to3.
+ >>> 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, 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
+ and returns the corresponding template as a string, preferably
+ as a unicode string. If there is no template with that name,
+ the get() method should either return None (as dict.get() does)
+ or raise an exception.
+ If this argument is None, the rendering process will use
+ the normal procedure of locating and reading templates from
+ the file system -- using relevant instance attributes like
+ search_dirs, file_encoding, etc.
+
+ 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
+ that is again unicode (or a subclass of unicode).
+ This function need not handle strings of type `str` because
+ this class will only pass it unicode strings. The constructor
+ assigns this function to the constructed instance's escape()
+ method.
+ To disable escaping entirely, one can pass `lambda u: u`
+ as the escape function, for example. One may also wish to
+ consider using markupsafe's escape function: markupsafe.escape().
+ This argument defaults to the package default.
+
+ 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.
+
+ """
+ if decode_errors is None:
+ decode_errors = defaults.DECODE_ERRORS
+
+ if escape is None:
+ escape = defaults.TAG_ESCAPE
+
+ if file_encoding is None:
+ file_encoding = defaults.FILE_ENCODING
+
+ 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
+
+ if string_encoding is None:
+ string_encoding = defaults.STRING_ENCODING
+
+ if isinstance(search_dirs, basestring):
+ search_dirs = [search_dirs]
+
+ self._context = None
+ self.decode_errors = decode_errors
+ 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
+
+ # This is an experimental way of giving views access to the current context.
+ # TODO: consider another approach of not giving access via a property,
+ # but instead letting the caller pass the initial context to the
+ # main render() method by reference. This approach would probably
+ # be less likely to be misused.
+ @property
+ def context(self):
+ """
+ Return the current rendering context [experimental].
+
+ """
+ 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.
+
+ """
+ # We type-check to avoid "TypeError: decoding Unicode is not supported".
+ # We avoid the Python ternary operator for Python 2.4 support.
+ if isinstance(s, unicode):
+ return s
+ return self.unicode(s)
+
+ def _to_unicode_hard(self, s):
+ """
+ Convert a basestring to a string with type unicode (not subclass).
+
+ """
+ return unicode(self._to_unicode_soft(s))
+
+ def _escape_to_unicode(self, s):
+ """
+ Convert a basestring to unicode (preserving any unicode subclass), and escape it.
+
+ Returns a unicode string (not subclass).
+
+ """
+ return unicode(self.escape(self._to_unicode_soft(s)))
+
+ def unicode(self, b, encoding=None):
+ """
+ Convert a byte string to unicode, using string_encoding and decode_errors.
+
+ Arguments:
+
+ b: a byte string.
+
+ encoding: the name of an encoding. Defaults to the string_encoding
+ attribute for this instance.
+
+ Raises:
+
+ TypeError: Because this method calls Python's built-in unicode()
+ function, this method raises the following exception if the
+ given string is already unicode:
+
+ TypeError: decoding Unicode is not supported
+
+ """
+ if encoding is None:
+ encoding = self.string_encoding
+
+ # TODO: Wrap UnicodeDecodeErrors with a message about setting
+ # the string_encoding and decode_errors attributes.
+ return unicode(b, encoding, self.decode_errors)
+
+ def _make_loader(self):
+ """
+ Create a Loader instance using current attributes.
+
+ """
+ return Loader(file_encoding=self.file_encoding, extension=self.file_extension,
+ to_unicode=self.unicode, search_dirs=self.search_dirs)
+
+ def _make_load_template(self):
+ """
+ Return a function that loads a template by name.
+
+ """
+ loader = self._make_loader()
+
+ def load_template(template_name):
+ return loader.load_name(template_name)
+
+ return load_template
+
+ def _make_load_partial(self):
+ """
+ Return a function that loads a partial by name.
+
+ """
+ if self.partials is None:
+ return self._make_load_template()
+
+ # 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)))
+
+ # RenderEngine requires that the return value be unicode.
+ return self._to_unicode_hard(template)
+
+ return load_partial
+
+ def _is_missing_tags_strict(self):
+ """
+ Return whether missing_tags is set to strict.
+
+ """
+ val = self.missing_tags
+
+ if val == MissingTags.strict:
+ return True
+ elif val == MissingTags.ignore:
+ return False
+
+ raise Exception("Unsupported 'missing_tags' value: %s" % repr(val))
+
+ def _make_resolve_partial(self):
+ """
+ Return the resolve_partial function to pass to RenderEngine.__init__().
+
+ """
+ load_partial = self._make_load_partial()
+
+ 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):
+ """
+ Return the resolve_context function to pass to RenderEngine.__init__().
+
+ """
+ if self._is_missing_tags_strict():
+ return context_get
+ # Otherwise, ignore missing tags.
+
+ def resolve_context(stack, name):
+ try:
+ return context_get(stack, name)
+ except KeyNotFoundError:
+ return u''
+
+ return resolve_context
+
+ 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):
+ """
+ Render the template associated with the given object.
+
+ """
+ loader = self._make_loader()
+
+ # TODO: consider an approach that does not require using an if
+ # block here. For example, perhaps this class's loader can be
+ # a SpecLoader in all cases, and the SpecLoader instance can
+ # check the object's type. Or perhaps Loader and SpecLoader
+ # can be refactored to implement the same interface.
+ if isinstance(obj, TemplateSpec):
+ loader = SpecLoader(loader)
+ template = loader.load(obj)
+ else:
+ template = loader.load_object(obj)
+
+ context = [obj] + list(context)
+
+ 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.
+
+ Read the render() docstring for more information.
+
+ """
+ loader = self._make_loader()
+ template = loader.read(template_path)
+
+ 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 string, view template, or parsed template.
+
+ Returns a unicode string.
+
+ 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 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
+ arguments are skipped. Items in the *context list are added to
+ the context stack in order so that later items in the argument
+ list take precedence over earlier items.
+
+ **kwargs: additional key-value data to add to the context stack.
+ As these arguments appear after all items in the *context list,
+ in the case of key conflicts these values take precedence over
+ all items in the *context list.
+
+ """
+ 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/python/pystache/pystache/specloader.py b/python/pystache/pystache/specloader.py
new file mode 100644
index 000000000..3a77d4c52
--- /dev/null
+++ b/python/pystache/pystache/specloader.py
@@ -0,0 +1,90 @@
+# coding: utf-8
+
+"""
+This module supports customized (aka special or specified) template loading.
+
+"""
+
+import os.path
+
+from pystache.loader import Loader
+
+
+# TODO: add test cases for this class.
+class SpecLoader(object):
+
+ """
+ Supports loading custom-specified templates (from TemplateSpec instances).
+
+ """
+
+ def __init__(self, loader=None):
+ if loader is None:
+ loader = Loader()
+
+ self.loader = loader
+
+ def _find_relative(self, spec):
+ """
+ Return the path to the template as a relative (dir, file_name) pair.
+
+ The directory returned is relative to the directory containing the
+ class definition of the given object. The method returns None for
+ this directory if the directory is unknown without first searching
+ the search directories.
+
+ """
+ if spec.template_rel_path is not None:
+ return os.path.split(spec.template_rel_path)
+ # Otherwise, determine the file name separately.
+
+ locator = self.loader._make_locator()
+
+ # We do not use the ternary operator for Python 2.4 support.
+ if spec.template_name is not None:
+ template_name = spec.template_name
+ else:
+ template_name = locator.make_template_name(spec)
+
+ file_name = locator.make_file_name(template_name, spec.template_extension)
+
+ return (spec.template_rel_directory, file_name)
+
+ def _find(self, spec):
+ """
+ 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()
+
+ if dir_path is None:
+ # Then we need to search for the path.
+ path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name)
+ else:
+ obj_dir = locator.get_object_directory(spec)
+ path = os.path.join(obj_dir, dir_path, file_name)
+
+ return path
+
+ def load(self, spec):
+ """
+ Find and return the template associated to a TemplateSpec instance.
+
+ Returns the template as a unicode string.
+
+ Arguments:
+
+ spec: a TemplateSpec instance.
+
+ """
+ if spec.template is not None:
+ return self.loader.unicode(spec.template, spec.template_encoding)
+
+ path = self._find(spec)
+
+ return self.loader.read(path, spec.template_encoding)
diff --git a/python/pystache/pystache/template_spec.py b/python/pystache/pystache/template_spec.py
new file mode 100644
index 000000000..9e9f454c1
--- /dev/null
+++ b/python/pystache/pystache/template_spec.py
@@ -0,0 +1,53 @@
+# coding: utf-8
+
+"""
+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 "special" or "specified" template information.
+
+"""
+
+class TemplateSpec(object):
+
+ """
+ A mixin or interface for specifying custom template information.
+
+ The "spec" in TemplateSpec can be taken to mean that the template
+ information is either "specified" or "special."
+
+ A view should subclass this class only if customized template loading
+ is needed. The following attributes allow one to customize/override
+ template information on a per view basis. A None value means to use
+ default behavior for that value and perform no customization. All
+ attributes are initialized to None.
+
+ Attributes:
+
+ template: the template as a string.
+
+ 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_encoding = None
+ template_extension = None
+ template_name = None
+ template_path = None
+ template_rel_directory = None
+ template_rel_path = None
diff --git a/python/pystache/setup.py b/python/pystache/setup.py
new file mode 100644
index 000000000..0d99aae8f
--- /dev/null
+++ b/python/pystache/setup.py
@@ -0,0 +1,413 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+"""
+This script supports publishing Pystache to PyPI.
+
+This docstring contains instructions to Pystache maintainers on how
+to release a new version of Pystache.
+
+(1) Prepare the release.
+
+Make sure the code is finalized and merged to master. Bump the version
+number in setup.py, update the release date in the HISTORY file, 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
+
+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:
+
+ python setup.py publish
+
+If you get an error like the following--
+
+ Upload failed (401): You must be identified to edit package information
+
+then add a file called .pyirc to your home directory with the following
+contents:
+
+ [server-login]
+ username: <PyPI username>
+ password: <PyPI password>
+
+as described here, for example:
+
+ http://docs.python.org/release/2.5.2/dist/pypirc.html
+
+(3) Tag the release on GitHub. Here are some commands for tagging.
+
+List current tags:
+
+ git tag -l -n3
+
+Create an annotated tag:
+
+ git tag -a -m "Version 0.5.1" "v0.5.1"
+
+Push a tag to GitHub:
+
+ git push --tags defunkt v0.5.1
+
+"""
+
+import os
+import shutil
+import sys
+
+
+py_version = sys.version_info
+
+# 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
+ from distutils import core
+ setup = core.setup
+else:
+ import setuptools as dist
+ setup = dist.setup
+
+
+VERSION = '0.5.4' # Also change in pystache/__init__.py.
+
+FILE_ENCODING = 'utf-8'
+
+README_PATH = 'README.md'
+HISTORY_PATH = 'HISTORY.md'
+LICENSE_PATH = 'LICENSE'
+
+RST_DESCRIPTION_PATH = 'setup_description.rst'
+
+TEMP_EXTENSION = '.temp'
+
+PREP_COMMAND = 'prep'
+
+CLASSIFIERS = (
+ 'Development Status :: 4 - Beta',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.4',
+ 'Programming Language :: Python :: 2.5',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.1',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ '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):
+ """
+ Read and return the contents of a text file as a unicode string.
+
+ """
+ # This function implementation was chosen to be compatible across Python 2/3.
+ f = open(path, 'rb')
+ # We avoid use of the with keyword for Python 2.4 support.
+ try:
+ b = f.read()
+ finally:
+ f.close()
+
+ return b.decode(FILE_ENCODING)
+
+
+def write(u, path):
+ """
+ Write a unicode string to a file (as utf-8).
+
+ """
+ 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():
+ """
+ Generate the reST long_description for setup() from source files.
+
+ Returns the generated long_description as a unicode string.
+
+ """
+ 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 = [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)
+
+ 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 use the package simplejson for older Python versions since Python
+# does not contain the module json before 2.6:
+#
+# http://docs.python.org/library/json.html
+#
+# Moreover, simplejson stopped officially support for Python 2.4 in version 2.1.0:
+#
+# https://github.com/simplejson/simplejson/blob/master/CHANGES.txt
+#
+requires = []
+if py_version < (2, 5):
+ requires.append('simplejson<2.1')
+elif py_version < (2, 6):
+ requires.append('simplejson')
+
+INSTALL_REQUIRES = requires
+
+# TODO: decide whether to use find_packages() instead. I'm not sure that
+# find_packages() is available with distutils, for example.
+PACKAGES = [
+ 'pystache',
+ 'pystache.commands',
+ # The following packages are only for testing.
+ 'pystache.tests',
+ 'pystache.tests.data',
+ 'pystache.tests.data.locator',
+ 'pystache.tests.examples',
+]
+
+
+# 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):
+
+ # TODO: use the logging module instead of printing.
+ # TODO: include the following in a verbose mode.
+ sys.stderr.write("pystache: using: version %s of %s\n" % (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,
+ license='MIT',
+ description='Mustache for Python',
+ long_description=long_description,
+ 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,
+ package_data = {
+ # Include template files so tests can be run.
+ 'pystache.tests.data': template_files,
+ 'pystache.tests.data.locator': template_files,
+ 'pystache.tests.examples': template_files,
+ },
+ entry_points = {
+ 'console_scripts': [
+ 'pystache=pystache.commands.render:main',
+ 'pystache-test=pystache.commands.test:main',
+ ],
+ },
+ classifiers = CLASSIFIERS,
+ **extra_args
+ )
+
+
+if __name__=='__main__':
+ main(sys.argv)
diff --git a/python/pystache/setup_description.rst b/python/pystache/setup_description.rst
new file mode 100644
index 000000000..724c45723
--- /dev/null
+++ b/python/pystache/setup_description.rst
@@ -0,0 +1,513 @@
+.. 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:: http://defunkt.github.com/pystache/images/logo_phillips.png
+ :alt: mustachioed, monocled snake by David Phillips
+
+.. figure:: https://secure.travis-ci.org/defunkt/pystache.png
+ :alt: Travis CI current build status
+
+`Pystache <http://defunkt.github.com/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
+- Python 3.3
+- `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'
+
+Official support for Python 2.4 will end with Pystache version 0.6.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/>`__ is
+licensed under a `Creative Commons Attribution-ShareAlike 3.0 Unported
+License <http://creativecommons.org/licenses/by-sa/3.0/deed.en_US>`__.
+|image0|
+
+History
+=======
+
+**Note:** Official support for Python 2.4 will end with Pystache version
+0.6.0.
+
+0.5.4 (2014-07-11)
+------------------
+
+- Bugfix: made test with filenames OS agnostic (issue #162).
+
+0.5.3 (2012-11-03)
+------------------
+
+- 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 Python 3.3 to the list of supported versions.
+- 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).
+- Bugfix: allow ``test_pystache.py`` and ``tox`` to pass when run from
+ a downloaded sdist (i.e. without the spec test directory).
+- 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/python/pystache/test_pystache.py b/python/pystache/test_pystache.py
new file mode 100644
index 000000000..9a1a3ca26
--- /dev/null
+++ b/python/pystache/test_pystache.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+"""
+Runs project tests.
+
+This script is a substitute for running--
+
+ python -m pystache.commands.test
+
+It is useful in Python 2.4 because the -m flag does not accept subpackages
+in Python 2.4:
+
+ http://docs.python.org/using/cmdline.html#cmdoption-m
+
+"""
+
+import sys
+
+from pystache.commands import test
+from pystache.tests.main import FROM_SOURCE_OPTION
+
+
+def main(sys_argv=sys.argv):
+ sys.argv.insert(1, FROM_SOURCE_OPTION)
+ test.main()
+
+
+if __name__=='__main__':
+ main()
diff --git a/python/pystache/tox.ini b/python/pystache/tox.ini
new file mode 100644
index 000000000..d1eaebfbf
--- /dev/null
+++ b/python/pystache/tox.ini
@@ -0,0 +1,36 @@
+# A tox configuration file to test across multiple Python versions.
+#
+# http://pypi.python.org/pypi/tox
+#
+[tox]
+# Tox 1.4 drops py24 and adds py33. In the current version, we want to
+# support 2.4, so we can't simultaneously support 3.3.
+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
+# in the original location.
+changedir =
+ {envbindir}
+commands =
+ pystache-test {toxinidir}
+
+# Check that the spec tests work with PyYAML.
+[testenv:py27-yaml]
+basepython =
+ python2.7
+deps =
+ PyYAML
+changedir =
+ {envbindir}
+commands =
+ pystache-test {toxinidir}
+
+# Check that pystache-test works from an install with no arguments.
+[testenv:py27-noargs]
+basepython =
+ python2.7
+changedir =
+ {envbindir}
+commands =
+ pystache-test