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