summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2012-04-03 18:42:21 -0700
committerChris Jerdonek <chris.jerdonek@gmail.com>2012-04-03 18:42:21 -0700
commita4d763d54d83d4cbc9c254c897165563b119f7e8 (patch)
treeee134c32eec215ac28dcb00d0748dcb0bb9887be
parenta06fd4233d55ea317ea03b81c98b149e3d5b1364 (diff)
parent368f0dfd2f61b0e4a92d530e033eaec4a6fcfeb9 (diff)
downloadpystache-a4d763d54d83d4cbc9c254c897165563b119f7e8.tar.gz
Merge branch 'development' to 'master': staging version 0.5.0-rc.
-rw-r--r--.gitmodules3
-rw-r--r--HISTORY.rst51
-rw-r--r--LICENSE1
-rw-r--r--README.rst140
-rw-r--r--examples/comments.mustache2
-rw-r--r--examples/comments.py5
-rw-r--r--examples/complex.mustache6
-rw-r--r--examples/complex.py (renamed from examples/complex_view.py)7
-rw-r--r--examples/complex_view.mustache1
-rw-r--r--examples/delimiters.py5
-rw-r--r--examples/double_section.py5
-rw-r--r--examples/escaped.py5
-rw-r--r--examples/extensionless1
-rw-r--r--examples/inner_partial.txt2
-rw-r--r--examples/inverted.py9
-rw-r--r--examples/lambdas.py8
-rw-r--r--examples/nested_context.py15
-rw-r--r--examples/partials_with_lambdas.py6
-rw-r--r--examples/readme.py3
-rw-r--r--examples/say_hello.mustache1
-rw-r--r--examples/simple.py5
-rw-r--r--examples/tagless.mustache1
-rw-r--r--examples/template_partial.py15
-rw-r--r--examples/unescaped.py5
-rw-r--r--examples/unicode_input.mustache2
-rw-r--r--examples/unicode_input.py6
-rw-r--r--examples/unicode_output.py5
m---------ext/spec0
-rw-r--r--pystache/__init__.py10
-rw-r--r--pystache/commands.py81
-rw-r--r--pystache/context.py264
-rw-r--r--pystache/defaults.py50
-rw-r--r--pystache/init.py21
-rw-r--r--pystache/loader.py190
-rw-r--r--pystache/locator.py152
-rw-r--r--pystache/parsed.py49
-rw-r--r--pystache/parser.py197
-rw-r--r--pystache/renderengine.py236
-rw-r--r--pystache/renderer.py336
-rw-r--r--pystache/spec_loader.py87
-rw-r--r--pystache/template.py181
-rw-r--r--pystache/template_spec.py43
-rw-r--r--pystache/view.py94
-rw-r--r--setup.cfg3
-rw-r--r--setup.py84
-rw-r--r--tests/__init__.py0
-rwxr-xr-xtests/benchmark.py94
-rw-r--r--tests/common.py53
-rw-r--r--tests/data/__init__.py0
-rw-r--r--tests/data/ascii.mustache1
-rw-r--r--tests/data/duplicate.mustache1
-rw-r--r--tests/data/locator/duplicate.mustache1
-rw-r--r--tests/data/non_ascii.mustache1
-rw-r--r--tests/data/sample_view.mustache1
-rw-r--r--tests/data/say_hello.mustache1
-rw-r--r--tests/data/views.py16
-rw-r--r--tests/test_commands.py45
-rw-r--r--tests/test_context.py400
-rw-r--r--tests/test_examples.py90
-rw-r--r--tests/test_loader.py217
-rw-r--r--tests/test_locator.py150
-rw-r--r--tests/test_pystache.py93
-rw-r--r--tests/test_renderengine.py455
-rw-r--r--tests/test_renderer.py556
-rw-r--r--tests/test_simple.py94
-rw-r--r--tests/test_spec.py99
-rw-r--r--tests/test_template_spec.py388
-rw-r--r--tests/test_view.py112
68 files changed, 4603 insertions, 658 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..c55c8e5
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ext/spec"]
+ path = ext/spec
+ url = http://github.com/mustache/spec.git
diff --git a/HISTORY.rst b/HISTORY.rst
index 6e8f1a1..dcf8a99 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,6 +1,53 @@
History
=======
+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]
@@ -44,3 +91,7 @@ History
------------------
* First release
+
+
+.. _issue #13: https://github.com/defunkt/pystache/issues/13
+.. _Mustache spec: https://github.com/mustache/spec
diff --git a/LICENSE b/LICENSE
index 2745bcc..1943585 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,4 @@
+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/README.rst b/README.rst
index 46b436d..9781fc2 100644
--- a/README.rst
+++ b/README.rst
@@ -4,27 +4,38 @@ Pystache
.. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png
-Inspired by ctemplate_ and et_, Mustache_ is a
-framework-agnostic way to render logic-free views.
+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."
-As ctemplates says, "It 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 a Python implementation of Mustache. Pystache works on--
+Pystache is `semantically versioned`_ and can be found on PyPI_. This
+version of Pystache passes all tests in `version 1.0.3`_ of the spec.
-* Python 2.4
-* Python 2.5
-* Python 2.6
-* Python 2.7
+Logo: `David Phillips`_
-Pystache is semantically versioned: http://semver.org.
-Logo: David Phillips - http://davidphillips.us/
+Requirements
+============
-Documentation
-=============
+Pystache is tested with the following versions of Python:
+
+* Python 2.4 (requires simplejson version 2.0.9 or earlier)
+* Python 2.5 (requires simplejson)
+* Python 2.6
+* Python 2.7
+
+JSON support is needed only for the command-line interface and to run the
+spec tests. Python's json_ module is new as of Python 2.6. Python's
+simplejson_ package works with earlier versions of Python. Because
+simplejson stopped officially supporting Python 2.4 as of version 2.1.0,
+Python 2.4 requires an earlier version.
-The different Mustache tags are documented at `mustache(5)`_.
Install It
==========
@@ -41,25 +52,64 @@ Use It
>>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
- 'Hi Mom!'
+ u'Hi Mom!'
You can also create dedicated view classes to hold your view logic.
-Here's your simple.py::
+Here's your view class (in examples/readme.py)::
+
+ class SayHello(object):
- import pystache
- class Simple(pystache.View):
- def thing(self):
- return "pizza"
+ def to(self):
+ return "Pizza"
-Then your template, simple.mustache::
+Like so::
- Hi {{thing}}!
+ >>> from examples.readme import SayHello
+ >>> hello = SayHello()
+
+Then your template, say_hello.mustache::
+
+ Hello, {{to}}!
Pull it together::
- >>> Simple().render()
- 'Hi pizza!'
+ >>> renderer = pystache.Renderer()
+ >>> renderer.render(hello)
+ u'Hello, Pizza!'
+
+
+Unicode Handling
+================
+
+This section describes Pystache's handling of unicode (e.g. strings and
+encodings).
+
+Internally, Pystache uses `only unicode strings`_. For input, Pystache accepts
+both ``unicode`` and ``str`` strings. For output, Pystache's template
+rendering methods return only unicode.
+
+Pystache's ``Renderer`` class supports a number of attributes that control how
+Pystache converts ``str`` strings to unicode on input. These include the
+``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes.
+
+The ``file_encoding`` attribute is the encoding the renderer uses to convert
+to unicode any files read from the file system. Similarly, ``string_encoding``
+is the encoding the renderer uses to convert to unicode any other strings of
+type ``str`` encountered during the rendering process (e.g. context values
+of type ``str``).
+
+The ``decode_errors`` attribute is what the renderer passes as the ``errors``
+argument to Python's `built-in unicode function`_ ``unicode()`` when
+converting. The valid values for this argument are ``strict``, ``ignore``,
+and ``replace``.
+
+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.
Test It
@@ -76,27 +126,57 @@ to type, for example ::
nosetests-2.4
+To include tests from the Mustache spec in your test runs: ::
+
+ git submodule init
+ git submodule update
+
+To run all available tests (including doctests)::
+
+ nosetests --with-doctest --doctest-extension=rst
+
+or alternatively (using setup.cfg)::
+
+ python setup.py nosetests
+
+To run a subset of the tests, you can use this pattern, for example: ::
+
+ nosetests --tests tests/test_context.py:GetValueTests.test_dictionary__key_present
+
Mailing List
-==================
-As of Nov 26, 2011, there's a mailing list, pystache@librelist.com.
+============
+
+As of November 2011, there's a mailing list, pystache@librelist.com.
Archive: http://librelist.com/browser/pystache/
Note: There's a bit of a delay in seeing the latest emails appear
in the archive.
+
Author
======
::
- context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
- pystache.render("{{author}} :: {{email}}", context)
+ >>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
+ >>> pystache.render("{{author}} :: {{email}}", context)
+ u'Chris Wanstrath :: chris@ozmm.org'
.. _ctemplate: http://code.google.com/p/google-ctemplate/
+.. _David Phillips: http://davidphillips.us/
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
-.. _Mustache: http://defunkt.github.com/mustache/
+.. _json: http://docs.python.org/library/json.html
+.. _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 \ No newline at end of file
+.. _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
+.. _semantically versioned: http://semver.org
+.. _simplejson: http://pypi.python.org/pypi/simplejson/
+.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode
+.. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7
diff --git a/examples/comments.mustache b/examples/comments.mustache
index 8bdfb5e..2a2a08b 100644
--- a/examples/comments.mustache
+++ b/examples/comments.mustache
@@ -1 +1 @@
-<h1>{{title}}{{! just something interesting... #or not... }}</h1>
+<h1>{{title}}{{! just something interesting... #or not... }}</h1> \ No newline at end of file
diff --git a/examples/comments.py b/examples/comments.py
index 1d2ed0b..f9c3125 100644
--- a/examples/comments.py
+++ b/examples/comments.py
@@ -1,7 +1,4 @@
-import pystache
-
-class Comments(pystache.View):
- template_path = 'examples'
+class Comments(object):
def title(self):
return "A Comedy of Errors"
diff --git a/examples/complex.mustache b/examples/complex.mustache
new file mode 100644
index 0000000..6de758b
--- /dev/null
+++ b/examples/complex.mustache
@@ -0,0 +1,6 @@
+<h1>{{ header }}</h1>
+{{#list}}
+<ul>
+{{#item}}{{# current }}<li><strong>{{name}}</strong></li>
+{{/ current }}{{#link}}<li><a href="{{url}}">{{name}}</a></li>
+{{/link}}{{/item}}</ul>{{/list}}{{#empty}}<p>The list is empty.</p>{{/empty}} \ No newline at end of file
diff --git a/examples/complex_view.py b/examples/complex.py
index ef45ff7..e3f1767 100644
--- a/examples/complex_view.py
+++ b/examples/complex.py
@@ -1,7 +1,4 @@
-import pystache
-
-class ComplexView(pystache.View):
- template_path = 'examples'
+class Complex(object):
def header(self):
return "Colors"
@@ -18,6 +15,6 @@ class ComplexView(pystache.View):
def empty(self):
return len(self.item()) == 0
-
+
def empty_list(self):
return [];
diff --git a/examples/complex_view.mustache b/examples/complex_view.mustache
deleted file mode 100644
index 45ea706..0000000
--- a/examples/complex_view.mustache
+++ /dev/null
@@ -1 +0,0 @@
-<h1>{{ header }}</h1>{{#list}}<ul>{{#item}}{{# current }}<li><strong>{{name}}</strong></li>{{/ current }}{{#link}}<li><a href="{{url}}">{{name}}</a></li>{{/link}}{{/item}}</ul>{{/list}}{{#empty}}<p>The list is empty.</p>{{/empty}} \ No newline at end of file
diff --git a/examples/delimiters.py b/examples/delimiters.py
index 53f65f2..a132ed0 100644
--- a/examples/delimiters.py
+++ b/examples/delimiters.py
@@ -1,7 +1,4 @@
-import pystache
-
-class Delimiters(pystache.View):
- template_path = 'examples'
+class Delimiters(object):
def first(self):
return "It worked the first time."
diff --git a/examples/double_section.py b/examples/double_section.py
index 7085739..0bec602 100644
--- a/examples/double_section.py
+++ b/examples/double_section.py
@@ -1,7 +1,4 @@
-import pystache
-
-class DoubleSection(pystache.View):
- template_path = 'examples'
+class DoubleSection(object):
def t(self):
return True
diff --git a/examples/escaped.py b/examples/escaped.py
index ddaad4f..fed1705 100644
--- a/examples/escaped.py
+++ b/examples/escaped.py
@@ -1,7 +1,4 @@
-import pystache
-
-class Escaped(pystache.View):
- template_path = 'examples'
+class Escaped(object):
def title(self):
return "Bear > Shark"
diff --git a/examples/extensionless b/examples/extensionless
new file mode 100644
index 0000000..452c9fe
--- /dev/null
+++ b/examples/extensionless
@@ -0,0 +1 @@
+No file extension: {{foo}} \ No newline at end of file
diff --git a/examples/inner_partial.txt b/examples/inner_partial.txt
index 3d1396e..650c959 100644
--- a/examples/inner_partial.txt
+++ b/examples/inner_partial.txt
@@ -1 +1 @@
-## Again, {{title}}! ##
+## Again, {{title}}! ## \ No newline at end of file
diff --git a/examples/inverted.py b/examples/inverted.py
index e0f7aaf..2a05302 100644
--- a/examples/inverted.py
+++ b/examples/inverted.py
@@ -1,7 +1,6 @@
-import pystache
+from pystache import TemplateSpec
-class Inverted(pystache.View):
- template_path = 'examples'
+class Inverted(object):
def t(self):
return True
@@ -14,11 +13,11 @@ class Inverted(pystache.View):
def empty_list(self):
return []
-
+
def populated_list(self):
return ['some_value']
-class InvertedLists(Inverted):
+class InvertedLists(Inverted, TemplateSpec):
template_name = 'inverted'
def t(self):
diff --git a/examples/lambdas.py b/examples/lambdas.py
index 01a3697..653531d 100644
--- a/examples/lambdas.py
+++ b/examples/lambdas.py
@@ -1,4 +1,4 @@
-import pystache
+from pystache import TemplateSpec
def rot(s, n=13):
r = ""
@@ -17,8 +17,10 @@ def rot(s, n=13):
def replace(subject, this='foo', with_this='bar'):
return subject.replace(this, with_this)
-class Lambdas(pystache.View):
- template_path = 'examples'
+
+# This class subclasses TemplateSpec because at least one unit test
+# sets the template attribute.
+class Lambdas(TemplateSpec):
def replace_foo_with_bar(self, text=None):
return replace
diff --git a/examples/nested_context.py b/examples/nested_context.py
index 83565a1..4626ac0 100644
--- a/examples/nested_context.py
+++ b/examples/nested_context.py
@@ -1,7 +1,12 @@
-import pystache
+from pystache import TemplateSpec
-class NestedContext(pystache.View):
- template_path = 'examples'
+class NestedContext(TemplateSpec):
+
+ def __init__(self, renderer):
+ self.renderer = renderer
+
+ def _context_get(self, key):
+ return self.renderer.context.get(key)
def outer_thing(self):
return "two"
@@ -16,6 +21,6 @@ class NestedContext(pystache.View):
return [{'outer': 'car'}]
def nested_context_in_view(self):
- if self.get('outer') == self.get('inner'):
+ if self._context_get('outer') == self._context_get('inner'):
return 'it works!'
- return '' \ No newline at end of file
+ return ''
diff --git a/examples/partials_with_lambdas.py b/examples/partials_with_lambdas.py
index 4c3ee97..463a3ce 100644
--- a/examples/partials_with_lambdas.py
+++ b/examples/partials_with_lambdas.py
@@ -1,8 +1,6 @@
-import pystache
from examples.lambdas import rot
-class PartialsWithLambdas(pystache.View):
- template_path = 'examples'
-
+class PartialsWithLambdas(object):
+
def rot(self):
return rot \ No newline at end of file
diff --git a/examples/readme.py b/examples/readme.py
new file mode 100644
index 0000000..23b44f5
--- /dev/null
+++ b/examples/readme.py
@@ -0,0 +1,3 @@
+class SayHello(object):
+ def to(self):
+ return "Pizza"
diff --git a/examples/say_hello.mustache b/examples/say_hello.mustache
new file mode 100644
index 0000000..7d8dfea
--- /dev/null
+++ b/examples/say_hello.mustache
@@ -0,0 +1 @@
+Hello, {{to}}! \ No newline at end of file
diff --git a/examples/simple.py b/examples/simple.py
index 792b243..3252a81 100644
--- a/examples/simple.py
+++ b/examples/simple.py
@@ -1,7 +1,6 @@
-import pystache
+from pystache import TemplateSpec
-class Simple(pystache.View):
- template_path = 'examples'
+class Simple(TemplateSpec):
def thing(self):
return "pizza"
diff --git a/examples/tagless.mustache b/examples/tagless.mustache
new file mode 100644
index 0000000..ad4dd31
--- /dev/null
+++ b/examples/tagless.mustache
@@ -0,0 +1 @@
+No tags... \ No newline at end of file
diff --git a/examples/template_partial.py b/examples/template_partial.py
index 651cbe5..e96c83b 100644
--- a/examples/template_partial.py
+++ b/examples/template_partial.py
@@ -1,7 +1,12 @@
-import pystache
+from pystache import TemplateSpec
-class TemplatePartial(pystache.View):
- template_path = 'examples'
+class TemplatePartial(TemplateSpec):
+
+ def __init__(self, renderer):
+ self.renderer = renderer
+
+ def _context_get(self, key):
+ return self.renderer.context.get(key)
def title(self):
return "Welcome"
@@ -11,6 +16,6 @@ class TemplatePartial(pystache.View):
def looping(self):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
-
+
def thing(self):
- return self['prop'] \ No newline at end of file
+ return self._context_get('prop') \ No newline at end of file
diff --git a/examples/unescaped.py b/examples/unescaped.py
index a040dcb..67c12ca 100644
--- a/examples/unescaped.py
+++ b/examples/unescaped.py
@@ -1,7 +1,4 @@
-import pystache
-
-class Unescaped(pystache.View):
- template_path = 'examples'
+class Unescaped(object):
def title(self):
return "Bear > Shark"
diff --git a/examples/unicode_input.mustache b/examples/unicode_input.mustache
index 9b5e335..f654cd1 100644
--- a/examples/unicode_input.mustache
+++ b/examples/unicode_input.mustache
@@ -1 +1 @@
-<p>If alive today, Henri Poincaré would be {{age}} years old.</p> \ No newline at end of file
+abcdé \ No newline at end of file
diff --git a/examples/unicode_input.py b/examples/unicode_input.py
index 9f3684f..2c10fcb 100644
--- a/examples/unicode_input.py
+++ b/examples/unicode_input.py
@@ -1,7 +1,7 @@
-import pystache
+from pystache import TemplateSpec
+
+class UnicodeInput(TemplateSpec):
-class UnicodeInput(pystache.View):
- template_path = 'examples'
template_encoding = 'utf8'
def age(self):
diff --git a/examples/unicode_output.py b/examples/unicode_output.py
index 3cb9260..d5579c3 100644
--- a/examples/unicode_output.py
+++ b/examples/unicode_output.py
@@ -1,9 +1,6 @@
# encoding: utf-8
-import pystache
-
-class UnicodeOutput(pystache.View):
- template_path = 'examples'
+class UnicodeOutput(object):
def name(self):
return u'Henri Poincaré'
diff --git a/ext/spec b/ext/spec
new file mode 160000
+Subproject 48c933b0bb780875acbfd15816297e263c53d6f
diff --git a/pystache/__init__.py b/pystache/__init__.py
index 314c5c8..daf7f52 100644
--- a/pystache/__init__.py
+++ b/pystache/__init__.py
@@ -1,8 +1,2 @@
-from pystache.template import Template
-from pystache.view import View
-from pystache.loader import Loader
-
-def render(template, context=None, **kwargs):
- context = context and context.copy() or {}
- context.update(kwargs)
- return Template(template, context).render()
+# We keep all initialization code in a separate module.
+from init import *
diff --git a/pystache/commands.py b/pystache/commands.py
new file mode 100644
index 0000000..1801d40
--- /dev/null
+++ b/pystache/commands.py
@@ -0,0 +1,81 @@
+# coding: utf-8
+
+"""
+This module provides command-line access to pystache.
+
+Run this script using the -h option for command-line help.
+
+"""
+
+
+try:
+ import json
+except:
+ # The json module is new in Python 2.6, whereas simplejson is
+ # compatible with earlier versions.
+ import simplejson as json
+
+# The optparse module is deprecated in Python 2.7 in favor of argparse.
+# However, argparse is not available in Python 2.6 and earlier.
+from optparse import OptionParser
+import sys
+
+# We use absolute imports here to allow use of this script from its
+# location in source control (e.g. for development purposes).
+# Otherwise, the following error occurs:
+#
+# ValueError: Attempted relative import in non-package
+#
+from pystache.renderer import Renderer
+
+
+USAGE = """\
+%prog [-h] template context
+
+Render a mustache template with the given context.
+
+positional arguments:
+ template A filename or template string.
+ context A filename or JSON string."""
+
+
+def parse_args(sys_argv, usage):
+ """
+ Return an OptionParser for the script.
+
+ """
+ args = sys_argv[1:]
+
+ parser = OptionParser(usage=usage)
+ options, args = parser.parse_args(args)
+
+ template, context = args
+
+ return template, context
+
+
+def main(sys_argv):
+ template, context = parse_args(sys_argv, USAGE)
+
+ if template.endswith('.mustache'):
+ template = template[:-9]
+
+ renderer = Renderer()
+
+ try:
+ template = renderer.load_template(template)
+ except IOError:
+ pass
+
+ try:
+ context = json.load(open(context))
+ except IOError:
+ context = json.loads(context)
+
+ rendered = renderer.render(template, context)
+ print rendered
+
+
+if __name__=='__main__':
+ main(sys.argv)
+
diff --git a/pystache/context.py b/pystache/context.py
new file mode 100644
index 0000000..1621d61
--- /dev/null
+++ b/pystache/context.py
@@ -0,0 +1,264 @@
+# coding: utf-8
+
+"""
+Defines a Context class to represent mustache(5)'s notion of context.
+
+"""
+
+class NotFound(object): pass
+# We use this private global variable as a return value to represent a key
+# not being found on lookup. This lets us distinguish between the case
+# of a key's value being None with the case of a key not being found --
+# without having to rely on exceptions (e.g. KeyError) for flow control.
+_NOT_FOUND = NotFound()
+
+
+# TODO: share code with template.check_callable().
+def _is_callable(obj):
+ return hasattr(obj, '__call__')
+
+
+def _get_value(item, key):
+ """
+ Retrieve a key's value from an item.
+
+ Returns _NOT_FOUND if the key does not exist.
+
+ The Context.get() docstring documents this function's intended behavior.
+
+ """
+ if isinstance(item, dict):
+ # Then we consider the argument a "hash" for the purposes of the spec.
+ #
+ # We do a membership test to avoid using exceptions for flow control
+ # (e.g. catching KeyError).
+ if key in item:
+ return item[key]
+ elif type(item).__module__ != '__builtin__':
+ # Then we consider the argument an "object" for the purposes of
+ # the spec.
+ #
+ # The elif test above lets us avoid treating instances of built-in
+ # types like integers and strings as objects (cf. issue #81).
+ # Instances of user-defined classes on the other hand, for example,
+ # are considered objects by the test above.
+ if hasattr(item, key):
+ attr = getattr(item, key)
+ if _is_callable(attr):
+ return attr()
+ return attr
+
+ return _NOT_FOUND
+
+
+class Context(object):
+
+ """
+ Provides dictionary-like access to a stack of zero or more items.
+
+ Instances of this class are meant to act as the rendering context
+ when rendering Mustache templates in accordance with mustache(5)
+ and the Mustache spec.
+
+ Instances encapsulate a private stack of hashes, objects, and built-in
+ type instances. Querying the stack for the value of a key queries
+ the items in the stack in order from last-added objects to first
+ (last in, first out).
+
+ Caution: this class does not currently support recursive nesting in
+ that items in the stack cannot themselves be Context instances.
+
+ See the docstrings of the methods of this class for more details.
+
+ """
+
+ # We reserve keyword arguments for future options (e.g. a "strict=True"
+ # option for enabling a strict mode).
+ def __init__(self, *items):
+ """
+ Construct an instance, and initialize the private stack.
+
+ The *items arguments are the items with which to populate the
+ initial stack. Items in the argument list are added to the
+ stack in order so that, in particular, items at the end of
+ the argument list are queried first when querying the stack.
+
+ Caution: items should not themselves be Context instances, as
+ recursive nesting does not behave as one might expect.
+
+ """
+ self._stack = list(items)
+
+ def __repr__(self):
+ """
+ Return a string representation of the instance.
+
+ For example--
+
+ >>> context = Context({'alpha': 'abc'}, {'numeric': 123})
+ >>> repr(context)
+ "Context({'alpha': 'abc'}, {'numeric': 123})"
+
+ """
+ return "%s%s" % (self.__class__.__name__, tuple(self._stack))
+
+ @staticmethod
+ def create(*context, **kwargs):
+ """
+ Build a Context instance from a sequence of context-like items.
+
+ This factory-style method is more general than the Context class's
+ constructor in that, unlike the constructor, the argument list
+ can itself contain Context instances.
+
+ Here is an example illustrating various aspects of this method:
+
+ >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
+ >>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'})
+ >>>
+ >>> context = Context.create(obj1, None, obj2, mineral='gold')
+ >>>
+ >>> context.get('animal')
+ 'cat'
+ >>> context.get('vegetable')
+ 'spinach'
+ >>> context.get('mineral')
+ 'gold'
+
+ Arguments:
+
+ *context: zero or more dictionaries, Context instances, or objects
+ with which to populate the initial context stack. None
+ arguments will be skipped. Items in the *context list are
+ added to the stack in order so that later items in the argument
+ list take precedence over earlier items. This behavior is the
+ same as the constructor's.
+
+ **kwargs: additional key-value data to add to the context stack.
+ As these arguments appear after all items in the *context list,
+ in the case of key conflicts these values take precedence over
+ all items in the *context list. This behavior is the same as
+ the constructor's.
+
+ """
+ items = context
+
+ context = Context()
+
+ for item in items:
+ if item is None:
+ continue
+ if isinstance(item, Context):
+ context._stack.extend(item._stack)
+ else:
+ context.push(item)
+
+ if kwargs:
+ context.push(kwargs)
+
+ return context
+
+ def get(self, key, default=None):
+ """
+ Query the stack for the given key, and return the resulting value.
+
+ This method queries items in the stack in order from last-added
+ objects to first (last in, first out). The value returned is
+ the value of the key in the first item that contains the key.
+ If the key is not found in any item in the stack, then the default
+ value is returned. The default value defaults to None.
+
+ When speaking about returning values from a context, the Mustache
+ spec distinguishes between two types of context stack elements:
+ hashes and objects.
+
+ In accordance with the spec, this method queries items in the
+ stack for a key in the following way. For the purposes of querying,
+ each item is classified into one of the following three mutually
+ exclusive categories: a hash, an object, or neither:
+
+ (1) Hash: if the item's type is a subclass of dict, then the item
+ is considered a hash (in the terminology of the spec), and
+ the key's value is the dictionary value of the key. If the
+ dictionary doesn't contain the key, the key is not found.
+
+ (2) Object: if the item isn't a hash and isn't an instance of a
+ built-in type, then the item is considered an object (again
+ using the language of the spec). In this case, the method
+ looks for an attribute with the same name as the key. If an
+ attribute with that name exists, the value of the attribute is
+ returned. If the attribute is callable, however (i.e. if the
+ attribute is a method), then the attribute is called with no
+ arguments and instead that value returned. If there is no
+ attribute with the same name as the key, then the key is
+ considered not found.
+
+ (3) Neither: if the item is neither a hash nor an object, then
+ the key is considered not found.
+
+ *Caution*:
+
+ Callables are handled differently depending on whether they are
+ dictionary values, as in (1) above, or attributes, as in (2).
+ The former are returned as-is, while the latter are first
+ called and that value returned.
+
+ Here is an example to illustrate:
+
+ >>> def greet():
+ ... return "Hi Bob!"
+ >>>
+ >>> class Greeter(object):
+ ... greet = None
+ >>>
+ >>> dct = {'greet': greet}
+ >>> obj = Greeter()
+ >>> obj.greet = greet
+ >>>
+ >>> dct['greet'] is obj.greet
+ True
+ >>> Context(dct).get('greet') #doctest: +ELLIPSIS
+ <function greet at 0x...>
+ >>> Context(obj).get('greet')
+ 'Hi Bob!'
+
+ TODO: explain the rationale for this difference in treatment.
+
+ """
+ for obj in reversed(self._stack):
+ val = _get_value(obj, key)
+ if val is _NOT_FOUND:
+ continue
+ # Otherwise, the key was found.
+ return val
+ # Otherwise, no item in the stack contained the key.
+
+ return default
+
+ def push(self, item):
+ """
+ Push an item onto the stack.
+
+ """
+ self._stack.append(item)
+
+ def pop(self):
+ """
+ Pop an item off of the stack, and return it.
+
+ """
+ return self._stack.pop()
+
+ def top(self):
+ """
+ Return the item last added to the stack.
+
+ """
+ return self._stack[-1]
+
+ def copy(self):
+ """
+ Return a copy of this instance.
+
+ """
+ return Context(*self._stack)
diff --git a/pystache/defaults.py b/pystache/defaults.py
new file mode 100644
index 0000000..b696410
--- /dev/null
+++ b/pystache/defaults.py
@@ -0,0 +1,50 @@
+# coding: utf-8
+
+"""
+This module provides a central location for defining default behavior.
+
+Throughout the package, these defaults take effect only when the user
+does not otherwise specify a value.
+
+"""
+
+import cgi
+import os
+import sys
+
+
+# How to handle encoding errors when decoding strings from str to unicode.
+#
+# This value is passed as the "errors" argument to Python's built-in
+# unicode() function:
+#
+# http://docs.python.org/library/functions.html#unicode
+#
+DECODE_ERRORS = 'strict'
+
+# The name of the encoding to use when converting to unicode any strings of
+# type str encountered during the rendering process.
+STRING_ENCODING = sys.getdefaultencoding()
+
+# The name of the encoding to use when converting file contents to unicode.
+# This default takes precedence over the STRING_ENCODING default for
+# strings that arise from files.
+FILE_ENCODING = sys.getdefaultencoding()
+
+# The starting list of directories in which to search for templates when
+# loading a template by file name.
+SEARCH_DIRS = [os.curdir] # i.e. ['.']
+
+# The escape function to apply to strings that require escaping when
+# rendering templates (e.g. for tags enclosed in double braces).
+# Only unicode strings will be passed to this function.
+#
+# The quote=True argument causes double quotes to be escaped,
+# but not single quotes:
+#
+# http://docs.python.org/library/cgi.html#cgi.escape
+#
+TAG_ESCAPE = lambda u: cgi.escape(u, quote=True)
+
+# The default template extension.
+TEMPLATE_EXTENSION = 'mustache'
diff --git a/pystache/init.py b/pystache/init.py
new file mode 100644
index 0000000..b285a5c
--- /dev/null
+++ b/pystache/init.py
@@ -0,0 +1,21 @@
+# encoding: utf-8
+
+"""
+This module contains the initialization logic called by __init__.py.
+
+"""
+
+from pystache.renderer import Renderer
+from pystache.template_spec import TemplateSpec
+
+
+__all__ = ['render', 'Renderer', 'TemplateSpec']
+
+
+def render(template, context=None, **kwargs):
+ """
+ Return the given template string rendered using the given context.
+
+ """
+ renderer = Renderer()
+ return renderer.render(template, context, **kwargs)
diff --git a/pystache/loader.py b/pystache/loader.py
index 63b7ee6..bcba71b 100644
--- a/pystache/loader.py
+++ b/pystache/loader.py
@@ -1,47 +1,155 @@
+# coding: utf-8
+
+"""
+This module provides a Loader class for locating and reading templates.
+
+"""
+
import os
+import sys
+
+from pystache import defaults
+from pystache.locator import Locator
+
+
+def _to_unicode(s, encoding=None):
+ """
+ Raises a TypeError exception if the given string is already unicode.
+
+ """
+ if encoding is None:
+ encoding = defaults.STRING_ENCODING
+ return unicode(s, encoding, defaults.DECODE_ERRORS)
+
class Loader(object):
-
- template_extension = 'mustache'
- template_path = '.'
- template_encoding = None
-
- def load_template(self, template_name, template_dirs=None, encoding=None, extension=None):
- '''Returns the template string from a file or throws IOError if it non existent'''
- if None == template_dirs:
- template_dirs = self.template_path
-
- if encoding is not None:
- self.template_encoding = encoding
-
- if extension is not None:
- self.template_extension = extension
-
- file_name = template_name + '.' + self.template_extension
-
- # Given a single directory we'll load from it
- if isinstance(template_dirs, basestring):
- file_path = os.path.join(template_dirs, file_name)
-
- return self._load_template_file(file_path)
-
- # Given a list of directories we'll check each for our file
- for path in template_dirs:
- file_path = os.path.join(path, file_name)
- if os.path.exists(file_path):
- return self._load_template_file(file_path)
-
- raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),))
-
- def _load_template_file(self, file_path):
- '''Loads and returns the template file from disk'''
- f = open(file_path, 'r')
-
+
+ """
+ Loads the template associated to a name or user-defined object.
+
+ """
+
+ def __init__(self, file_encoding=None, extension=None, to_unicode=None,
+ search_dirs=None):
+ """
+ Construct a template loader instance.
+
+ Arguments:
+
+ extension: the template file extension. Pass False for no
+ extension (i.e. to use extensionless template files).
+ Defaults to the package default.
+
+ file_encoding: the name of the encoding to use when converting file
+ contents to unicode. Defaults to the package default.
+
+ search_dirs: the list of directories in which to search when loading
+ a template by name or file name. Defaults to the package default.
+
+ to_unicode: the function to use when converting strings of type
+ str to unicode. The function should have the signature:
+
+ to_unicode(s, encoding=None)
+
+ It should accept a string of type str and an optional encoding
+ name and return a string of type unicode. Defaults to calling
+ Python's built-in function unicode() using the package string
+ encoding and decode errors defaults.
+
+ """
+ if extension is None:
+ extension = defaults.TEMPLATE_EXTENSION
+
+ if file_encoding is None:
+ file_encoding = defaults.FILE_ENCODING
+
+ if search_dirs is None:
+ search_dirs = defaults.SEARCH_DIRS
+
+ if to_unicode is None:
+ to_unicode = _to_unicode
+
+ self.extension = extension
+ self.file_encoding = file_encoding
+ # TODO: unit test setting this attribute.
+ self.search_dirs = search_dirs
+ self.to_unicode = to_unicode
+
+ def _make_locator(self):
+ return Locator(extension=self.extension)
+
+ def unicode(self, s, encoding=None):
+ """
+ Convert a string to unicode using the given encoding, and return it.
+
+ This function uses the underlying to_unicode attribute.
+
+ Arguments:
+
+ s: a basestring instance to convert to unicode. Unlike Python's
+ built-in unicode() function, it is okay to pass unicode strings
+ to this function. (Passing a unicode string to Python's unicode()
+ with the encoding argument throws the error, "TypeError: decoding
+ Unicode is not supported.")
+
+ encoding: the encoding to pass to the to_unicode attribute.
+ Defaults to None.
+
+ """
+ if isinstance(s, unicode):
+ return unicode(s)
+
+ return self.to_unicode(s, encoding)
+
+ def read(self, path, encoding=None):
+ """
+ Read the template at the given path, and return it as a unicode string.
+
+ """
+ # We avoid use of the with keyword for Python 2.4 support.
+ f = open(path, 'r')
try:
- template = f.read()
- if self.template_encoding:
- template = unicode(template, self.template_encoding)
+ text = f.read()
finally:
f.close()
-
- return template \ No newline at end of file
+
+ if encoding is None:
+ encoding = self.file_encoding
+
+ return self.unicode(text, encoding)
+
+ # TODO: unit-test this method.
+ def load_name(self, name):
+ """
+ Find and return the template with the given name.
+
+ Arguments:
+
+ name: the name of the template.
+
+ search_dirs: the list of directories in which to search.
+
+ """
+ locator = self._make_locator()
+
+ path = locator.find_name(name, self.search_dirs)
+
+ return self.read(path)
+
+ # TODO: unit-test this method.
+ def load_object(self, obj):
+ """
+ Find and return the template associated to the given object.
+
+ Arguments:
+
+ obj: an instance of a user-defined class.
+
+ search_dirs: the list of directories in which to search.
+
+ """
+ locator = self._make_locator()
+
+ path = locator.find_object(obj, self.search_dirs)
+
+ return self.read(path)
diff --git a/pystache/locator.py b/pystache/locator.py
new file mode 100644
index 0000000..a1f06db
--- /dev/null
+++ b/pystache/locator.py
@@ -0,0 +1,152 @@
+# coding: utf-8
+
+"""
+This module provides a Locator class for finding template files.
+
+"""
+
+import os
+import re
+import sys
+
+from pystache import defaults
+
+
+class Locator(object):
+
+ def __init__(self, extension=None):
+ """
+ Construct a template locator.
+
+ Arguments:
+
+ extension: the template file extension. Pass False for no
+ extension (i.e. to use extensionless template files).
+ Defaults to the package default.
+
+ """
+ if extension is None:
+ extension = defaults.TEMPLATE_EXTENSION
+
+ self.template_extension = extension
+
+ def get_object_directory(self, obj):
+ """
+ Return the directory containing an object's defining class.
+
+ Returns None if there is no such directory, for example if the
+ class was defined in an interactive Python session, or in a
+ doctest that appears in a text file (rather than a Python file).
+
+ """
+ if not hasattr(obj, '__module__'):
+ return None
+
+ module = sys.modules[obj.__module__]
+
+ if not hasattr(module, '__file__'):
+ # TODO: add a unit test for this case.
+ return None
+
+ path = module.__file__
+
+ return os.path.dirname(path)
+
+ def make_template_name(self, obj):
+ """
+ Return the canonical template name for an object instance.
+
+ This method converts Python-style class names (PEP 8's recommended
+ CamelCase, aka CapWords) to lower_case_with_underscords. Here
+ is an example with code:
+
+ >>> class HelloWorld(object):
+ ... pass
+ >>> hi = HelloWorld()
+ >>>
+ >>> locator = Locator()
+ >>> locator.make_template_name(hi)
+ 'hello_world'
+
+ """
+ template_name = obj.__class__.__name__
+
+ def repl(match):
+ return '_' + match.group(0).lower()
+
+ return re.sub('[A-Z]', repl, template_name)[1:]
+
+ def make_file_name(self, template_name, template_extension=None):
+ """
+ Generate and return the file name for the given template name.
+
+ Arguments:
+
+ template_extension: defaults to the instance's extension.
+
+ """
+ file_name = template_name
+
+ if template_extension is None:
+ template_extension = self.template_extension
+
+ if template_extension is not False:
+ file_name += os.path.extsep + template_extension
+
+ return file_name
+
+ def _find_path(self, search_dirs, file_name):
+ """
+ Search for the given file, and return the path.
+
+ Returns None if the file is not found.
+
+ """
+ for dir_path in search_dirs:
+ file_path = os.path.join(dir_path, file_name)
+ if os.path.exists(file_path):
+ return file_path
+
+ return None
+
+ def _find_path_required(self, search_dirs, file_name):
+ """
+ Return the path to a template with the given file name.
+
+ """
+ path = self._find_path(search_dirs, file_name)
+
+ if path is None:
+ # TODO: we should probably raise an exception of our own type.
+ raise IOError('Template file %s not found in directories: %s' %
+ (repr(file_name), repr(search_dirs)))
+
+ return path
+
+ def find_name(self, template_name, search_dirs):
+ """
+ Return the path to a template with the given name.
+
+ """
+ file_name = self.make_file_name(template_name)
+
+ return self._find_path_required(search_dirs, file_name)
+
+ def find_object(self, obj, search_dirs, file_name=None):
+ """
+ Return the path to a template associated with the given object.
+
+ """
+ if file_name is None:
+ # TODO: should we define a make_file_name() method?
+ template_name = self.make_template_name(obj)
+ file_name = self.make_file_name(template_name)
+
+ dir_path = self.get_object_directory(obj)
+
+ if dir_path is not None:
+ search_dirs = [dir_path] + search_dirs
+
+ path = self._find_path_required(search_dirs, file_name)
+
+ return path
diff --git a/pystache/parsed.py b/pystache/parsed.py
new file mode 100644
index 0000000..5418ec1
--- /dev/null
+++ b/pystache/parsed.py
@@ -0,0 +1,49 @@
+# coding: utf-8
+
+"""
+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:
+
+ parse_tree: a list, each element of which is either--
+
+ (1) a unicode string, or
+ (2) a "rendering" callable that accepts a Context instance
+ and returns a unicode string.
+
+ The possible rendering callables are the return values of the
+ following functions:
+
+ * RenderEngine._make_get_escaped()
+ * RenderEngine._make_get_inverse()
+ * RenderEngine._make_get_literal()
+ * RenderEngine._make_get_partial()
+ * RenderEngine._make_get_section()
+
+ """
+ self._parse_tree = parse_tree
+
+ def render(self, 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
+ parts = map(get_unicode, self._parse_tree)
+ s = ''.join(parts)
+
+ return unicode(s)
+
diff --git a/pystache/parser.py b/pystache/parser.py
new file mode 100644
index 0000000..d07ebf6
--- /dev/null
+++ b/pystache/parser.py
@@ -0,0 +1,197 @@
+# coding: utf-8
+
+"""
+Provides a class for parsing template strings.
+
+This module is only meant for internal use by the renderengine module.
+
+"""
+
+import re
+
+from parsed import ParsedTemplate
+
+
+DEFAULT_DELIMITERS = ('{{', '}}')
+END_OF_LINE_CHARACTERS = ['\r', '\n']
+NON_BLANK_RE = re.compile(r'^(.)', re.M)
+
+
+def _compile_template_re(delimiters):
+
+ # The possible tag type characters following the opening tag,
+ # excluding "=" and "{".
+ tag_types = "!>&/#^"
+
+ # TODO: are we following this in the spec?
+ #
+ # The tag's content MUST be a non-whitespace character sequence
+ # NOT containing the current closing delimiter.
+ #
+ tag = r"""
+ (?P<whitespace>[\ \t]*)
+ %(otag)s \s*
+ (?:
+ (?P<change>=) \s* (?P<delims>.+?) \s* = |
+ (?P<raw>{) \s* (?P<raw_name>.+?) \s* } |
+ (?P<tag>[%(tag_types)s]?) \s* (?P<tag_key>[\s\S]+?)
+ )
+ \s* %(ctag)s
+ """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])}
+
+ return re.compile(tag, re.VERBOSE)
+
+
+class ParsingError(Exception):
+
+ pass
+
+
+class Parser(object):
+
+ _delimiters = None
+ _template_re = None
+
+ def __init__(self, engine, delimiters=None):
+ """
+ Construct an instance.
+
+ Arguments:
+
+ engine: a RenderEngine instance.
+
+ """
+ if delimiters is None:
+ delimiters = DEFAULT_DELIMITERS
+
+ self._delimiters = delimiters
+ self.engine = engine
+
+ def compile_template_re(self):
+ self._template_re = _compile_template_re(self._delimiters)
+
+ def _change_delimiters(self, delimiters):
+ self._delimiters = delimiters
+ self.compile_template_re()
+
+ def parse(self, template, index=0, section_key=None):
+ """
+ Parse a template string into a ParsedTemplate instance.
+
+ This method uses the current tag delimiter.
+
+ Arguments:
+
+ template: a template string of type unicode.
+
+ """
+ parse_tree = []
+ start_index = index
+
+ while True:
+ match = self._template_re.search(template, index)
+
+ if match is None:
+ break
+
+ 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.
+ if matches['change'] is not None:
+ matches.update(tag='=', tag_key=matches['delims'])
+ elif matches['raw'] is not None:
+ matches.update(tag='&', tag_key=matches['raw_name'])
+
+ tag_type = matches['tag']
+ tag_key = matches['tag_key']
+ leading_whitespace = matches['whitespace']
+
+ # Standalone (non-interpolation) tags consume the entire line,
+ # both leading whitespace and trailing newline.
+ did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS
+ did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
+ is_tag_interpolating = tag_type in ['', '&']
+
+ if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating:
+ if end_index < len(template):
+ end_index += template[end_index] == '\r' and 1 or 0
+ if end_index < len(template):
+ end_index += template[end_index] == '\n' and 1 or 0
+ elif leading_whitespace:
+ 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" % (repr(tag_key), repr(section_key)))
+
+ return ParsedTemplate(parse_tree), template[start_index:match_index], end_index
+
+ index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
+
+ # Save the rest of the template.
+ parse_tree.append(template[index:])
+
+ return ParsedTemplate(parse_tree)
+
+ def _parse_section(self, template, index_start, section_key):
+ parsed_template, template, index_end = self.parse(template=template, index=index_start, section_key=section_key)
+
+ return parsed_template, template, index_end
+
+ def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
+
+ # TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
+ if tag_type == '!':
+ return end_index
+
+ if tag_type == '=':
+ delimiters = tag_key.split()
+ self._change_delimiters(delimiters)
+ return end_index
+
+ engine = self.engine
+
+ if tag_type == '':
+
+ func = engine._make_get_escaped(tag_key)
+
+ elif tag_type == '&':
+
+ func = engine._make_get_literal(tag_key)
+
+ elif tag_type == '#':
+
+ parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
+ func = engine._make_get_section(tag_key, parsed_section, template, self._delimiters)
+
+ elif tag_type == '^':
+
+ parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
+ func = engine._make_get_inverse(tag_key, parsed_section)
+
+ elif tag_type == '>':
+
+ template = engine.load_partial(tag_key)
+
+ # Indent before rendering.
+ template = re.sub(NON_BLANK_RE, leading_whitespace + r'\1', template)
+
+ func = engine._make_get_partial(template)
+
+ else:
+
+ raise Exception("Unrecognized tag type: %s" % repr(tag_type))
+
+ parse_tree.append(func)
+
+ return end_index
+
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
new file mode 100644
index 0000000..4361dca
--- /dev/null
+++ b/pystache/renderengine.py
@@ -0,0 +1,236 @@
+# coding: utf-8
+
+"""
+Defines a class responsible for rendering logic.
+
+"""
+
+import re
+
+from parser import Parser
+
+
+class RenderEngine(object):
+
+ """
+ Provides a render() method.
+
+ This class is meant only for internal use.
+
+ As a rule, the code in this class operates on unicode strings where
+ possible rather than, say, strings of type str or markupsafe.Markup.
+ This means that strings obtained from "external" sources like partials
+ and variable tag values are immediately converted to unicode (or
+ escaped and converted to unicode) before being operated on further.
+ This makes maintaining, reasoning about, and testing the correctness
+ of the code much simpler. In particular, it keeps the implementation
+ of this class independent of the API details of one (or possibly more)
+ unicode subclasses (e.g. markupsafe.Markup).
+
+ """
+
+ def __init__(self, load_partial=None, literal=None, escape=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).
+
+ literal: the function used to convert unescaped variable tag
+ values to unicode, e.g. the value corresponding to a tag
+ "{{{name}}}". The function should accept a string of type
+ str or unicode (or a subclass) and return a string of type
+ unicode (but not a proper subclass of unicode).
+ This class will only pass basestring instances to this
+ function. For example, it will call str() on integer variable
+ values prior to passing them to this function.
+
+ escape: the function used to escape and convert variable tag
+ values to unicode, e.g. the value corresponding to a tag
+ "{{name}}". The function should obey the same properties
+ described above for the "literal" function argument.
+ This function should take care to convert any str
+ arguments to unicode just as the literal function should, as
+ this class will not pass tag values to literal prior to passing
+ them to this function. This allows for more flexibility,
+ for example using a custom escape function that handles
+ incoming strings of type markupssafe.Markup differently
+ from plain unicode strings.
+
+ """
+ self.escape = escape
+ self.literal = literal
+ self.load_partial = load_partial
+
+ def _get_string_value(self, context, tag_name):
+ """
+ Get a value from the given context as a basestring instance.
+
+ """
+ val = context.get(tag_name)
+
+ # We use "==" rather than "is" to compare integers, as using "is"
+ # relies on an implementation detail of CPython. The test about
+ # rendering zeroes failed while using PyPy when using "is".
+ # See issue #34: https://github.com/defunkt/pystache/issues/34
+ if not val and val != 0:
+ if tag_name != '.':
+ return ''
+ val = context.top()
+
+ 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 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.
+
+ """
+ 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.
+
+ """
+ data = context.get(name)
+ 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)
+ if not data:
+ data = []
+ elif callable(data):
+ # TODO: should we check the arity?
+ template = data(template)
+ parsed_template = self._parse(template, delimiters=delims)
+ data = [ data ]
+ elif not hasattr(data, '__iter__') or isinstance(data, dict):
+ data = [ data ]
+
+ parts = []
+ for element in data:
+ 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:
+
+ template: a template string of type unicode.
+
+ """
+ parser = Parser(self, delimiters=delimiters)
+ parser.compile_template_re()
+
+ 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 Context 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):
+ """
+ Return a template rendered as a string with type unicode.
+
+ Arguments:
+
+ template: a template string of type unicode (but not a proper
+ subclass of unicode).
+
+ context: a Context 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)
+
+ return self._render(template, context)
diff --git a/pystache/renderer.py b/pystache/renderer.py
new file mode 100644
index 0000000..5bd2a3f
--- /dev/null
+++ b/pystache/renderer.py
@@ -0,0 +1,336 @@
+# coding: utf-8
+
+"""
+This module provides a Renderer class to render templates.
+
+"""
+
+from pystache import defaults
+from pystache.context import Context
+from pystache.loader import Loader
+from pystache.renderengine import RenderEngine
+from pystache.spec_loader import SpecLoader
+from pystache.template_spec import TemplateSpec
+
+
+class Renderer(object):
+
+ """
+ A class for rendering mustache templates.
+
+ This class supports several rendering options which are described in
+ the constructor's docstring. Among these, the constructor supports
+ passing a custom partial loader.
+
+ Here is an example of rendering a template using a custom partial loader
+ that loads partials from a string-string dictionary.
+
+ >>> partials = {'partial': 'Hello, {{thing}}!'}
+ >>> renderer = Renderer(partials=partials)
+ >>> renderer.render('{{>partial}}', {'thing': 'world'})
+ u'Hello, world!'
+
+ """
+
+ def __init__(self, file_encoding=None, string_encoding=None,
+ decode_errors=None, search_dirs=None, file_extension=None,
+ escape=None, partials=None):
+ """
+ Construct an instance.
+
+ Arguments:
+
+ partials: an object (e.g. a dictionary) for custom partial loading
+ during the rendering process.
+ The object should have a get() method that accepts a string
+ and returns the corresponding template as a string, preferably
+ as a unicode string. If there is no template with that name,
+ the get() method should either return None (as dict.get() does)
+ or raise an exception.
+ If this argument is None, the rendering process will use
+ the normal procedure of locating and reading templates from
+ the file system -- using relevant instance attributes like
+ search_dirs, file_encoding, etc.
+
+ 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
+ that is again unicode (or a subclass of unicode).
+ This function need not handle strings of type `str` because
+ this class will only pass it unicode strings. The constructor
+ assigns this function to the constructed instance's escape()
+ method.
+ The argument defaults to `cgi.escape(s, quote=True)`. To
+ disable escaping entirely, one can pass `lambda u: u` as the
+ escape function, for example. One may also wish to consider
+ using markupsafe's escape function: markupsafe.escape().
+
+ 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
+ package default.
+
+ """
+ if decode_errors is None:
+ decode_errors = defaults.DECODE_ERRORS
+
+ if escape is None:
+ escape = defaults.TAG_ESCAPE
+
+ if file_encoding is None:
+ file_encoding = defaults.FILE_ENCODING
+
+ if file_extension is None:
+ file_extension = defaults.TEMPLATE_EXTENSION
+
+ if search_dirs is None:
+ search_dirs = defaults.SEARCH_DIRS
+
+ if string_encoding is None:
+ string_encoding = defaults.STRING_ENCODING
+
+ if isinstance(search_dirs, basestring):
+ search_dirs = [search_dirs]
+
+ self._context = None
+ self.decode_errors = decode_errors
+ self.escape = escape
+ self.file_encoding = file_encoding
+ self.file_extension = file_extension
+ self.partials = partials
+ self.search_dirs = search_dirs
+ self.string_encoding = string_encoding
+
+ # This is an experimental way of giving views access to the current context.
+ # TODO: consider another approach of not giving access via a property,
+ # but instead letting the caller pass the initial context to the
+ # main render() method by reference. This approach would probably
+ # be less likely to be misused.
+ @property
+ def context(self):
+ """
+ Return the current rendering context [experimental].
+
+ """
+ return self._context
+
+ def _to_unicode_soft(self, s):
+ """
+ Convert a basestring to unicode, preserving any unicode subclass.
+
+ """
+ # We type-check to avoid "TypeError: decoding Unicode is not supported".
+ # We avoid the Python ternary operator for Python 2.4 support.
+ if isinstance(s, unicode):
+ return s
+ return self.unicode(s)
+
+ def _to_unicode_hard(self, s):
+ """
+ Convert a basestring to a string with type unicode (not subclass).
+
+ """
+ return unicode(self._to_unicode_soft(s))
+
+ def _escape_to_unicode(self, s):
+ """
+ Convert a basestring to unicode (preserving any unicode subclass), and escape it.
+
+ Returns a unicode string (not subclass).
+
+ """
+ return unicode(self.escape(self._to_unicode_soft(s)))
+
+ def unicode(self, s, encoding=None):
+ """
+ Convert a string to unicode, using string_encoding and decode_errors.
+
+ Raises:
+
+ TypeError: Because this method calls Python's built-in unicode()
+ function, this method raises the following exception if the
+ given string is already unicode:
+
+ TypeError: decoding Unicode is not supported
+
+ """
+ if encoding is None:
+ encoding = self.string_encoding
+
+ # TODO: Wrap UnicodeDecodeErrors with a message about setting
+ # the string_encoding and decode_errors attributes.
+ return unicode(s, encoding, self.decode_errors)
+
+ def _make_loader(self):
+ """
+ Create a Loader instance using current attributes.
+
+ """
+ return Loader(file_encoding=self.file_encoding, extension=self.file_extension,
+ to_unicode=self.unicode, search_dirs=self.search_dirs)
+
+ def _make_load_template(self):
+ """
+ Return a function that loads a template by name.
+
+ """
+ loader = self._make_loader()
+
+ def load_template(template_name):
+ return loader.load_name(template_name)
+
+ return load_template
+
+ def _make_load_partial(self):
+ """
+ Return the load_partial function to pass to RenderEngine.__init__().
+
+ """
+ if self.partials is None:
+ load_template = self._make_load_template()
+ return load_template
+
+ # Otherwise, create a load_partial function from the custom partial
+ # loader that satisfies RenderEngine requirements (and that provides
+ # a nicer exception, etc).
+ partials = self.partials
+
+ def load_partial(name):
+ template = partials.get(name)
+
+ if template is None:
+ # TODO: make a TemplateNotFoundException type that provides
+ # the original partials as an attribute.
+ raise Exception("Partial not found with name: %s" % repr(name))
+
+ # RenderEngine requires that the return value be unicode.
+ return self._to_unicode_hard(template)
+
+ return load_partial
+
+ def _make_render_engine(self):
+ """
+ Return a RenderEngine instance for rendering.
+
+ """
+ load_partial = self._make_load_partial()
+
+ engine = RenderEngine(load_partial=load_partial,
+ literal=self._to_unicode_hard,
+ escape=self._escape_to_unicode)
+ 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_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)
+
+ context = Context.create(*context, **kwargs)
+ self._context = context
+
+ engine = self._make_render_engine()
+ rendered = engine.render(template, context)
+
+ return unicode(rendered)
+
+ def _render_object(self, obj, *context, **kwargs):
+ """
+ Render the template associated with the given object.
+
+ """
+ loader = self._make_loader()
+
+ # TODO: consider an approach that does not require using an if
+ # block here. For example, perhaps this class's loader can be
+ # a SpecLoader in all cases, and the SpecLoader instance can
+ # check the object's type. Or perhaps Loader and SpecLoader
+ # can be refactored to implement the same interface.
+ if isinstance(obj, TemplateSpec):
+ loader = SpecLoader(loader)
+ template = loader.load(obj)
+ else:
+ template = loader.load_object(obj)
+
+ context = [obj] + list(context)
+
+ return self._render_string(template, *context, **kwargs)
+
+ def render_path(self, template_path, *context, **kwargs):
+ """
+ Render the template at the given path using the given context.
+
+ Read the render() docstring for more information.
+
+ """
+ loader = self._make_loader()
+ template = loader.read(template_path)
+
+ return self._render_string(template, *context, **kwargs)
+
+ def render(self, template, *context, **kwargs):
+ """
+ Render the given template (or template object) using the given context.
+
+ Returns the rendering as 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.
+
+ 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.
+
+ *context: zero or more dictionaries, Context instances, or objects
+ with which to populate the initial context stack. None
+ arguments are skipped. Items in the *context list are added to
+ the context stack in order so that later items in the argument
+ list take precedence over earlier items.
+
+ **kwargs: additional key-value data to add to the context stack.
+ As these arguments appear after all items in the *context list,
+ in the case of key conflicts these values take precedence over
+ all items in the *context list.
+
+ """
+ if isinstance(template, basestring):
+ return self._render_string(template, *context, **kwargs)
+ # Otherwise, we assume the template is an object.
+
+ return self._render_object(template, *context, **kwargs)
diff --git a/pystache/spec_loader.py b/pystache/spec_loader.py
new file mode 100644
index 0000000..3cb0f1a
--- /dev/null
+++ b/pystache/spec_loader.py
@@ -0,0 +1,87 @@
+# coding: utf-8
+
+"""
+This module supports customized (aka special or specified) template loading.
+
+"""
+
+import os.path
+
+from pystache.loader import Loader
+
+
+# TODO: add test cases for this class.
+class SpecLoader(object):
+
+ """
+ Supports loading custom-specified templates (from TemplateSpec instances).
+
+ """
+
+ def __init__(self, loader=None):
+ if loader is None:
+ loader = Loader()
+
+ self.loader = loader
+
+ def _find_relative(self, spec):
+ """
+ Return the path to the template as a relative (dir, file_name) pair.
+
+ The directory returned is relative to the directory containing the
+ class definition of the given object. The method returns None for
+ this directory if the directory is unknown without first searching
+ the search directories.
+
+ """
+ if spec.template_rel_path is not None:
+ return os.path.split(spec.template_rel_path)
+ # Otherwise, determine the file name separately.
+
+ locator = self.loader._make_locator()
+
+ # We do not use the ternary operator for Python 2.4 support.
+ if spec.template_name is not None:
+ template_name = spec.template_name
+ else:
+ template_name = locator.make_template_name(spec)
+
+ file_name = locator.make_file_name(template_name, spec.template_extension)
+
+ return (spec.template_rel_directory, file_name)
+
+ def _find(self, spec):
+ """
+ Find and return the path to the template associated to the instance.
+
+ """
+ dir_path, file_name = self._find_relative(spec)
+
+ locator = self.loader._make_locator()
+
+ if dir_path is None:
+ # Then we need to search for the path.
+ path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name)
+ else:
+ obj_dir = locator.get_object_directory(spec)
+ path = os.path.join(obj_dir, dir_path, file_name)
+
+ return path
+
+ def load(self, spec):
+ """
+ Find and return the template associated to a TemplateSpec instance.
+
+ Returns the template as a unicode string.
+
+ Arguments:
+
+ spec: a TemplateSpec instance.
+
+ """
+ if spec.template is not None:
+ return self.loader.unicode(spec.template, spec.template_encoding)
+
+ path = self._find(spec)
+
+ return self.loader.read(path, spec.template_encoding)
diff --git a/pystache/template.py b/pystache/template.py
deleted file mode 100644
index f8219cb..0000000
--- a/pystache/template.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import re
-import cgi
-import collections
-import os
-import copy
-
-try:
- import markupsafe
- escape = markupsafe.escape
- literal = markupsafe.Markup
-
-except ImportError:
- escape = lambda x: cgi.escape(unicode(x))
- literal = unicode
-
-
-class Modifiers(dict):
- """Dictionary with a decorator for assigning functions to keys."""
-
- def set(self, key):
- """
- Decorator function to set the given key to the decorated function.
-
- >>> modifiers = {}
- >>> @modifiers.set('P')
- ... def render_tongue(self, tag_name=None, context=None):
- ... return ":P %s" % tag_name
- >>> modifiers
- {'P': <function render_tongue at 0x...>}
- """
-
- def setter(func):
- self[key] = func
- return func
- return setter
-
-
-class Template(object):
-
- tag_re = None
-
- otag = '{{'
-
- ctag = '}}'
-
- modifiers = Modifiers()
-
- def __init__(self, template=None, context=None, **kwargs):
- from view import View
-
- self.template = template
-
- if kwargs:
- context.update(kwargs)
-
- if isinstance(context, View):
- self.view = context
- else:
- self.view = View(context=context)
-
- self._compile_regexps()
-
- def _compile_regexps(self):
- tags = {
- 'otag': re.escape(self.otag),
- 'ctag': re.escape(self.ctag)
- }
-
- section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?\s*)%(otag)s/\1%(ctag)s"
- self.section_re = re.compile(section % tags, re.M|re.S)
-
- tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
- self.tag_re = re.compile(tag % tags)
-
- def _render_sections(self, template, view):
- while True:
- match = self.section_re.search(template)
- if match is None:
- break
-
- section, section_name, inner = match.group(0, 1, 2)
- section_name = section_name.strip()
- it = self.view.get(section_name, None)
- replacer = ''
-
- # Callable
- if it and callable(it):
- replacer = it(inner)
- # Dictionary
- elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
- if section[2] != '^':
- replacer = self._render_dictionary(inner, it)
- # Lists
- elif it and hasattr(it, '__iter__'):
- if section[2] != '^':
- replacer = self._render_list(inner, it)
- # Other objects
- elif it and isinstance(it, object):
- if section[2] != '^':
- replacer = self._render_dictionary(inner, it)
- # Falsey and Negated or Truthy and Not Negated
- elif (not it and section[2] == '^') or (it and section[2] != '^'):
- replacer = self._render_dictionary(inner, it)
-
- template = literal(template.replace(section, replacer))
-
- return template
-
- def _render_tags(self, template):
- while True:
- match = self.tag_re.search(template)
- if match is None:
- break
-
- tag, tag_type, tag_name = match.group(0, 1, 2)
- tag_name = tag_name.strip()
- func = self.modifiers[tag_type]
- replacement = func(self, tag_name)
- template = template.replace(tag, replacement)
-
- return template
-
- def _render_dictionary(self, template, context):
- self.view.context_list.insert(0, context)
- template = Template(template, self.view)
- out = template.render()
- self.view.context_list.pop(0)
- return out
-
- def _render_list(self, template, listing):
- insides = []
- for item in listing:
- insides.append(self._render_dictionary(template, item))
-
- return ''.join(insides)
-
- @modifiers.set(None)
- def _render_tag(self, tag_name):
- raw = self.view.get(tag_name, '')
-
- # For methods with no return value
- if not raw and raw is not 0:
- if tag_name == '.':
- raw = self.view.context_list[0]
- else:
- return ''
-
- return escape(raw)
-
- @modifiers.set('!')
- def _render_comment(self, tag_name):
- return ''
-
- @modifiers.set('>')
- def _render_partial(self, template_name):
- from pystache import Loader
- markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding)
- template = Template(markup, self.view)
- return template.render()
-
- @modifiers.set('=')
- def _change_delimiter(self, tag_name):
- """Changes the Mustache delimiter."""
- self.otag, self.ctag = tag_name.split(' ')
- self._compile_regexps()
- return ''
-
- @modifiers.set('{')
- @modifiers.set('&')
- def render_unescaped(self, tag_name):
- """Render a tag without escaping it."""
- return literal(self.view.get(tag_name, ''))
-
- def render(self, encoding=None):
- template = self._render_sections(self.template, self.view)
- result = self._render_tags(template)
-
- if encoding is not None:
- result = result.encode(encoding)
-
- return result
diff --git a/pystache/template_spec.py b/pystache/template_spec.py
new file mode 100644
index 0000000..c33f30b
--- /dev/null
+++ b/pystache/template_spec.py
@@ -0,0 +1,43 @@
+# coding: utf-8
+
+"""
+This module supports customized (aka special or specified) template loading.
+
+"""
+
+# TODO: finish the class docstring.
+class TemplateSpec(object):
+
+ """
+ A mixin or interface for specifying custom template information.
+
+ The "spec" in TemplateSpec can be taken to mean that the template
+ information is either "specified" or "special."
+
+ A view should subclass this class only if customized template loading
+ is needed. The following attributes allow one to customize/override
+ template information on a per view basis. A None value means to use
+ default behavior for that value and perform no customization. All
+ attributes are initialized to None.
+
+ Attributes:
+
+ template: the template as a string.
+
+ template_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_extension: the template file extension. Defaults to "mustache".
+ Pass False for no extension (i.e. extensionless template files).
+
+ """
+
+ template = None
+ template_rel_path = None
+ template_rel_directory = None
+ template_name = None
+ template_extension = None
+ template_encoding = None
diff --git a/pystache/view.py b/pystache/view.py
deleted file mode 100644
index 925998e..0000000
--- a/pystache/view.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from pystache import Template
-import os.path
-import re
-from types import *
-
-def get_or_attr(context_list, name, default=None):
- if not context_list:
- return default
-
- for obj in context_list:
- try:
- return obj[name]
- except KeyError:
- pass
- except:
- try:
- return getattr(obj, name)
- except AttributeError:
- pass
- return default
-
-class View(object):
-
- template_name = None
- template_path = None
- template = None
- template_encoding = None
- template_extension = 'mustache'
-
- def __init__(self, template=None, context=None, **kwargs):
- self.template = template
- context = context or {}
- context.update(**kwargs)
-
- self.context_list = [context]
-
- def get(self, attr, default=None):
- attr = get_or_attr(self.context_list, attr, getattr(self, attr, default))
- if hasattr(attr, '__call__') and type(attr) is UnboundMethodType:
- return attr()
- else:
- return attr
-
- def get_template(self, template_name):
- if not self.template:
- from pystache import Loader
- template_name = self._get_template_name(template_name)
- self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension)
-
- return self.template
-
- def _get_template_name(self, template_name=None):
- """TemplatePartial => template_partial
- Takes a string but defaults to using the current class' name or
- the `template_name` attribute
- """
- if template_name:
- return template_name
-
- template_name = self.__class__.__name__
-
- def repl(match):
- return '_' + match.group(0).lower()
-
- return re.sub('[A-Z]', repl, template_name)[1:]
-
- def _get_context(self):
- context = {}
- for item in self.context_list:
- if hasattr(item, 'keys') and hasattr(item, '__getitem__'):
- context.update(item)
- return context
-
- def render(self, encoding=None):
- return Template(self.get_template(self.template_name), self).render(encoding=encoding)
-
- def __contains__(self, needle):
- return needle in self.context or hasattr(self, needle)
-
- def __getitem__(self, attr):
- val = self.get(attr, None)
-
- if not val and val is not 0:
- raise KeyError("Key '%s' does not exist in View" % attr)
- return val
-
- def __getattr__(self, attr):
- if attr == 'context':
- return self._get_context()
-
- raise AttributeError("Attribute '%s' does not exist in View" % attr)
-
- def __str__(self):
- return self.render() \ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..f91c44e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[nosetests]
+with-doctest=1
+doctest-extension=rst
diff --git a/setup.py b/setup.py
index d625922..65d8d6f 100644
--- a/setup.py
+++ b/setup.py
@@ -1,44 +1,94 @@
#!/usr/bin/env python
+# coding: utf-8
+
"""
-Run the following to publish to PyPI:
+This script supports installing and distributing pystache.
+
+Below are instructions to pystache maintainers on how to push a new
+version of pystache to PyPI--
+
+ http://pypi.python.org/pypi/pystache
+
+Create a PyPI user account. The user account will need permissions to push
+to PyPI. A current "Package Index Owner" of pystache can grant you those
+permissions.
+
+When you have permissions, run the following (after preparing the release,
+bumping the version number in setup.py, etc):
+
+ > python setup.py publish
+
+If you get an error like the following--
+
+ Upload failed (401): You must be identified to edit package information
+
+then add a file called .pyirc to your home directory with the following
+contents:
+
+ [server-login]
+ username: <PyPI username>
+ password: <PyPI password>
+
+as described here, for example:
-> python setup.py publish
+ http://docs.python.org/release/2.5.2/dist/pypirc.html
"""
import os
import sys
+
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
+
def publish():
- """Publish to Pypi"""
- os.system("python setup.py sdist upload")
+ """
+ Publish this package to PyPI (aka "the Cheeseshop").
+
+ """
+ os.system('python setup.py sdist upload')
+
+
+def make_long_description():
+ """
+ Return the long description for the package.
-if sys.argv[-1] == "publish":
+ """
+ long_description = open('README.rst').read() + '\n\n' + open('HISTORY.rst').read()
+
+ return long_description
+
+
+if sys.argv[-1] == 'publish':
publish()
sys.exit()
+long_description = make_long_description()
+
setup(name='pystache',
- version='0.4.1',
+ version='0.5.0-rc',
description='Mustache for Python',
- long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
+ long_description=long_description,
author='Chris Wanstrath',
author_email='chris@ozmm.org',
+ maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache',
packages=['pystache'],
license='MIT',
+ entry_points = {
+ 'console_scripts': ['pystache=pystache.commands:main'],
+ },
classifiers = (
- "Development Status :: 4 - Beta",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 2.4",
- "Programming Language :: Python :: 2.5",
- "Programming Language :: Python :: 2.6",
- "Programming Language :: Python :: 2.7",
- )
- )
-
+ 'Development Status :: 4 - Beta',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.4',
+ 'Programming Language :: Python :: 2.5',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ )
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/benchmark.py b/tests/benchmark.py
new file mode 100755
index 0000000..d46e973
--- /dev/null
+++ b/tests/benchmark.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+"""
+A rudimentary backward- and forward-compatible script to benchmark pystache.
+
+Usage:
+
+tests/benchmark.py 10000
+
+"""
+
+import sys
+from timeit import Timer
+
+import pystache
+
+# TODO: make the example realistic.
+
+examples = [
+ # Test case: 1
+ ("""{{#person}}Hi {{name}}{{/person}}""",
+ {"person": {"name": "Jon"}},
+ "Hi Jon"),
+
+ # Test case: 2
+ ("""\
+<div class="comments">
+<h3>{{header}}</h3>
+<ul>
+{{#comments}}<li class="comment">
+<h5>{{name}}</h5><p>{{body}}</p>
+</li>{{/comments}}
+</ul>
+</div>""",
+ {'header': "My Post Comments",
+ 'comments': [
+ {'name': "Joe", 'body': "Thanks for this post!"},
+ {'name': "Sam", 'body': "Thanks for this post!"},
+ {'name': "Heather", 'body': "Thanks for this post!"},
+ {'name': "Kathy", 'body': "Thanks for this post!"},
+ {'name': "George", 'body': "Thanks for this post!"}]},
+ """\
+<div class="comments">
+<h3>My Post Comments</h3>
+<ul>
+<li class="comment">
+<h5>Joe</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>Sam</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>Heather</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>Kathy</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>George</h5><p>Thanks for this post!</p>
+</li>
+</ul>
+</div>"""),
+]
+
+
+def make_test_function(example):
+
+ template, context, expected = example
+
+ def test():
+ actual = pystache.render(template, context)
+ if actual != expected:
+ raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual))
+
+ return test
+
+
+def main(sys_argv):
+ args = sys_argv[1:]
+ count = int(args[0])
+
+ print "Benchmarking: %sx" % count
+ print
+
+ for example in examples:
+
+ test = make_test_function(example)
+
+ t = Timer(test,)
+ print min(t.repeat(repeat=3, number=count))
+
+ print "Done"
+
+
+if __name__ == '__main__':
+ main(sys.argv)
+
diff --git a/tests/common.py b/tests/common.py
new file mode 100644
index 0000000..adc3ec2
--- /dev/null
+++ b/tests/common.py
@@ -0,0 +1,53 @@
+# coding: utf-8
+
+"""
+Provides test-related code that can be used by all tests.
+
+"""
+
+import os
+
+import examples
+
+
+DATA_DIR = 'tests/data'
+EXAMPLES_DIR = os.path.dirname(examples.__file__)
+
+
+def get_data_path(file_name):
+ return os.path.join(DATA_DIR, file_name)
+
+
+class AssertStringMixin:
+
+ """A unittest.TestCase mixin to check string equality."""
+
+ def assertString(self, actual, expected):
+ """
+ Assert that the given strings are equal and have the same type.
+
+ """
+ # Show both friendly and literal versions.
+ message = """String mismatch: %%s\
+
+
+ Expected: \"""%s\"""
+ Actual: \"""%s\"""
+
+ Expected: %s
+ Actual: %s""" % (expected, actual, repr(expected), repr(actual))
+
+ self.assertEquals(actual, expected, message % "different characters")
+
+ details = "types different: %s != %s" % (repr(type(expected)), repr(type(actual)))
+ self.assertEquals(type(expected), type(actual), message % details)
+
+
+class AssertIsMixin:
+
+ """A unittest.TestCase mixin adding assertIs()."""
+
+ # unittest.assertIs() is not available until Python 2.7:
+ # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
+ def assertIs(self, first, second):
+ self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
diff --git a/tests/data/__init__.py b/tests/data/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/data/__init__.py
diff --git a/tests/data/ascii.mustache b/tests/data/ascii.mustache
new file mode 100644
index 0000000..e86737b
--- /dev/null
+++ b/tests/data/ascii.mustache
@@ -0,0 +1 @@
+ascii: abc \ No newline at end of file
diff --git a/tests/data/duplicate.mustache b/tests/data/duplicate.mustache
new file mode 100644
index 0000000..a0515e3
--- /dev/null
+++ b/tests/data/duplicate.mustache
@@ -0,0 +1 @@
+This file is used to test locate_path()'s search order. \ No newline at end of file
diff --git a/tests/data/locator/duplicate.mustache b/tests/data/locator/duplicate.mustache
new file mode 100644
index 0000000..a0515e3
--- /dev/null
+++ b/tests/data/locator/duplicate.mustache
@@ -0,0 +1 @@
+This file is used to test locate_path()'s search order. \ No newline at end of file
diff --git a/tests/data/non_ascii.mustache b/tests/data/non_ascii.mustache
new file mode 100644
index 0000000..bd69b61
--- /dev/null
+++ b/tests/data/non_ascii.mustache
@@ -0,0 +1 @@
+non-ascii: é \ No newline at end of file
diff --git a/tests/data/sample_view.mustache b/tests/data/sample_view.mustache
new file mode 100644
index 0000000..e86737b
--- /dev/null
+++ b/tests/data/sample_view.mustache
@@ -0,0 +1 @@
+ascii: abc \ No newline at end of file
diff --git a/tests/data/say_hello.mustache b/tests/data/say_hello.mustache
new file mode 100644
index 0000000..84ab4c9
--- /dev/null
+++ b/tests/data/say_hello.mustache
@@ -0,0 +1 @@
+Hello, {{to}} \ No newline at end of file
diff --git a/tests/data/views.py b/tests/data/views.py
new file mode 100644
index 0000000..4d9df02
--- /dev/null
+++ b/tests/data/views.py
@@ -0,0 +1,16 @@
+# coding: utf-8
+
+from pystache import TemplateSpec
+
+class SayHello(object):
+
+ def to(self):
+ return "World"
+
+
+class SampleView(TemplateSpec):
+ pass
+
+
+class NonAscii(TemplateSpec):
+ pass
diff --git a/tests/test_commands.py b/tests/test_commands.py
new file mode 100644
index 0000000..f1817e7
--- /dev/null
+++ b/tests/test_commands.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+
+"""
+Unit tests of commands.py.
+
+"""
+
+import sys
+import unittest
+
+from pystache.commands import main
+
+
+ORIGINAL_STDOUT = sys.stdout
+
+
+class MockStdout(object):
+
+ def __init__(self):
+ self.output = ""
+
+ def write(self, str):
+ self.output += str
+
+
+class CommandsTestCase(unittest.TestCase):
+
+ def setUp(self):
+ sys.stdout = MockStdout()
+
+ def callScript(self, template, context):
+ argv = ['pystache', template, context]
+ main(argv)
+ return sys.stdout.output
+
+ def testMainSimple(self):
+ """
+ Test a simple command-line case.
+
+ """
+ actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
+ self.assertEquals(actual, u"Hi world\n")
+
+ def tearDown(self):
+ sys.stdout = ORIGINAL_STDOUT
diff --git a/tests/test_context.py b/tests/test_context.py
new file mode 100644
index 0000000..decf4fb
--- /dev/null
+++ b/tests/test_context.py
@@ -0,0 +1,400 @@
+# coding: utf-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 Context
+from tests.common import AssertIsMixin
+
+class SimpleObject(object):
+
+ """A sample class that does not define __getitem__()."""
+
+ def __init__(self):
+ self.foo = "bar"
+
+ def foo_callable(self):
+ return "called..."
+
+
+class DictLike(object):
+
+ """A sample class that implements __getitem__() and __contains__()."""
+
+ def __init__(self):
+ self._dict = {'foo': 'bar'}
+ self.fuzz = 'buzz'
+
+ def __contains__(self, key):
+ return key in self._dict
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+
+class GetValueTests(unittest.TestCase, AssertIsMixin):
+
+ """Test context._get_value()."""
+
+ def assertNotFound(self, item, key):
+ """
+ Assert that a call to _get_value() returns _NOT_FOUND.
+
+ """
+ self.assertIs(_get_value(item, key), _NOT_FOUND)
+
+ ### Case: the item is a dictionary.
+
+ def test_dictionary__key_present(self):
+ """
+ Test getting a key from a dictionary.
+
+ """
+ item = {"foo": "bar"}
+ self.assertEquals(_get_value(item, "foo"), "bar")
+
+ def test_dictionary__callable_not_called(self):
+ """
+ Test that callable values are returned as-is (and in particular not called).
+
+ """
+ def foo_callable(self):
+ return "bar"
+
+ item = {"foo": foo_callable}
+ self.assertNotEquals(_get_value(item, "foo"), "bar")
+ self.assertTrue(_get_value(item, "foo") is foo_callable)
+
+ def test_dictionary__key_missing(self):
+ """
+ Test getting a missing key from a dictionary.
+
+ """
+ item = {}
+ self.assertNotFound(item, "missing")
+
+ def test_dictionary__attributes_not_checked(self):
+ """
+ Test that dictionary attributes are not checked.
+
+ """
+ item = {}
+ attr_name = "keys"
+ self.assertEquals(getattr(item, attr_name)(), [])
+ self.assertNotFound(item, attr_name)
+
+ def test_dictionary__dict_subclass(self):
+ """
+ Test that subclasses of dict are treated as dictionaries.
+
+ """
+ class DictSubclass(dict): pass
+
+ item = DictSubclass()
+ item["foo"] = "bar"
+
+ self.assertEquals(_get_value(item, "foo"), "bar")
+
+ ### Case: the item is an object.
+
+ def test_object__attribute_present(self):
+ """
+ Test getting an attribute from an object.
+
+ """
+ item = SimpleObject()
+ self.assertEquals(_get_value(item, "foo"), "bar")
+
+ def test_object__attribute_missing(self):
+ """
+ Test getting a missing attribute from an object.
+
+ """
+ item = SimpleObject()
+ self.assertNotFound(item, "missing")
+
+ def test_object__attribute_is_callable(self):
+ """
+ Test getting a callable attribute from an object.
+
+ """
+ item = SimpleObject()
+ self.assertEquals(_get_value(item, "foo_callable"), "called...")
+
+ def test_object__non_built_in_type(self):
+ """
+ Test getting an attribute from an instance of a type that isn't built-in.
+
+ """
+ item = datetime(2012, 1, 2)
+ self.assertEquals(_get_value(item, "day"), 2)
+
+ def test_object__dict_like(self):
+ """
+ Test getting a key from a dict-like object (an object that implements '__getitem__').
+
+ """
+ item = DictLike()
+ self.assertEquals(item["foo"], "bar")
+ self.assertNotFound(item, "foo")
+
+ ### Case: the item is an instance of a built-in type.
+
+ def test_built_in_type__integer(self):
+ """
+ Test getting from an integer.
+
+ """
+ class MyInt(int): pass
+
+ item1 = MyInt(10)
+ item2 = 10
+
+ try:
+ item2.real
+ except AttributeError:
+ # Then skip this unit test. The numeric type hierarchy was
+ # added only in Python 2.6, in which case integers inherit
+ # from complex numbers the "real" attribute, etc:
+ #
+ # http://docs.python.org/library/numbers.html
+ #
+ return
+
+ self.assertEquals(item1.real, 10)
+ self.assertEquals(item2.real, 10)
+
+ self.assertEquals(_get_value(item1, 'real'), 10)
+ self.assertNotFound(item2, 'real')
+
+ def test_built_in_type__string(self):
+ """
+ Test getting from a string.
+
+ """
+ class MyStr(str): pass
+
+ item1 = MyStr('abc')
+ item2 = 'abc'
+
+ self.assertEquals(item1.upper(), 'ABC')
+ self.assertEquals(item2.upper(), 'ABC')
+
+ self.assertEquals(_get_value(item1, 'upper'), 'ABC')
+ self.assertNotFound(item2, 'upper')
+
+ def test_built_in_type__list(self):
+ """
+ Test getting from a list.
+
+ """
+ class MyList(list): pass
+
+ item1 = MyList([1, 2, 3])
+ item2 = [1, 2, 3]
+
+ self.assertEquals(item1.pop(), 3)
+ self.assertEquals(item2.pop(), 3)
+
+ self.assertEquals(_get_value(item1, 'pop'), 2)
+ self.assertNotFound(item2, 'pop')
+
+
+class ContextTests(unittest.TestCase, AssertIsMixin):
+
+ """
+ Test the Context class.
+
+ """
+
+ def test_init__no_elements(self):
+ """
+ Check that passing nothing to __init__() raises no exception.
+
+ """
+ context = Context()
+
+ def test_init__many_elements(self):
+ """
+ Check that passing more than two items to __init__() raises no exception.
+
+ """
+ context = Context({}, {}, {})
+
+ def test__repr(self):
+ context = Context()
+ self.assertEquals(repr(context), 'Context()')
+
+ context = Context({'foo': 'bar'})
+ self.assertEquals(repr(context), "Context({'foo': 'bar'},)")
+
+ context = Context({'foo': 'bar'}, {'abc': 123})
+ self.assertEquals(repr(context), "Context({'foo': 'bar'}, {'abc': 123})")
+
+ def test__str(self):
+ context = Context()
+ self.assertEquals(str(context), 'Context()')
+
+ context = Context({'foo': 'bar'})
+ self.assertEquals(str(context), "Context({'foo': 'bar'},)")
+
+ context = Context({'foo': 'bar'}, {'abc': 123})
+ self.assertEquals(str(context), "Context({'foo': 'bar'}, {'abc': 123})")
+
+ ## Test the static create() method.
+
+ def test_create__dictionary(self):
+ """
+ Test passing a dictionary.
+
+ """
+ context = Context.create({'foo': 'bar'})
+ self.assertEquals(context.get('foo'), 'bar')
+
+ def test_create__none(self):
+ """
+ Test passing None.
+
+ """
+ context = Context.create({'foo': 'bar'}, None)
+ self.assertEquals(context.get('foo'), 'bar')
+
+ def test_create__object(self):
+ """
+ Test passing an object.
+
+ """
+ class Foo(object):
+ foo = 'bar'
+ context = Context.create(Foo())
+ self.assertEquals(context.get('foo'), 'bar')
+
+ def test_create__context(self):
+ """
+ Test passing a Context instance.
+
+ """
+ obj = Context({'foo': 'bar'})
+ context = Context.create(obj)
+ self.assertEquals(context.get('foo'), 'bar')
+
+ def test_create__kwarg(self):
+ """
+ Test passing a keyword argument.
+
+ """
+ context = Context.create(foo='bar')
+ self.assertEquals(context.get('foo'), 'bar')
+
+ def test_create__precedence_positional(self):
+ """
+ Test precedence of positional arguments.
+
+ """
+ context = Context.create({'foo': 'bar'}, {'foo': 'buzz'})
+ self.assertEquals(context.get('foo'), 'buzz')
+
+ def test_create__precedence_keyword(self):
+ """
+ Test precedence of keyword arguments.
+
+ """
+ context = Context.create({'foo': 'bar'}, foo='buzz')
+ self.assertEquals(context.get('foo'), 'buzz')
+
+ def test_get__key_present(self):
+ """
+ Test getting a key.
+
+ """
+ context = Context({"foo": "bar"})
+ self.assertEquals(context.get("foo"), "bar")
+
+ def test_get__key_missing(self):
+ """
+ Test getting a missing key.
+
+ """
+ context = Context()
+ self.assertTrue(context.get("foo") is None)
+
+ def test_get__default(self):
+ """
+ Test that get() respects the default value.
+
+ """
+ context = Context()
+ self.assertEquals(context.get("foo", "bar"), "bar")
+
+ def test_get__precedence(self):
+ """
+ Test that get() respects the order of precedence (later items first).
+
+ """
+ context = Context({"foo": "bar"}, {"foo": "buzz"})
+ self.assertEquals(context.get("foo"), "buzz")
+
+ def test_get__fallback(self):
+ """
+ Check that first-added stack items are queried on context misses.
+
+ """
+ context = Context({"fuzz": "buzz"}, {"foo": "bar"})
+ self.assertEquals(context.get("fuzz"), "buzz")
+
+ def test_push(self):
+ """
+ Test push().
+
+ """
+ key = "foo"
+ context = Context({key: "bar"})
+ self.assertEquals(context.get(key), "bar")
+
+ context.push({key: "buzz"})
+ self.assertEquals(context.get(key), "buzz")
+
+ def test_pop(self):
+ """
+ Test pop().
+
+ """
+ key = "foo"
+ context = Context({key: "bar"}, {key: "buzz"})
+ self.assertEquals(context.get(key), "buzz")
+
+ item = context.pop()
+ self.assertEquals(item, {"foo": "buzz"})
+ self.assertEquals(context.get(key), "bar")
+
+ def test_top(self):
+ key = "foo"
+ context = Context({key: "bar"}, {key: "buzz"})
+ self.assertEquals(context.get(key), "buzz")
+
+ top = context.top()
+ self.assertEquals(top, {"foo": "buzz"})
+ # Make sure calling top() didn't remove the item from the stack.
+ self.assertEquals(context.get(key), "buzz")
+
+ def test_copy(self):
+ key = "foo"
+ original = Context({key: "bar"}, {key: "buzz"})
+ self.assertEquals(original.get(key), "buzz")
+
+ new = original.copy()
+ # Confirm that the copy behaves the same.
+ self.assertEquals(new.get(key), "buzz")
+ # Change the copy, and confirm it is changed.
+ new.pop()
+ self.assertEquals(new.get(key), "bar")
+ # Confirm the original is unchanged.
+ self.assertEquals(original.get(key), "buzz")
+
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 1e861bd..179b089 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -1,7 +1,6 @@
# encoding: utf-8
import unittest
-import pystache
from examples.comments import Comments
from examples.double_section import DoubleSection
@@ -12,72 +11,91 @@ from examples.delimiters import Delimiters
from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput
from examples.nested_context import NestedContext
+from pystache import Renderer
+from tests.common import EXAMPLES_DIR
+from tests.common import AssertStringMixin
+
+
+class TestView(unittest.TestCase, AssertStringMixin):
+
+ def _assert(self, obj, expected):
+ renderer = Renderer()
+ actual = renderer.render(obj)
+ self.assertString(actual, expected)
-class TestView(unittest.TestCase):
def test_comments(self):
- self.assertEquals(Comments().render(), """<h1>A Comedy of Errors</h1>
-""")
+ self._assert(Comments(), u"<h1>A Comedy of Errors</h1>")
def test_double_section(self):
- self.assertEquals(DoubleSection().render(),"""* first\n* second\n* third""")
+ self._assert(DoubleSection(), u"* first\n* second\n* third")
def test_unicode_output(self):
- self.assertEquals(UnicodeOutput().render(), u'<p>Name: Henri Poincaré</p>')
-
- def test_encoded_output(self):
- self.assertEquals(UnicodeOutput().render('utf8'), '<p>Name: Henri Poincar\xc3\xa9</p>')
+ renderer = Renderer()
+ actual = renderer.render(UnicodeOutput())
+ self.assertString(actual, u'<p>Name: Henri Poincaré</p>')
def test_unicode_input(self):
- self.assertEquals(UnicodeInput().render(),
- u'<p>If alive today, Henri Poincaré would be 156 years old.</p>')
+ renderer = Renderer()
+ actual = renderer.render(UnicodeInput())
+ self.assertString(actual, u'abcdé')
- def test_escaped(self):
- self.assertEquals(Escaped().render(), "<h1>Bear &gt; Shark</h1>")
+ def test_escaping(self):
+ self._assert(Escaped(), u"<h1>Bear &gt; Shark</h1>")
- def test_unescaped(self):
- self.assertEquals(Unescaped().render(), "<h1>Bear > Shark</h1>")
-
- def test_unescaped_sigil(self):
- view = Escaped(template="<h1>{{& thing}}</h1>", context={
- 'thing': 'Bear > Giraffe'
- })
- self.assertEquals(view.render(), "<h1>Bear > Giraffe</h1>")
+ def test_literal(self):
+ renderer = Renderer()
+ actual = renderer.render(Unescaped())
+ self.assertString(actual, u"<h1>Bear > Shark</h1>")
def test_template_partial(self):
- self.assertEquals(TemplatePartial().render(), """<h1>Welcome</h1>
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+ actual = renderer.render(TemplatePartial(renderer=renderer))
+
+ self.assertString(actual, u"""<h1>Welcome</h1>
Again, Welcome!""")
def test_template_partial_extension(self):
- view = TemplatePartial()
- view.template_extension = 'txt'
- self.assertEquals(view.render(), """Welcome
--------
+ renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
-Again, Welcome!
-""")
+ view = TemplatePartial(renderer=renderer)
+ actual = renderer.render(view)
+ self.assertString(actual, u"""Welcome
+-------
+
+## Again, Welcome! ##""")
def test_delimiters(self):
- self.assertEquals(Delimiters().render(), """
+ renderer = Renderer()
+ actual = renderer.render(Delimiters())
+ self.assertString(actual, u"""\
* It worked the first time.
-
* And it worked the second time.
-
* Then, surprisingly, it worked the third time.
""")
def test_nested_context(self):
- self.assertEquals(NestedContext().render(), "one and foo and two")
+ renderer = Renderer()
+ actual = renderer.render(NestedContext(renderer))
+ self.assertString(actual, u"one and foo and two")
def test_nested_context_is_available_in_view(self):
- view = NestedContext()
+ renderer = Renderer()
+
+ view = NestedContext(renderer)
view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}'
- self.assertEquals(view.render(), 'it works!')
+
+ actual = renderer.render(view)
+ self.assertString(actual, u'it works!')
def test_partial_in_partial_has_access_to_grand_parent_context(self):
- view = TemplatePartial(context = {'prop': 'derp'})
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+
+ view = TemplatePartial(renderer=renderer)
view.template = '''{{>partial_in_partial}}'''
- self.assertEquals(view.render(), 'Hi derp!')
+
+ actual = renderer.render(view, {'prop': 'derp'})
+ self.assertEquals(actual, 'Hi derp!')
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 42222ac..119ebef 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -1,21 +1,198 @@
+# encoding: utf-8
+
+"""
+Unit tests of reader.py.
+
+"""
+
+import os
+import sys
import unittest
-import pystache
-
-class TestLoader(unittest.TestCase):
-
- def test_template_is_loaded(self):
- loader = pystache.Loader()
- template = loader.load_template('simple', 'examples')
-
- self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
-
- def test_using_list_of_paths(self):
- loader = pystache.Loader()
- template = loader.load_template('simple', ['doesnt_exist', 'examples'])
-
- self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
-
- def test_non_existent_template_fails(self):
- loader = pystache.Loader()
-
- self.assertRaises(IOError, loader.load_template, 'simple', 'doesnt_exist') \ No newline at end of file
+
+from tests.common import AssertStringMixin
+from pystache import defaults
+from pystache.loader import Loader
+
+
+DATA_DIR = 'tests/data'
+
+
+class LoaderTests(unittest.TestCase, AssertStringMixin):
+
+ def test_init__extension(self):
+ loader = Loader(extension='foo')
+ self.assertEquals(loader.extension, 'foo')
+
+ def test_init__extension__default(self):
+ # Test the default value.
+ loader = Loader()
+ self.assertEquals(loader.extension, 'mustache')
+
+ def test_init__file_encoding(self):
+ loader = Loader(file_encoding='bar')
+ self.assertEquals(loader.file_encoding, 'bar')
+
+ def test_init__file_encoding__default(self):
+ file_encoding = defaults.FILE_ENCODING
+ try:
+ defaults.FILE_ENCODING = 'foo'
+ loader = Loader()
+ self.assertEquals(loader.file_encoding, 'foo')
+ finally:
+ defaults.FILE_ENCODING = file_encoding
+
+ def test_init__to_unicode(self):
+ to_unicode = lambda x: x
+ loader = Loader(to_unicode=to_unicode)
+ self.assertEquals(loader.to_unicode, to_unicode)
+
+ def test_init__to_unicode__default(self):
+ loader = Loader()
+ self.assertRaises(TypeError, loader.to_unicode, u"abc")
+
+ decode_errors = defaults.DECODE_ERRORS
+ string_encoding = defaults.STRING_ENCODING
+
+ nonascii = 'abcdé'
+
+ try:
+ defaults.DECODE_ERRORS = 'strict'
+ defaults.STRING_ENCODING = 'ascii'
+ loader = Loader()
+ self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
+
+ defaults.DECODE_ERRORS = 'ignore'
+ loader = Loader()
+ self.assertString(loader.to_unicode(nonascii), u'abcd')
+
+ defaults.STRING_ENCODING = 'utf-8'
+ loader = Loader()
+ self.assertString(loader.to_unicode(nonascii), u'abcdé')
+
+ finally:
+ defaults.DECODE_ERRORS = decode_errors
+ defaults.STRING_ENCODING = string_encoding
+
+ def _get_path(self, filename):
+ return os.path.join(DATA_DIR, filename)
+
+ def test_unicode__basic__input_str(self):
+ """
+ Test unicode(): default arguments with str input.
+
+ """
+ reader = Loader()
+ actual = reader.unicode("foo")
+
+ self.assertString(actual, u"foo")
+
+ def test_unicode__basic__input_unicode(self):
+ """
+ Test unicode(): default arguments with unicode input.
+
+ """
+ reader = Loader()
+ actual = reader.unicode(u"foo")
+
+ self.assertString(actual, u"foo")
+
+ def test_unicode__basic__input_unicode_subclass(self):
+ """
+ Test unicode(): default arguments with unicode-subclass input.
+
+ """
+ class UnicodeSubclass(unicode):
+ pass
+
+ s = UnicodeSubclass(u"foo")
+
+ reader = Loader()
+ actual = reader.unicode(s)
+
+ self.assertString(actual, u"foo")
+
+ def test_unicode__to_unicode__attribute(self):
+ """
+ Test unicode(): encoding attribute.
+
+ """
+ reader = Loader()
+
+ non_ascii = u'abcdé'.encode('utf-8')
+
+ self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
+
+ def to_unicode(s, encoding=None):
+ if encoding is None:
+ encoding = 'utf-8'
+ return unicode(s, encoding)
+
+ reader.to_unicode = to_unicode
+ self.assertString(reader.unicode(non_ascii), u"abcdé")
+
+ def test_unicode__encoding_argument(self):
+ """
+ Test unicode(): encoding argument.
+
+ """
+ reader = Loader()
+
+ non_ascii = u'abcdé'.encode('utf-8')
+
+ self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
+
+ actual = reader.unicode(non_ascii, encoding='utf-8')
+ self.assertString(actual, u'abcdé')
+
+ # TODO: check the read() unit tests.
+ def test_read(self):
+ """
+ Test read().
+
+ """
+ reader = Loader()
+ path = self._get_path('ascii.mustache')
+ actual = reader.read(path)
+ self.assertString(actual, u'ascii: abc')
+
+ def test_read__file_encoding__attribute(self):
+ """
+ Test read(): file_encoding attribute respected.
+
+ """
+ loader = Loader()
+ path = self._get_path('non_ascii.mustache')
+
+ self.assertRaises(UnicodeDecodeError, loader.read, path)
+
+ loader.file_encoding = 'utf-8'
+ actual = loader.read(path)
+ self.assertString(actual, u'non-ascii: é')
+
+ def test_read__encoding__argument(self):
+ """
+ Test read(): encoding argument respected.
+
+ """
+ reader = Loader()
+ path = self._get_path('non_ascii.mustache')
+
+ self.assertRaises(UnicodeDecodeError, reader.read, path)
+
+ actual = reader.read(path, encoding='utf-8')
+ self.assertString(actual, u'non-ascii: é')
+
+ def test_reader__to_unicode__attribute(self):
+ """
+ Test read(): to_unicode attribute respected.
+
+ """
+ reader = Loader()
+ path = self._get_path('non_ascii.mustache')
+
+ self.assertRaises(UnicodeDecodeError, reader.read, path)
+
+ #reader.decode_errors = 'ignore'
+ #actual = reader.read(path)
+ #self.assertString(actual, u'non-ascii: ')
+
diff --git a/tests/test_locator.py b/tests/test_locator.py
new file mode 100644
index 0000000..94a55ad
--- /dev/null
+++ b/tests/test_locator.py
@@ -0,0 +1,150 @@
+# encoding: utf-8
+
+"""
+Contains locator.py unit tests.
+
+"""
+
+from datetime import datetime
+import os
+import sys
+import unittest
+
+# TODO: remove this alias.
+from pystache.loader import Loader as Reader
+from pystache.locator import Locator
+
+from tests.common import DATA_DIR
+from data.views import SayHello
+
+
+class LocatorTests(unittest.TestCase):
+
+ def _locator(self):
+ return Locator(search_dirs=DATA_DIR)
+
+ def test_init__extension(self):
+ # Test the default value.
+ locator = Locator()
+ self.assertEquals(locator.template_extension, 'mustache')
+
+ locator = Locator(extension='txt')
+ self.assertEquals(locator.template_extension, 'txt')
+
+ locator = Locator(extension=False)
+ self.assertTrue(locator.template_extension is False)
+
+ def test_get_object_directory(self):
+ locator = Locator()
+
+ obj = SayHello()
+ actual = locator.get_object_directory(obj)
+
+ self.assertEquals(actual, os.path.abspath(DATA_DIR))
+
+ def test_get_object_directory__not_hasattr_module(self):
+ locator = Locator()
+
+ obj = datetime(2000, 1, 1)
+ self.assertFalse(hasattr(obj, '__module__'))
+ self.assertEquals(locator.get_object_directory(obj), None)
+
+ self.assertFalse(hasattr(None, '__module__'))
+ self.assertEquals(locator.get_object_directory(None), None)
+
+ def test_make_file_name(self):
+ locator = Locator()
+
+ locator.template_extension = 'bar'
+ self.assertEquals(locator.make_file_name('foo'), 'foo.bar')
+
+ locator.template_extension = False
+ self.assertEquals(locator.make_file_name('foo'), 'foo')
+
+ locator.template_extension = ''
+ self.assertEquals(locator.make_file_name('foo'), 'foo.')
+
+ def test_make_file_name__template_extension_argument(self):
+ locator = Locator()
+
+ self.assertEquals(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
+
+ def test_find_name(self):
+ locator = Locator()
+ path = locator.find_name(search_dirs=['examples'], template_name='simple')
+
+ self.assertEquals(os.path.basename(path), 'simple.mustache')
+
+ def test_find_name__using_list_of_paths(self):
+ locator = Locator()
+ path = locator.find_name(search_dirs=['doesnt_exist', 'examples'], template_name='simple')
+
+ self.assertTrue(path)
+
+ def test_find_name__precedence(self):
+ """
+ Test the order in which find_name() searches directories.
+
+ """
+ locator = Locator()
+
+ dir1 = DATA_DIR
+ dir2 = os.path.join(DATA_DIR, 'locator')
+
+ self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate'))
+ self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate'))
+
+ path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate')
+ dirpath = os.path.dirname(path)
+ dirname = os.path.split(dirpath)[-1]
+
+ self.assertEquals(dirname, 'locator')
+
+ def test_find_name__non_existent_template_fails(self):
+ locator = Locator()
+
+ self.assertRaises(IOError, locator.find_name, search_dirs=[], template_name='doesnt_exist')
+
+ def test_find_object(self):
+ locator = Locator()
+
+ obj = SayHello()
+
+ actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache')
+ expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
+
+ self.assertEquals(actual, expected)
+
+ def test_find_object__none_file_name(self):
+ locator = Locator()
+
+ obj = SayHello()
+
+ actual = locator.find_object(search_dirs=[], obj=obj)
+ expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache'))
+
+ self.assertEquals(actual, expected)
+
+ def test_find_object__none_object_directory(self):
+ locator = Locator()
+
+ obj = None
+ self.assertEquals(None, locator.get_object_directory(obj))
+
+ actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache')
+ expected = os.path.join(DATA_DIR, 'say_hello.mustache')
+
+ self.assertEquals(actual, expected)
+
+ def test_make_template_name(self):
+ """
+ Test make_template_name().
+
+ """
+ locator = Locator()
+
+ class FooBar(object):
+ pass
+ foo = FooBar()
+
+ self.assertEquals(locator.make_template_name(foo), 'foo_bar')
diff --git a/tests/test_pystache.py b/tests/test_pystache.py
index c04489b..f9857cd 100644
--- a/tests/test_pystache.py
+++ b/tests/test_pystache.py
@@ -2,8 +2,15 @@
import unittest
import pystache
+from pystache import renderer
+
+
+class PystacheTests(unittest.TestCase):
+
+ def _assert_rendered(self, expected, template, context):
+ actual = pystache.render(template, context)
+ self.assertEquals(actual, expected)
-class TestPystache(unittest.TestCase):
def test_basic(self):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
self.assertEquals(ret, "Hi world!")
@@ -14,66 +21,96 @@ class TestPystache(unittest.TestCase):
def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?"
- ret = pystache.render(template, { 'beverage': 'soda', 'person': 'Bob' })
- self.assertEquals(ret, "It's a nice day for soda, right Bob?")
+ context = { 'beverage': 'soda', 'person': 'Bob' }
+ self._assert_rendered("It's a nice day for soda, right Bob?", template, context)
def test_even_less_basic(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?"
- ret = pystache.render(template, { 'name': 'Jon', 'thing': 'racecar' })
- self.assertEquals(ret, "I think Jon wants a racecar, right Jon?")
+ context = { 'name': 'Jon', 'thing': 'racecar' }
+ self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context)
def test_ignores_misses(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?"
- ret = pystache.render(template, { 'name': 'Jon' })
- self.assertEquals(ret, "I think Jon wants a , right Jon?")
+ context = { 'name': 'Jon' }
+ self._assert_rendered("I think Jon wants a , right Jon?", template, context)
def test_render_zero(self):
template = 'My value is {{value}}.'
- ret = pystache.render(template, { 'value': 0 })
- self.assertEquals(ret, 'My value is 0.')
+ context = { 'value': 0 }
+ self._assert_rendered('My value is 0.', template, context)
def test_comments(self):
template = "What {{! the }} what?"
- ret = pystache.render(template)
- self.assertEquals(ret, "What what?")
+ actual = pystache.render(template)
+ self.assertEquals("What what?", actual)
def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!"
- ret = pystache.render(template, { 'set': False })
- self.assertEquals(ret, "Ready go!")
+ context = { 'set': False }
+ self._assert_rendered("Ready go!", template, context)
def test_true_sections_are_shown(self):
template = "Ready {{#set}}set{{/set}} go!"
- ret = pystache.render(template, { 'set': True })
- self.assertEquals(ret, "Ready set go!")
+ context = { 'set': True }
+ self._assert_rendered("Ready set go!", template, context)
+
+ non_strings_expected = """(123 & ['something'])(chris & 0.9)"""
def test_non_strings(self):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
stats = []
stats.append({'key': 123, 'value': ['something']})
stats.append({'key': u"chris", 'value': 0.900})
-
- ret = pystache.render(template, { 'stats': stats })
- self.assertEquals(ret, """(123 & ['something'])(chris & 0.9)""")
+ context = { 'stats': stats }
+ self._assert_rendered(self.non_strings_expected, template, context)
def test_unicode(self):
template = 'Name: {{name}}; Age: {{age}}'
- ret = pystache.render(template, { 'name': u'Henri Poincaré',
- 'age': 156 })
- self.assertEquals(ret, u'Name: Henri Poincaré; Age: 156')
+ context = {'name': u'Henri Poincaré', 'age': 156 }
+ self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context)
def test_sections(self):
template = """<ul>{{#users}}<li>{{name}}</li>{{/users}}</ul>"""
context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] }
- ret = pystache.render(template, context)
- self.assertEquals(ret, """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>""")
-
+ expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
+ self._assert_rendered(expected, template, context)
+
def test_implicit_iterator(self):
template = """<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"""
context = { 'users': [ 'Chris', 'Tom','PJ' ] }
- ret = pystache.render(template, context)
- self.assertEquals(ret, """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>""")
+ expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
+ self._assert_rendered(expected, template, context)
+
+ # The spec says that sections should not alter surrounding whitespace.
+ def test_surrounding_whitepace_not_altered(self):
+ template = "first{{#spacing}} second {{/spacing}}third"
+ context = {"spacing": True}
+ self._assert_rendered("first second third", template, context)
+
+ def test__section__non_false_value(self):
+ """
+ Test when a section value is a (non-list) "non-false value".
+
+ From mustache(5):
+
+ When the value [of a section key] is non-false but not a list, it
+ will be used as the context for a single rendering of the block.
+
+ """
+ template = """{{#person}}Hi {{name}}{{/person}}"""
+ context = {"person": {"name": "Jon"}}
+ self._assert_rendered("Hi Jon", template, context)
+
+ def test_later_list_section_with_escapable_character(self):
+ """
+ This is a simple test case intended to cover issue #53.
+
+ The test case failed with markupsafe enabled, as follows:
+
+ AssertionError: Markup(u'foo &lt;') != 'foo <'
-if __name__ == '__main__':
- unittest.main()
+ """
+ template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}"""
+ context = {'s1': True, 's2': [True]}
+ self._assert_rendered("foo <", template, context)
diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py
new file mode 100644
index 0000000..6c2831a
--- /dev/null
+++ b/tests/test_renderengine.py
@@ -0,0 +1,455 @@
+# coding: utf-8
+
+"""
+Unit tests of renderengine.py.
+
+"""
+
+import cgi
+import unittest
+
+from pystache.context import Context
+from pystache.parser import ParsingError
+from pystache.renderengine import RenderEngine
+from tests.common import AssertStringMixin
+
+
+class RenderEngineTestCase(unittest.TestCase):
+
+ """Test the RenderEngine class."""
+
+ def test_init(self):
+ """
+ Test that __init__() stores all of the arguments correctly.
+
+ """
+ # In real-life, these arguments would be functions
+ engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
+
+ self.assertEquals(engine.escape, "escape")
+ self.assertEquals(engine.literal, "literal")
+ self.assertEquals(engine.load_partial, "foo")
+
+
+class RenderTests(unittest.TestCase, AssertStringMixin):
+
+ """
+ Tests RenderEngine.render().
+
+ Explicit spec-test-like tests best go in this class since the
+ RenderEngine class contains all parsing logic. This way, the unit tests
+ will be more focused and fail "closer to the code".
+
+ """
+
+ def _engine(self):
+ """
+ Create and return a default RenderEngine for testing.
+
+ """
+ escape = lambda s: unicode(cgi.escape(s))
+ engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
+ return engine
+
+ def _assert_render(self, expected, template, *context, **kwargs):
+ """
+ Test rendering the given template using the given context.
+
+ """
+ partials = kwargs.get('partials')
+ engine = kwargs.get('engine', self._engine())
+
+ if partials is not None:
+ engine.load_partial = lambda key: unicode(partials[key])
+
+ context = Context(*context)
+
+ actual = engine.render(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):
+ """
+ Test that render() uses the load_template attribute.
+
+ """
+ engine = self._engine()
+ partials = {'partial': u"{{person}}"}
+ engine.load_partial = lambda key: partials[key]
+
+ self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
+
+ def test__literal(self):
+ """
+ Test that render() uses the literal attribute.
+
+ """
+ engine = self._engine()
+ engine.literal = lambda s: s.upper()
+
+ self._assert_render(u'BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine)
+
+ def test_literal__sigil(self):
+ template = "<h1>{{& thing}}</h1>"
+ context = {'thing': 'Bear > Giraffe'}
+
+ expected = u"<h1>Bear > Giraffe</h1>"
+
+ self._assert_render(expected, template, context)
+
+ def test__escape(self):
+ """
+ Test that render() uses the escape attribute.
+
+ """
+ engine = self._engine()
+ engine.escape = lambda s: "**" + s
+
+ self._assert_render(u'**bar', '{{foo}}', {'foo': 'bar'}, engine=engine)
+
+ def test__escape_does_not_call_literal(self):
+ """
+ Test that render() does not call literal before or after calling escape.
+
+ """
+ engine = self._engine()
+ engine.literal = lambda s: s.upper() # a test version
+ engine.escape = lambda s: "**" + s
+
+ template = 'literal: {{{foo}}} escaped: {{foo}}'
+ context = {'foo': 'bar'}
+
+ self._assert_render(u'literal: BAR escaped: **bar', template, context, engine=engine)
+
+ def test__escape_preserves_unicode_subclasses(self):
+ """
+ Test that render() preserves unicode subclasses when passing to escape.
+
+ This is useful, for example, if one wants to respect whether a
+ variable value is markupsafe.Markup when escaping.
+
+ """
+ class MyUnicode(unicode):
+ pass
+
+ def escape(s):
+ if type(s) is MyUnicode:
+ return "**" + s
+ else:
+ return s + "**"
+
+ engine = self._engine()
+ engine.escape = escape
+
+ template = '{{foo1}} {{foo2}}'
+ context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'}
+
+ self._assert_render(u'**bar bar**', template, context, engine=engine)
+
+ def test__non_basestring__literal_and_escaped(self):
+ """
+ Test a context value that is not a basestring instance.
+
+ """
+ # We use include upper() to make sure we are actually using
+ # our custom function in the tests
+ to_unicode = lambda s: unicode(s, encoding='ascii').upper()
+ engine = self._engine()
+ engine.escape = to_unicode
+ engine.literal = to_unicode
+
+ self.assertRaises(TypeError, engine.literal, 100)
+
+ template = '{{text}} {{int}} {{{int}}}'
+ context = {'int': 100, 'text': 'foo'}
+
+ self._assert_render(u'FOO 100 100', template, context, engine=engine)
+
+ def test_tag__output_not_interpolated(self):
+ """
+ Context values should not be treated as templates (issue #44).
+
+ """
+ template = '{{template}}: {{planet}}'
+ context = {'template': '{{planet}}', 'planet': 'Earth'}
+ self._assert_render(u'{{planet}}: Earth', template, context)
+
+ def test_tag__output_not_interpolated__section(self):
+ """
+ Context values should not be treated as templates (issue #44).
+
+ """
+ template = '{{test}}'
+ context = {'test': '{{#hello}}'}
+ self._assert_render(u'{{#hello}}', template, context)
+
+ def test_interpolation__built_in_type__string(self):
+ """
+ Check tag interpolation with a string on the top of the context stack.
+
+ """
+ item = 'abc'
+ # item.upper() == 'ABC'
+ template = '{{#section}}{{upper}}{{/section}}'
+ context = {'section': item, 'upper': 'XYZ'}
+ self._assert_render(u'XYZ', template, context)
+
+ def test_interpolation__built_in_type__integer(self):
+ """
+ Check tag interpolation with an integer on the top of the context stack.
+
+ """
+ item = 10
+ # item.real == 10
+ template = '{{#section}}{{real}}{{/section}}'
+ context = {'section': item, 'real': 1000}
+ self._assert_render(u'1000', template, context)
+
+ def test_interpolation__built_in_type__list(self):
+ """
+ Check tag interpolation with a list on the top of the context stack.
+
+ """
+ item = [[1, 2, 3]]
+ # item[0].pop() == 3
+ template = '{{#section}}{{pop}}{{/section}}'
+ context = {'section': item, 'pop': 7}
+ self._assert_render(u'7', template, context)
+
+ def test_implicit_iterator__literal(self):
+ """
+ Test an implicit iterator in a literal tag.
+
+ """
+ template = """{{#test}}{{{.}}}{{/test}}"""
+ context = {'test': ['<', '>']}
+
+ self._assert_render(u'<>', template, context)
+
+ def test_implicit_iterator__escaped(self):
+ """
+ Test an implicit iterator in a normal tag.
+
+ """
+ template = """{{#test}}{{.}}{{/test}}"""
+ context = {'test': ['<', '>']}
+
+ self._assert_render(u'&lt;&gt;', template, context)
+
+ def test_literal__in_section(self):
+ """
+ Check that literals work in sections.
+
+ """
+ template = '{{#test}}1 {{{less_than}}} 2{{/test}}'
+ context = {'test': {'less_than': '<'}}
+
+ self._assert_render(u'1 < 2', template, context)
+
+ def test_literal__in_partial(self):
+ """
+ Check that literals work in partials.
+
+ """
+ template = '{{>partial}}'
+ partials = {'partial': '1 {{{less_than}}} 2'}
+ context = {'less_than': '<'}
+
+ self._assert_render(u'1 < 2', template, context, partials=partials)
+
+ def test_partial(self):
+ partials = {'partial': "{{person}}"}
+ self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials)
+
+ def test_partial__context_values(self):
+ """
+ Test that escape and literal work on context values in partials.
+
+ """
+ engine = self._engine()
+
+ template = '{{>partial}}'
+ partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'}
+ context = {'foo': '<'}
+
+ self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
+
+ ## Test cases related specifically to sections.
+
+ def test_section__end_tag_with_no_start_tag(self):
+ """
+ Check what happens if there is an end tag with no start tag.
+
+ """
+ template = '{{/section}}'
+ try:
+ self._assert_render(None, template)
+ except ParsingError, err:
+ self.assertEquals(str(err), "Section end tag mismatch: u'section' != None")
+
+ def test_section__end_tag_mismatch(self):
+ """
+ Check what happens if the end tag doesn't match.
+
+ """
+ template = '{{#section_start}}{{/section_end}}'
+ try:
+ self._assert_render(None, template)
+ except ParsingError, err:
+ self.assertEquals(str(err), "Section end tag mismatch: u'section_end' != u'section_start'")
+
+ def test_section__context_values(self):
+ """
+ Test that escape and literal work on context values in sections.
+
+ """
+ engine = self._engine()
+
+ template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}'
+ context = {'test': {'foo': '<'}}
+
+ self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine)
+
+ def test_section__context_precedence(self):
+ """
+ Check that items higher in the context stack take precedence.
+
+ """
+ template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}'
+ context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}}
+ self._assert_render(u'chicken : beans and rice', template, context)
+
+ def test_section__list_referencing_outer_context(self):
+ """
+ Check that list items can access the parent context.
+
+ For sections whose value is a list, check that items in the list
+ have access to the values inherited from the parent context
+ when rendering.
+
+ """
+ context = {
+ "greeting": "Hi",
+ "list": [{"name": "Al"}, {"name": "Bob"}],
+ }
+
+ template = "{{#list}}{{greeting}} {{name}}, {{/list}}"
+
+ self._assert_render(u"Hi Al, Hi Bob, ", template, context)
+
+ def test_section__output_not_interpolated(self):
+ """
+ Check that rendered section output is not interpolated.
+
+ """
+ template = '{{#section}}{{template}}{{/section}}: {{planet}}'
+ context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
+ self._assert_render(u'{{planet}}: Earth', template, context)
+
+ def test_section__nested_truthy(self):
+ """
+ Check that "nested truthy" sections get rendered.
+
+ Test case for issue #24: https://github.com/defunkt/pystache/issues/24
+
+ This test is copied from the spec. We explicitly include it to
+ prevent regressions for those who don't pull down the spec tests.
+
+ """
+ template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |'
+ context = {'bool': True}
+ self._assert_render(u'| A B C D E |', template, context)
+
+ def test_section__nested_with_same_keys(self):
+ """
+ Check a doubly-nested section with the same context key.
+
+ Test case for issue #36: https://github.com/defunkt/pystache/issues/36
+
+ """
+ # Start with an easier, working case.
+ template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}'
+ context = {'x': {'z': {'y': 1}}}
+ self._assert_render(u'1', template, context)
+
+ template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}'
+ context = {'x': {'x': {'y': 1}}}
+ self._assert_render(u'1', template, context)
+
+ def test_section__lambda(self):
+ template = '{{#test}}Mom{{/test}}'
+ context = {'test': (lambda text: 'Hi %s' % text)}
+ self._assert_render(u'Hi Mom', template, context)
+
+ def test_section__iterable(self):
+ """
+ Check that objects supporting iteration (aside from dicts) behave like lists.
+
+ """
+ template = '{{#iterable}}{{.}}{{/iterable}}'
+
+ context = {'iterable': (i for i in range(3))} # type 'generator'
+ self._assert_render(u'012', template, context)
+
+ context = {'iterable': xrange(4)} # type 'xrange'
+ self._assert_render(u'0123', template, context)
+
+ d = {'foo': 0, 'bar': 0}
+ # We don't know what order of keys we'll be given, but from the
+ # Python documentation:
+ # "If items(), keys(), values(), iteritems(), iterkeys(), and
+ # itervalues() are called with no intervening modifications to
+ # the dictionary, the lists will directly correspond."
+ expected = u''.join(d.keys())
+ context = {'iterable': d.iterkeys()} # type 'dictionary-keyiterator'
+ self._assert_render(expected, template, context)
+
+ def test_section__lambda__tag_in_output(self):
+ """
+ Check that callable output is treated as a template string (issue #46).
+
+ The spec says--
+
+ 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.
+
+ """
+ template = '{{#test}}Hi {{person}}{{/test}}'
+ context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
+ self._assert_render(u'Hi Mom :)', template, context)
+
+ def test_comment__multiline(self):
+ """
+ Check that multiline comments are permitted.
+
+ """
+ self._assert_render(u'foobar', 'foo{{! baz }}bar')
+ self._assert_render(u'foobar', 'foo{{! \nbaz }}bar')
+
+ def test_custom_delimiters__sections(self):
+ """
+ Check that custom delimiters can be used to start a section.
+
+ Test case for issue #20: https://github.com/defunkt/pystache/issues/20
+
+ """
+ template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]'
+ context = {'foo': True}
+ self._assert_render(u'bar', template, context)
+
+ def test_custom_delimiters__not_retroactive(self):
+ """
+ Check that changing custom delimiters back is not "retroactive."
+
+ Test case for issue #35: https://github.com/defunkt/pystache/issues/35
+
+ """
+ expected = u' {{foo}} '
+ self._assert_render(expected, '{{=$ $=}} {{foo}} ')
+ self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '.
diff --git a/tests/test_renderer.py b/tests/test_renderer.py
new file mode 100644
index 0000000..a69d11a
--- /dev/null
+++ b/tests/test_renderer.py
@@ -0,0 +1,556 @@
+# coding: utf-8
+
+"""
+Unit tests of template.py.
+
+"""
+
+import codecs
+import os
+import sys
+import unittest
+
+from examples.simple import Simple
+from pystache import Renderer
+from pystache import TemplateSpec
+from pystache.loader import Loader
+
+from tests.common import get_data_path
+from tests.common import AssertStringMixin
+from tests.data.views import SayHello
+
+
+class RendererInitTestCase(unittest.TestCase):
+
+ """
+ Tests the Renderer.__init__() method.
+
+ """
+
+ def test_partials__default(self):
+ """
+ Test the default value.
+
+ """
+ renderer = Renderer()
+ self.assertTrue(renderer.partials is None)
+
+ def test_partials(self):
+ """
+ Test that the attribute is set correctly.
+
+ """
+ renderer = Renderer(partials={'foo': 'bar'})
+ self.assertEquals(renderer.partials, {'foo': 'bar'})
+
+ def test_escape__default(self):
+ escape = Renderer().escape
+
+ self.assertEquals(escape(">"), "&gt;")
+ self.assertEquals(escape('"'), "&quot;")
+ # Single quotes are not escaped.
+ self.assertEquals(escape("'"), "'")
+
+ def test_escape(self):
+ escape = lambda s: "**" + s
+ renderer = Renderer(escape=escape)
+ self.assertEquals(renderer.escape("bar"), "**bar")
+
+ def test_decode_errors__default(self):
+ """
+ Check the default value.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.decode_errors, 'strict')
+
+ def test_decode_errors(self):
+ """
+ Check that the constructor sets the attribute correctly.
+
+ """
+ renderer = Renderer(decode_errors="foo")
+ self.assertEquals(renderer.decode_errors, "foo")
+
+ def test_file_encoding__default(self):
+ """
+ Check the file_encoding default.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.file_encoding, renderer.string_encoding)
+
+ def test_file_encoding(self):
+ """
+ Check that the file_encoding attribute is set correctly.
+
+ """
+ renderer = Renderer(file_encoding='foo')
+ self.assertEquals(renderer.file_encoding, 'foo')
+
+ def test_file_extension__default(self):
+ """
+ Check the file_extension default.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.file_extension, 'mustache')
+
+ def test_file_extension(self):
+ """
+ Check that the file_encoding attribute is set correctly.
+
+ """
+ renderer = Renderer(file_extension='foo')
+ self.assertEquals(renderer.file_extension, 'foo')
+
+ def test_search_dirs__default(self):
+ """
+ Check the search_dirs default.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.search_dirs, [os.curdir])
+
+ def test_search_dirs__string(self):
+ """
+ Check that the search_dirs attribute is set correctly when a string.
+
+ """
+ renderer = Renderer(search_dirs='foo')
+ self.assertEquals(renderer.search_dirs, ['foo'])
+
+ def test_search_dirs__list(self):
+ """
+ Check that the search_dirs attribute is set correctly when a list.
+
+ """
+ renderer = Renderer(search_dirs=['foo'])
+ self.assertEquals(renderer.search_dirs, ['foo'])
+
+ def test_string_encoding__default(self):
+ """
+ Check the default value.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.string_encoding, sys.getdefaultencoding())
+
+ def test_string_encoding(self):
+ """
+ Check that the constructor sets the attribute correctly.
+
+ """
+ renderer = Renderer(string_encoding="foo")
+ self.assertEquals(renderer.string_encoding, "foo")
+
+
+class RendererTests(unittest.TestCase, AssertStringMixin):
+
+ """Test the Renderer class."""
+
+ def _renderer(self):
+ return Renderer()
+
+ ## Test Renderer.unicode().
+
+ def test_unicode__string_encoding(self):
+ """
+ Test that the string_encoding attribute is respected.
+
+ """
+ renderer = Renderer()
+ s = "é"
+
+ renderer.string_encoding = "ascii"
+ self.assertRaises(UnicodeDecodeError, renderer.unicode, s)
+
+ renderer.string_encoding = "utf-8"
+ self.assertEquals(renderer.unicode(s), u"é")
+
+ def test_unicode__decode_errors(self):
+ """
+ Test that the decode_errors attribute is respected.
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = "ascii"
+ s = "déf"
+
+ renderer.decode_errors = "ignore"
+ self.assertEquals(renderer.unicode(s), "df")
+
+ renderer.decode_errors = "replace"
+ # U+FFFD is the official Unicode replacement character.
+ self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf')
+
+ ## Test the _make_loader() method.
+
+ def test__make_loader__return_type(self):
+ """
+ Test that _make_loader() returns a Loader.
+
+ """
+ renderer = Renderer()
+ loader = renderer._make_loader()
+
+ self.assertEquals(type(loader), Loader)
+
+ def test__make_loader__attributes(self):
+ """
+ Test that _make_loader() sets all attributes correctly..
+
+ """
+ unicode_ = lambda x: x
+
+ renderer = Renderer()
+ renderer.file_encoding = 'enc'
+ renderer.file_extension = 'ext'
+ renderer.unicode = unicode_
+
+ loader = renderer._make_loader()
+
+ self.assertEquals(loader.extension, 'ext')
+ self.assertEquals(loader.file_encoding, 'enc')
+ self.assertEquals(loader.to_unicode, unicode_)
+
+ ## Test the render() method.
+
+ def test_render__return_type(self):
+ """
+ Check that render() returns a string of type unicode.
+
+ """
+ renderer = Renderer()
+ rendered = renderer.render('foo')
+ self.assertEquals(type(rendered), unicode)
+
+ def test_render__unicode(self):
+ renderer = Renderer()
+ actual = renderer.render(u'foo')
+ self.assertEquals(actual, u'foo')
+
+ def test_render__str(self):
+ renderer = Renderer()
+ actual = renderer.render('foo')
+ self.assertEquals(actual, 'foo')
+
+ def test_render__non_ascii_character(self):
+ renderer = Renderer()
+ actual = renderer.render(u'Poincaré')
+ self.assertEquals(actual, u'Poincaré')
+
+ def test_render__context(self):
+ """
+ Test render(): passing a context.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom')
+
+ def test_render__context_and_kwargs(self):
+ """
+ Test render(): passing a context and **kwargs.
+
+ """
+ renderer = Renderer()
+ template = 'Hi {{person1}} and {{person2}}'
+ self.assertEquals(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad')
+
+ def test_render__kwargs_and_no_context(self):
+ """
+ Test render(): passing **kwargs and no context.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom')
+
+ def test_render__context_and_kwargs__precedence(self):
+ """
+ Test render(): **kwargs takes precedence over context.
+
+ """
+ renderer = Renderer()
+ self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad')
+
+ def test_render__kwargs_does_not_modify_context(self):
+ """
+ Test render(): passing **kwargs does not modify the passed context.
+
+ """
+ context = {}
+ renderer = Renderer()
+ renderer.render('Hi {{person}}', context=context, foo="bar")
+ self.assertEquals(context, {})
+
+ def test_render__nonascii_template(self):
+ """
+ Test passing a non-unicode template with non-ascii characters.
+
+ """
+ renderer = Renderer()
+ template = "déf"
+
+ # Check that decode_errors and string_encoding are both respected.
+ renderer.decode_errors = 'ignore'
+ renderer.string_encoding = 'ascii'
+ self.assertEquals(renderer.render(template), "df")
+
+ renderer.string_encoding = 'utf_8'
+ self.assertEquals(renderer.render(template), u"déf")
+
+ def test_make_load_partial(self):
+ """
+ Test the _make_load_partial() method.
+
+ """
+ renderer = Renderer()
+ renderer.partials = {'foo': 'bar'}
+ load_partial = renderer._make_load_partial()
+
+ actual = load_partial('foo')
+ self.assertEquals(actual, 'bar')
+ self.assertEquals(type(actual), unicode, "RenderEngine requires that "
+ "load_partial return unicode strings.")
+
+ def test_make_load_partial__unicode(self):
+ """
+ Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode.
+
+ """
+ renderer = Renderer()
+
+ renderer.partials = {'partial': 'foo'}
+ load_partial = renderer._make_load_partial()
+ self.assertEquals(load_partial("partial"), "foo")
+
+ # Now with a value that is already unicode.
+ renderer.partials = {'partial': u'foo'}
+ load_partial = renderer._make_load_partial()
+ # If the next line failed, we would get the following error:
+ # TypeError: decoding Unicode is not supported
+ self.assertEquals(load_partial("partial"), "foo")
+
+ def test_render_path(self):
+ """
+ Test the render_path() method.
+
+ """
+ renderer = Renderer()
+ path = get_data_path('say_hello.mustache')
+ actual = renderer.render_path(path, to='foo')
+ self.assertEquals(actual, "Hello, foo")
+
+ def test_render__object(self):
+ """
+ Test rendering an object instance.
+
+ """
+ renderer = Renderer()
+
+ say_hello = SayHello()
+ actual = renderer.render(say_hello)
+ self.assertEquals('Hello, World', actual)
+
+ actual = renderer.render(say_hello, to='Mars')
+ self.assertEquals('Hello, Mars', actual)
+
+ def test_render__template_spec(self):
+ """
+ Test rendering a TemplateSpec instance.
+
+ """
+ renderer = Renderer()
+
+ class Spec(TemplateSpec):
+ template = "hello, {{to}}"
+ to = 'world'
+
+ spec = Spec()
+ actual = renderer.render(spec)
+ self.assertString(actual, u'hello, world')
+
+ def test_render__view(self):
+ """
+ Test rendering a View instance.
+
+ """
+ renderer = Renderer()
+
+ view = Simple()
+ actual = renderer.render(view)
+ self.assertEquals('Hi pizza!', actual)
+
+
+# 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):
+
+ """
+ Check the RenderEngine returned by Renderer._make_render_engine().
+
+ """
+
+ ## Test the engine's load_partial attribute.
+
+ def test__load_partial__returns_unicode(self):
+ """
+ Check that load_partial returns unicode (and not a subclass).
+
+ """
+ class MyUnicode(unicode):
+ pass
+
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+ renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')}
+
+ engine = renderer._make_render_engine()
+
+ actual = engine.load_partial('str')
+ self.assertEquals(actual, "foo")
+ self.assertEquals(type(actual), unicode)
+
+ # Check that unicode subclasses are not preserved.
+ actual = engine.load_partial('subclass')
+ self.assertEquals(actual, "abc")
+ self.assertEquals(type(actual), unicode)
+
+ def test__load_partial__not_found(self):
+ """
+ Check that load_partial provides a nice message when a template is not found.
+
+ """
+ renderer = Renderer()
+ renderer.partials = {}
+
+ engine = renderer._make_render_engine()
+ load_partial = engine.load_partial
+
+ try:
+ load_partial("foo")
+ raise Exception("Shouldn't get here")
+ except Exception, err:
+ self.assertEquals(str(err), "Partial not found with name: 'foo'")
+
+ ## Test the engine's literal attribute.
+
+ def test__literal__uses_renderer_unicode(self):
+ """
+ Test that literal uses the renderer's unicode function.
+
+ """
+ renderer = Renderer()
+ renderer.unicode = lambda s: s.upper()
+
+ engine = renderer._make_render_engine()
+ literal = engine.literal
+
+ self.assertEquals(literal("foo"), "FOO")
+
+ def test__literal__handles_unicode(self):
+ """
+ Test that literal doesn't try to "double decode" unicode.
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+
+ engine = renderer._make_render_engine()
+ literal = engine.literal
+
+ self.assertEquals(literal(u"foo"), "foo")
+
+ def test__literal__returns_unicode(self):
+ """
+ Test that literal returns unicode (and not a subclass).
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+
+ engine = renderer._make_render_engine()
+ literal = engine.literal
+
+ self.assertEquals(type(literal("foo")), unicode)
+
+ class MyUnicode(unicode):
+ pass
+
+ s = MyUnicode("abc")
+
+ self.assertEquals(type(s), MyUnicode)
+ self.assertTrue(isinstance(s, unicode))
+ self.assertEquals(type(literal(s)), unicode)
+
+ ## Test the engine's escape attribute.
+
+ def test__escape__uses_renderer_escape(self):
+ """
+ Test that escape uses the renderer's escape function.
+
+ """
+ renderer = Renderer()
+ renderer.escape = lambda s: "**" + s
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ self.assertEquals(escape("foo"), "**foo")
+
+ def test__escape__uses_renderer_unicode(self):
+ """
+ Test that escape uses the renderer's unicode function.
+
+ """
+ renderer = Renderer()
+ renderer.unicode = lambda s: s.upper()
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ self.assertEquals(escape("foo"), "FOO")
+
+ def test__escape__has_access_to_original_unicode_subclass(self):
+ """
+ Test that escape receives strings with the unicode subclass intact.
+
+ """
+ renderer = Renderer()
+ renderer.escape = lambda s: type(s).__name__
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ class MyUnicode(unicode):
+ pass
+
+ self.assertEquals(escape("foo"), "unicode")
+ self.assertEquals(escape(u"foo"), "unicode")
+ self.assertEquals(escape(MyUnicode("foo")), "MyUnicode")
+
+ def test__escape__returns_unicode(self):
+ """
+ Test that literal returns unicode (and not a subclass).
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ self.assertEquals(type(escape("foo")), unicode)
+
+ # Check that literal doesn't preserve unicode subclasses.
+ class MyUnicode(unicode):
+ pass
+
+ s = MyUnicode("abc")
+
+ self.assertEquals(type(s), MyUnicode)
+ self.assertTrue(isinstance(s, unicode))
+ self.assertEquals(type(escape(s)), unicode)
+
diff --git a/tests/test_simple.py b/tests/test_simple.py
index 79e7a57..e19187f 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -1,49 +1,83 @@
import unittest
+
import pystache
+from pystache import Renderer
from examples.nested_context import NestedContext
-from examples.complex_view import ComplexView
+from examples.complex import Complex
from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial
from examples.simple import Simple
-class TestSimple(unittest.TestCase):
-
- def test_simple_render(self):
- self.assertEqual('herp', pystache.Template('{{derp}}', {'derp': 'herp'}).render())
-
+from tests.common import EXAMPLES_DIR
+from tests.common import AssertStringMixin
+
+
+class TestSimple(unittest.TestCase, AssertStringMixin):
+
def test_nested_context(self):
- view = NestedContext()
- self.assertEquals(pystache.Template('{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}', view).render(), "one and foo and two")
-
+ renderer = Renderer()
+ view = NestedContext(renderer)
+ view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}'
+
+ actual = renderer.render(view)
+ self.assertString(actual, u"one and foo and two")
+
def test_looping_and_negation_context(self):
- view = ComplexView()
- self.assertEquals(pystache.Template('{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}', view).render(), "Colors: red Colors: green Colors: blue ")
+ template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}'
+ context = Complex()
+
+ renderer = Renderer()
+ actual = renderer.render(template, context)
+ self.assertEquals(actual, "Colors: red Colors: green Colors: blue ")
def test_empty_context(self):
- view = ComplexView()
- self.assertEquals(pystache.Template('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me")
-
+ template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}'
+ self.assertEquals(pystache.Renderer().render(template), "Should see me")
+
def test_callables(self):
view = Lambdas()
- self.assertEquals(pystache.Template('{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}', view).render(), 'bar != bar. oh, it does!')
-
+ view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertString(actual, u'bar != bar. oh, it does!')
+
def test_rendering_partial(self):
- view = TemplatePartial()
- self.assertEquals(pystache.Template('{{>inner_partial}}', view).render(), 'Again, Welcome!')
-
- self.assertEquals(pystache.Template('{{#looping}}{{>inner_partial}} {{/looping}}', view).render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''')
-
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+
+ view = TemplatePartial(renderer=renderer)
+ view.template = '{{>inner_partial}}'
+
+ actual = renderer.render(view)
+ self.assertString(actual, u'Again, Welcome!')
+
+ view.template = '{{#looping}}{{>inner_partial}} {{/looping}}'
+ actual = renderer.render(view)
+ self.assertString(actual, u"Again, Welcome! Again, Welcome! Again, Welcome! ")
+
def test_non_existent_value_renders_blank(self):
view = Simple()
-
- self.assertEquals(pystache.Template('{{not_set}} {{blank}}', view).render(), ' ')
-
-
+ template = '{{not_set}} {{blank}}'
+ self.assertEquals(pystache.Renderer().render(template), ' ')
+
+
def test_template_partial_extension(self):
- view = TemplatePartial()
- view.template_extension = 'txt'
- self.assertEquals(view.render(), """Welcome
+ """
+ Side note:
+
+ From the spec--
+
+ Partial tags SHOULD be treated as standalone when appropriate.
+
+ In particular, this means that trailing newlines should be removed.
+
+ """
+ renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
+
+ view = TemplatePartial(renderer=renderer)
+
+ actual = renderer.render(view)
+ self.assertString(actual, u"""Welcome
-------
-Again, Welcome!
-""")
+## Again, Welcome! ##""")
diff --git a/tests/test_spec.py b/tests/test_spec.py
new file mode 100644
index 0000000..02f6080
--- /dev/null
+++ b/tests/test_spec.py
@@ -0,0 +1,99 @@
+# coding: utf-8
+
+"""
+Creates a unittest.TestCase for the tests defined in the mustache spec.
+
+"""
+
+# TODO: this module can be cleaned up somewhat.
+
+try:
+ # We deserialize the json form rather than the yaml form because
+ # json libraries are available for Python 2.4.
+ import json
+except:
+ # The json module is new in Python 2.6, whereas simplejson is
+ # compatible with earlier versions.
+ import simplejson as json
+
+import glob
+import os.path
+import unittest
+
+from pystache.renderer import Renderer
+
+
+root_path = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs')
+spec_paths = glob.glob(os.path.join(root_path, '*.json'))
+
+class MustacheSpec(unittest.TestCase):
+ pass
+
+def buildTest(testData, spec_filename):
+
+ name = testData['name']
+ description = testData['desc']
+
+ test_name = "%s (%s)" % (name, spec_filename)
+
+ def test(self):
+ template = testData['template']
+ partials = testData.has_key('partials') and testData['partials'] or {}
+ expected = testData['expected']
+ data = testData['data']
+
+ # Convert code strings to functions.
+ # TODO: make this section of code easier to understand.
+ new_data = {}
+ for key, val in data.iteritems():
+ if isinstance(val, dict) and val.get('__tag__') == 'code':
+ val = eval(val['python'])
+ new_data[key] = val
+
+ renderer = Renderer(partials=partials)
+ actual = renderer.render(template, new_data)
+ actual = actual.encode('utf-8')
+
+ message = """%s
+
+ Template: \"""%s\"""
+
+ Expected: %s
+ Actual: %s
+
+ Expected: \"""%s\"""
+ Actual: \"""%s\"""
+ """ % (description, template, repr(expected), repr(actual), expected, actual)
+
+ self.assertEquals(actual, expected, message)
+
+ # The name must begin with "test" for nosetests test discovery to work.
+ name = 'test: "%s"' % test_name
+
+ # If we don't convert unicode to str, we get the following error:
+ # "TypeError: __name__ must be set to a string object"
+ test.__name__ = str(name)
+
+ return test
+
+for spec_path in spec_paths:
+
+ file_name = os.path.basename(spec_path)
+
+ # We avoid use of the with keyword for Python 2.4 support.
+ f = open(spec_path, 'r')
+ try:
+ spec_data = json.load(f)
+ finally:
+ f.close()
+
+ tests = spec_data['tests']
+
+ for test in tests:
+ test = buildTest(test, file_name)
+ setattr(MustacheSpec, test.__name__, test)
+ # Prevent this variable from being interpreted as another test.
+ del(test)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py
new file mode 100644
index 0000000..9599c37
--- /dev/null
+++ b/tests/test_template_spec.py
@@ -0,0 +1,388 @@
+# coding: utf-8
+
+"""
+Unit tests for template_spec.py.
+
+"""
+
+import os.path
+import sys
+import unittest
+
+import examples
+from examples.simple import Simple
+from examples.complex import Complex
+from examples.lambdas import Lambdas
+from examples.inverted import Inverted, InvertedLists
+from pystache import Renderer
+from pystache import TemplateSpec
+from pystache.locator import Locator
+from pystache.loader import Loader
+from pystache.spec_loader import SpecLoader
+from tests.common import DATA_DIR
+from tests.common import EXAMPLES_DIR
+from tests.common import AssertIsMixin
+from tests.common import AssertStringMixin
+from tests.data.views import SampleView
+from tests.data.views import NonAscii
+
+
+class Thing(object):
+ pass
+
+
+class ViewTestCase(unittest.TestCase, AssertStringMixin):
+
+ def test_template_rel_directory(self):
+ """
+ Test that View.template_rel_directory is respected.
+
+ """
+ class Tagless(TemplateSpec):
+ pass
+
+ view = Tagless()
+ renderer = Renderer()
+
+ self.assertRaises(IOError, renderer.render, view)
+
+ view.template_rel_directory = "../examples"
+ actual = renderer.render(view)
+ self.assertEquals(actual, "No tags...")
+
+ def test_template_path_for_partials(self):
+ """
+ Test that View.template_rel_path is respected for partials.
+
+ """
+ spec = TemplateSpec()
+ spec.template = "Partial: {{>tagless}}"
+
+ renderer1 = Renderer()
+ renderer2 = Renderer(search_dirs=EXAMPLES_DIR)
+
+ self.assertRaises(IOError, renderer1.render, spec)
+
+ actual = renderer2.render(spec)
+ self.assertEquals(actual, "Partial: No tags...")
+
+ def test_basic_method_calls(self):
+ renderer = Renderer()
+ actual = renderer.render(Simple())
+
+ self.assertString(actual, u"Hi pizza!")
+
+ def test_non_callable_attributes(self):
+ view = Simple()
+ view.thing = 'Chris'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertEquals(actual, "Hi Chris!")
+
+ def test_complex(self):
+ renderer = Renderer()
+ actual = renderer.render(Complex())
+ self.assertString(actual, u"""\
+<h1>Colors</h1>
+<ul>
+<li><strong>red</strong></li>
+<li><a href="#Green">green</a></li>
+<li><a href="#Blue">blue</a></li>
+</ul>""")
+
+ def test_higher_order_replace(self):
+ renderer = Renderer()
+ actual = renderer.render(Lambdas())
+ self.assertEquals(actual, 'bar != bar. oh, it does!')
+
+ def test_higher_order_rot13(self):
+ view = Lambdas()
+ view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertString(actual, u'nopqrstuvwxyz')
+
+ def test_higher_order_lambda(self):
+ view = Lambdas()
+ view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertString(actual, u'abcdefghijklmnopqrstuvwxyz')
+
+ def test_partials_with_lambda(self):
+ view = Lambdas()
+ view.template = '{{>partial_with_lambda}}'
+
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+ actual = renderer.render(view)
+ self.assertEquals(actual, u'nopqrstuvwxyz')
+
+ def test_hierarchical_partials_with_lambdas(self):
+ view = Lambdas()
+ view.template = '{{>partial_with_partial_and_lambda}}'
+
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+ actual = renderer.render(view)
+ self.assertString(actual, u'nopqrstuvwxyznopqrstuvwxyz')
+
+ def test_inverted(self):
+ renderer = Renderer()
+ actual = renderer.render(Inverted())
+ self.assertString(actual, u"""one, two, three, empty list""")
+
+ def test_accessing_properties_on_parent_object_from_child_objects(self):
+ parent = Thing()
+ parent.this = 'derp'
+ parent.children = [Thing()]
+ view = Simple()
+ view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}"
+
+ renderer = Renderer()
+ actual = renderer.render(view, {'parent': parent})
+
+ self.assertString(actual, u'derp')
+
+ def test_inverted_lists(self):
+ renderer = Renderer()
+ actual = renderer.render(InvertedLists())
+ self.assertString(actual, u"""one, two, three, empty list""")
+
+
+class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+
+ """
+ Tests template_spec.SpecLoader.
+
+ """
+
+ def test_init__defaults(self):
+ custom = SpecLoader()
+
+ # Check the loader attribute.
+ loader = custom.loader
+ self.assertEquals(loader.extension, 'mustache')
+ self.assertEquals(loader.file_encoding, sys.getdefaultencoding())
+ # TODO: finish testing the other Loader attributes.
+ to_unicode = loader.to_unicode
+
+ def test_init__loader(self):
+ loader = Loader()
+ custom = SpecLoader(loader=loader)
+
+ self.assertIs(custom.loader, loader)
+
+ # TODO: rename to something like _assert_load().
+ def _assert_template(self, loader, custom, expected):
+ self.assertString(loader.load(custom), expected)
+
+ def test_load__template__type_str(self):
+ """
+ Test the template attribute: str string.
+
+ """
+ custom = TemplateSpec()
+ custom.template = "abc"
+
+ self._assert_template(SpecLoader(), custom, u"abc")
+
+ def test_load__template__type_unicode(self):
+ """
+ Test the template attribute: unicode string.
+
+ """
+ custom = TemplateSpec()
+ custom.template = u"abc"
+
+ self._assert_template(SpecLoader(), custom, u"abc")
+
+ def test_load__template__unicode_non_ascii(self):
+ """
+ Test the template attribute: non-ascii unicode string.
+
+ """
+ custom = TemplateSpec()
+ custom.template = u"é"
+
+ self._assert_template(SpecLoader(), custom, u"é")
+
+ def test_load__template__with_template_encoding(self):
+ """
+ Test the template attribute: with template encoding attribute.
+
+ """
+ custom = TemplateSpec()
+ custom.template = u'é'.encode('utf-8')
+
+ self.assertRaises(UnicodeDecodeError, self._assert_template, SpecLoader(), custom, u'é')
+
+ custom.template_encoding = 'utf-8'
+ self._assert_template(SpecLoader(), custom, u'é')
+
+ # TODO: make this test complete.
+ def test_load__template__correct_loader(self):
+ """
+ Test that reader.unicode() is called correctly.
+
+ This test tests that the correct reader is called with the correct
+ arguments. This is a catch-all test to supplement the other
+ test cases. It tests SpecLoader.load() independent of reader.unicode()
+ being implemented correctly (and tested).
+
+ """
+ class MockLoader(Loader):
+
+ def __init__(self):
+ self.s = None
+ self.encoding = None
+
+ # Overrides the existing method.
+ def unicode(self, s, encoding=None):
+ self.s = s
+ self.encoding = encoding
+ return u"foo"
+
+ loader = MockLoader()
+ custom_loader = SpecLoader()
+ custom_loader.loader = loader
+
+ view = TemplateSpec()
+ view.template = "template-foo"
+ view.template_encoding = "encoding-foo"
+
+ # Check that our unicode() above was called.
+ self._assert_template(custom_loader, view, u'foo')
+ self.assertEquals(loader.s, "template-foo")
+ self.assertEquals(loader.encoding, "encoding-foo")
+
+
+# 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):
+
+ # TODO: rename this method to _make_loader().
+ def _make_locator(self):
+ return SpecLoader()
+
+ def _assert_template_location(self, view, expected):
+ locator = self._make_locator()
+ actual = locator._find_relative(view)
+ self.assertEquals(actual, expected)
+
+ def test_find_relative(self):
+ """
+ Test _find_relative(): default behavior (no attributes set).
+
+ """
+ view = SampleView()
+ self._assert_template_location(view, (None, 'sample_view.mustache'))
+
+ def test_find_relative__template_rel_path__file_name_only(self):
+ """
+ Test _find_relative(): template_rel_path attribute.
+
+ """
+ view = SampleView()
+ view.template_rel_path = 'template.txt'
+ self._assert_template_location(view, ('', 'template.txt'))
+
+ def test_find_relative__template_rel_path__file_name_with_directory(self):
+ """
+ Test _find_relative(): template_rel_path attribute.
+
+ """
+ view = SampleView()
+ view.template_rel_path = 'foo/bar/template.txt'
+ self._assert_template_location(view, ('foo/bar', 'template.txt'))
+
+ def test_find_relative__template_rel_directory(self):
+ """
+ Test _find_relative(): template_rel_directory attribute.
+
+ """
+ view = SampleView()
+ view.template_rel_directory = 'foo'
+
+ self._assert_template_location(view, ('foo', 'sample_view.mustache'))
+
+ def test_find_relative__template_name(self):
+ """
+ Test _find_relative(): template_name attribute.
+
+ """
+ view = SampleView()
+ view.template_name = 'new_name'
+ self._assert_template_location(view, (None, 'new_name.mustache'))
+
+ def test_find_relative__template_extension(self):
+ """
+ Test _find_relative(): template_extension attribute.
+
+ """
+ view = SampleView()
+ view.template_extension = 'txt'
+ self._assert_template_location(view, (None, 'sample_view.txt'))
+
+ def test_find__with_directory(self):
+ """
+ Test _find() with a view that has a directory specified.
+
+ """
+ locator = self._make_locator()
+
+ view = SampleView()
+ view.template_rel_path = 'foo/bar.txt'
+ self.assertTrue(locator._find_relative(view)[0] is not None)
+
+ actual = locator._find(view)
+ expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt'))
+
+ self.assertEquals(actual, expected)
+
+ def test_find__without_directory(self):
+ """
+ Test _find() with a view that doesn't have a directory specified.
+
+ """
+ locator = self._make_locator()
+
+ view = SampleView()
+ self.assertTrue(locator._find_relative(view)[0] is None)
+
+ actual = locator._find(view)
+ expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
+
+ self.assertEquals(actual, expected)
+
+ def _assert_get_template(self, custom, expected):
+ locator = self._make_locator()
+ actual = locator.load(custom)
+
+ self.assertEquals(type(actual), unicode)
+ self.assertEquals(actual, expected)
+
+ def test_get_template(self):
+ """
+ Test get_template(): default behavior (no attributes set).
+
+ """
+ view = SampleView()
+
+ self._assert_get_template(view, u"ascii: abc")
+
+ def test_get_template__template_encoding(self):
+ """
+ Test get_template(): template_encoding attribute.
+
+ """
+ view = NonAscii()
+
+ self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo')
+
+ view.template_encoding = 'utf-8'
+ self._assert_get_template(view, u"non-ascii: é")
diff --git a/tests/test_view.py b/tests/test_view.py
deleted file mode 100644
index a3464df..0000000
--- a/tests/test_view.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import unittest
-import pystache
-
-from examples.simple import Simple
-from examples.complex_view import ComplexView
-from examples.lambdas import Lambdas
-from examples.inverted import Inverted, InvertedLists
-
-class Thing(object):
- pass
-
-class TestView(unittest.TestCase):
- def test_basic(self):
- view = Simple("Hi {{thing}}!", { 'thing': 'world' })
- self.assertEquals(view.render(), "Hi world!")
-
- def test_kwargs(self):
- view = Simple("Hi {{thing}}!", thing='world')
- self.assertEquals(view.render(), "Hi world!")
-
- def test_template_load(self):
- view = Simple(thing='world')
- self.assertEquals(view.render(), "Hi world!")
-
- def test_template_load_from_multiple_path(self):
- path = Simple.template_path
- Simple.template_path = ('examples/nowhere','examples',)
- try:
- view = Simple(thing='world')
- self.assertEquals(view.render(), "Hi world!")
- finally:
- Simple.template_path = path
-
- def test_template_load_from_multiple_path_fail(self):
- path = Simple.template_path
- Simple.template_path = ('examples/nowhere',)
- try:
- view = Simple(thing='world')
- self.assertRaises(IOError, view.render)
- finally:
- Simple.template_path = path
-
- def test_basic_method_calls(self):
- view = Simple()
- self.assertEquals(view.render(), "Hi pizza!")
-
- def test_non_callable_attributes(self):
- view = Simple()
- view.thing = 'Chris'
- self.assertEquals(view.render(), "Hi Chris!")
-
- def test_view_instances_as_attributes(self):
- other = Simple(name='chris')
- other.template = '{{name}}'
- view = Simple()
- view.thing = other
- self.assertEquals(view.render(), "Hi chris!")
-
- def test_complex(self):
- self.assertEquals(ComplexView().render(),
- """<h1>Colors</h1><ul><li><strong>red</strong></li><li><a href="#Green">green</a></li><li><a href="#Blue">blue</a></li></ul>""")
-
- def test_higher_order_replace(self):
- view = Lambdas()
- self.assertEquals(view.render(),
- 'bar != bar. oh, it does!')
-
- def test_higher_order_rot13(self):
- view = Lambdas()
- view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'
- self.assertEquals(view.render(), 'nopqrstuvwxyz')
-
- def test_higher_order_lambda(self):
- view = Lambdas()
- view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'
- self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz')
-
- def test_partials_with_lambda(self):
- view = Lambdas()
- view.template = '{{>partial_with_lambda}}'
- self.assertEquals(view.render(), 'nopqrstuvwxyz')
-
- def test_hierarchical_partials_with_lambdas(self):
- view = Lambdas()
- view.template = '{{>partial_with_partial_and_lambda}}'
- self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz')
-
- def test_inverted(self):
- view = Inverted()
- self.assertEquals(view.render(), """one, two, three, empty list""")
-
- def test_accessing_properties_on_parent_object_from_child_objects(self):
- parent = Thing()
- parent.this = 'derp'
- parent.children = [Thing()]
- view = Simple(context={'parent': parent})
- view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}"
-
- self.assertEquals(view.render(), 'derp')
-
- def test_context_returns_a_flattened_dict(self):
- view = Simple()
- view.context_list = [{'one':'1'}, {'two':'2'}, object()]
-
- self.assertEqual(view.context, {'one': '1', 'two': '2'})
-
- def test_inverted_lists(self):
- view = InvertedLists()
- self.assertEquals(view.render(), """one, two, three, empty list""")
-
-if __name__ == '__main__':
- unittest.main()