diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /python/pystache | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'python/pystache')
29 files changed, 3702 insertions, 0 deletions
diff --git a/python/pystache/.gitignore b/python/pystache/.gitignore new file mode 100644 index 0000000000..758d62df92 --- /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 0000000000..c55c8e5e3b --- /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 0000000000..00227053aa --- /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 0000000000..e5b7638aee --- /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 0000000000..42be9d6460 --- /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 0000000000..bdc64bf718 --- /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 0000000000..54a96088b4 --- /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 0000000000..cd82417657 --- /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 Binary files differnew file mode 100644 index 0000000000..7491901366 --- /dev/null +++ b/python/pystache/gh/images/logo_phillips.png diff --git a/python/pystache/pystache/__init__.py b/python/pystache/pystache/__init__.py new file mode 100644 index 0000000000..4cf24344e5 --- /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 0000000000..a0d386a38c --- /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 0000000000..1a9c309d52 --- /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 0000000000..0872453388 --- /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 0000000000..fb266dd8b5 --- /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 0000000000..6715916092 --- /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 0000000000..bcfdf4cd3a --- /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 0000000000..38bb1f5a0e --- /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 0000000000..d4a7e5310f --- /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 0000000000..30c5b01e01 --- /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 0000000000..372d96c666 --- /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 0000000000..9a4fba235b --- /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 0000000000..c797b1765a --- /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 0000000000..ff6a90c64b --- /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 0000000000..3a77d4c528 --- /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 0000000000..9e9f454c19 --- /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 0000000000..0d99aae8fb --- /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 0000000000..724c457233 --- /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 0000000000..9a1a3ca26d --- /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 0000000000..d1eaebfbfc --- /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 |