├── examples
├── __init__.py
├── escaped.mustache
├── inner_partial.mustache
├── simple.mustache
├── unescaped.mustache
├── unicode_output.mustache
├── inner_partial.txt
├── partial_with_lambda.mustache
├── template_partial.mustache
├── inverted.mustache
├── template_partial.txt
├── comments.mustache
├── unicode_input.mustache
├── partial_with_partial_and_lambda.mustache
├── double_section.mustache
├── lambdas.mustache
├── nested_context.mustache
├── delimiters.mustache
├── escaped.py
├── unescaped.py
├── comments.py
├── unicode_input.py
├── partials_with_lambdas.py
├── simple.py
├── unicode_output.py
├── double_section.py
├── template_partial.py
├── inverted.py
├── nested_context.py
├── complex_view.mustache
├── delimiters.py
├── complex_view.py
└── lambdas.py
├── MANIFEST.in
├── .gitignore
├── pystache
├── __init__.py
├── view.py
└── template.py
├── setup.py
├── HISTORY.rst
├── LICENSE
├── README.rst
└── tests
├── test_examples.py
├── test_pystache.py
└── test_view.py
/examples/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/escaped.mustache:
--------------------------------------------------------------------------------
1 |
{{title}}
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include HISTORY.rst README.rst
2 |
--------------------------------------------------------------------------------
/examples/inner_partial.mustache:
--------------------------------------------------------------------------------
1 | Again, {{title}}!
--------------------------------------------------------------------------------
/examples/simple.mustache:
--------------------------------------------------------------------------------
1 | Hi {{thing}}!{{blank}}
--------------------------------------------------------------------------------
/examples/unescaped.mustache:
--------------------------------------------------------------------------------
1 | {{{title}}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | build
3 | MANIFEST
4 | dist
5 |
--------------------------------------------------------------------------------
/examples/unicode_output.mustache:
--------------------------------------------------------------------------------
1 | Name: {{name}}
--------------------------------------------------------------------------------
/examples/inner_partial.txt:
--------------------------------------------------------------------------------
1 | ## Again, {{title}}! ##
2 |
--------------------------------------------------------------------------------
/examples/partial_with_lambda.mustache:
--------------------------------------------------------------------------------
1 | {{#rot13}}abcdefghijklm{{/rot13}}
--------------------------------------------------------------------------------
/examples/template_partial.mustache:
--------------------------------------------------------------------------------
1 | {{title}}
2 | {{>inner_partial}}
--------------------------------------------------------------------------------
/examples/inverted.mustache:
--------------------------------------------------------------------------------
1 | {{^f}}one{{/f}}, {{ two }}, {{^f}}three{{/f}}{{^t}}, four!{{/t}}
--------------------------------------------------------------------------------
/examples/template_partial.txt:
--------------------------------------------------------------------------------
1 | {{title}}
2 | {{title_bars}}
3 |
4 | {{>inner_partial}}
5 |
--------------------------------------------------------------------------------
/examples/comments.mustache:
--------------------------------------------------------------------------------
1 | {{title}}{{! just something interesting... #or not... }}
2 |
--------------------------------------------------------------------------------
/examples/unicode_input.mustache:
--------------------------------------------------------------------------------
1 | If alive today, Henri Poincaré would be {{age}} years old.
--------------------------------------------------------------------------------
/examples/partial_with_partial_and_lambda.mustache:
--------------------------------------------------------------------------------
1 | {{>partial_with_lambda}}{{#rot13}}abcdefghijklm{{/rot13}}
--------------------------------------------------------------------------------
/examples/double_section.mustache:
--------------------------------------------------------------------------------
1 | {{#t}}
2 | * first
3 | {{/t}}
4 | * {{two}}
5 | {{#t}}
6 | * third
7 | {{/t}}
--------------------------------------------------------------------------------
/examples/lambdas.mustache:
--------------------------------------------------------------------------------
1 | {{#replace_foo_with_bar}}
2 | foo != bar. oh, it does!
3 | {{/replace_foo_with_bar}}
--------------------------------------------------------------------------------
/examples/nested_context.mustache:
--------------------------------------------------------------------------------
1 | {{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}
2 |
--------------------------------------------------------------------------------
/examples/delimiters.mustache:
--------------------------------------------------------------------------------
1 | {{=<% %>=}}
2 | * <% first %>
3 | <%=| |=%>
4 | * | second |
5 | |={{ }}=|
6 | * {{ third }}
7 |
--------------------------------------------------------------------------------
/examples/escaped.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class Escaped(pystache.View):
4 | template_path = 'examples'
5 |
6 | def title(self):
7 | return "Bear > Shark"
8 |
--------------------------------------------------------------------------------
/examples/unescaped.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class Unescaped(pystache.View):
4 | template_path = 'examples'
5 |
6 | def title(self):
7 | return "Bear > Shark"
8 |
--------------------------------------------------------------------------------
/examples/comments.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class Comments(pystache.View):
4 | template_path = 'examples'
5 |
6 | def title(self):
7 | return "A Comedy of Errors"
8 |
--------------------------------------------------------------------------------
/examples/unicode_input.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class UnicodeInput(pystache.View):
4 | template_path = 'examples'
5 | template_encoding = 'utf8'
6 |
7 | def age(self):
8 | return 156
9 |
--------------------------------------------------------------------------------
/examples/partials_with_lambdas.py:
--------------------------------------------------------------------------------
1 | import pystache
2 | from examples.lambdas import rot
3 |
4 | class PartialsWithLambdas(pystache.View):
5 | template_path = 'examples'
6 |
7 | def rot(self):
8 | return rot
--------------------------------------------------------------------------------
/examples/simple.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class Simple(pystache.View):
4 | template_path = 'examples'
5 |
6 | def thing(self):
7 | return "pizza"
8 |
9 | def blank(self):
10 | pass
11 |
--------------------------------------------------------------------------------
/examples/unicode_output.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import pystache
4 |
5 | class UnicodeOutput(pystache.View):
6 | template_path = 'examples'
7 |
8 | def name(self):
9 | return u'Henri Poincaré'
10 |
--------------------------------------------------------------------------------
/examples/double_section.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class DoubleSection(pystache.View):
4 | template_path = 'examples'
5 |
6 | def t(self):
7 | return True
8 |
9 | def two(self):
10 | return "second"
11 |
--------------------------------------------------------------------------------
/examples/template_partial.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class TemplatePartial(pystache.View):
4 | template_path = 'examples'
5 |
6 | def title(self):
7 | return "Welcome"
8 |
9 | def title_bars(self):
10 | return '-' * len(self.title())
11 |
--------------------------------------------------------------------------------
/examples/inverted.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class Inverted(pystache.View):
4 | template_path = 'examples'
5 |
6 | def t(self):
7 | return True
8 |
9 | def f(self):
10 | return False
11 |
12 | def two(self):
13 | return 'two'
14 |
--------------------------------------------------------------------------------
/examples/nested_context.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class NestedContext(pystache.View):
4 | template_path = 'examples'
5 |
6 | def outer_thing(self):
7 | return "two"
8 |
9 | def foo(self):
10 | return {'thing1': 'one', 'thing2': 'foo'}
11 |
--------------------------------------------------------------------------------
/pystache/__init__.py:
--------------------------------------------------------------------------------
1 | from pystache.template import Template
2 | from pystache.view import View
3 |
4 | def render(template, context=None, **kwargs):
5 | context = context and context.copy() or {}
6 | context.update(kwargs)
7 | return Template(template, context).render()
8 |
--------------------------------------------------------------------------------
/examples/complex_view.mustache:
--------------------------------------------------------------------------------
1 | {{ header }}
2 | {{#list}}
3 |
4 | {{#item}}
5 | {{# current }}
6 | - {{name}}
7 | {{/ current }}
8 | {{#link}}
9 | - {{name}}
10 | {{/link}}
11 | {{/item}}
12 |
13 | {{/list}}
14 | {{#empty}}
15 | The list is empty.
16 | {{/empty}}
--------------------------------------------------------------------------------
/examples/delimiters.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class Delimiters(pystache.View):
4 | template_path = 'examples'
5 |
6 | def first(self):
7 | return "It worked the first time."
8 |
9 | def second(self):
10 | return "And it worked the second time."
11 |
12 | def third(self):
13 | return "Then, surprisingly, it worked the third time."
14 |
--------------------------------------------------------------------------------
/examples/complex_view.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | class ComplexView(pystache.View):
4 | template_path = 'examples'
5 |
6 | def header(self):
7 | return "Colors"
8 |
9 | def item(self):
10 | items = []
11 | items.append({ 'name': 'red', 'current': True, 'url': '#Red' })
12 | items.append({ 'name': 'green', 'link': True, 'url': '#Green' })
13 | items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' })
14 | return items
15 |
16 | def list(self):
17 | return not self.empty()
18 |
19 | def empty(self):
20 | return len(self.item()) == 0
21 |
--------------------------------------------------------------------------------
/examples/lambdas.py:
--------------------------------------------------------------------------------
1 | import pystache
2 |
3 | def rot(s, n=13):
4 | r = ""
5 | for c in s:
6 | cc = c
7 | if cc.isalpha():
8 | cc = cc.lower()
9 | o = ord(cc)
10 | ro = (o+n) % 122
11 | if ro == 0: ro = 122
12 | if ro < 97: ro += 96
13 | cc = chr(ro)
14 | r = ''.join((r,cc))
15 | return r
16 |
17 | def replace(subject, this='foo', with_this='bar'):
18 | return subject.replace(this, with_this)
19 |
20 | class Lambdas(pystache.View):
21 | template_path = 'examples'
22 |
23 | def replace_foo_with_bar(self, text=None):
24 | return replace
25 |
26 | def rot13(self, text=None):
27 | return rot
28 |
29 | def sort(self, text=None):
30 | return lambda text: ''.join(sorted(text))
31 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 | from distutils.core import setup
7 |
8 | def publish():
9 | """Publish to Pypi"""
10 | os.system("python setup.py sdist upload")
11 |
12 | if sys.argv[-1] == "publish":
13 | publish()
14 | sys.exit()
15 |
16 | setup(name='pystache',
17 | version='0.3.1',
18 | description='Mustache for Python',
19 | long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
20 | author='Chris Wanstrath',
21 | author_email='chris@ozmm.org',
22 | url='http://github.com/defunkt/pystache',
23 | packages=['pystache'],
24 | license='MIT',
25 | classifiers = (
26 | "Development Status :: 4 - Beta",
27 | "License :: OSI Approved :: MIT License",
28 | "Programming Language :: Python",
29 | "Programming Language :: Python :: 2.5",
30 | "Programming Language :: Python :: 2.6",
31 | )
32 | )
33 |
34 |
--------------------------------------------------------------------------------
/HISTORY.rst:
--------------------------------------------------------------------------------
1 | History
2 | =======
3 |
4 | 0.3.1 (2010-05-07)
5 | ------------------
6 |
7 | * Fix package
8 |
9 | 0.3.0 (2010-05-03)
10 | ------------------
11 |
12 | * View.template_path can now hold a list of path
13 | * Add {{& blah}} as an alias for {{{ blah }}}
14 | * Higher Order Sections
15 | * Inverted sections
16 |
17 | 0.2.0 (2010-02-15)
18 | ------------------
19 |
20 | * Bugfix: Methods returning False or None are not rendered
21 | * Bugfix: Don't render an empty string when a tag's value is 0. [enaeseth]
22 | * Add support for using non-callables as View attributes. [joshthecoder]
23 | * Allow using View instances as attributes. [joshthecoder]
24 | * Support for Unicode and non-ASCII-encoded bytestring output. [enaeseth]
25 | * Template file encoding awareness. [enaeseth]
26 |
27 | 0.1.1 (2009-11-13)
28 | ------------------
29 |
30 | * Ensure we're dealing with strings, always
31 | * Tests can be run by executing the test file directly
32 |
33 | 0.1.0 (2009-11-12)
34 | ------------------
35 |
36 | * First release
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 Chris Wanstrath
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ========
2 | Pystache
3 | ========
4 |
5 | Inspired by ctemplate_ and et_, Mustache_ is a
6 | framework-agnostic way to render logic-free views.
7 |
8 | As ctemplates says, "It emphasizes separating logic from presentation:
9 | it is impossible to embed application logic in this template language."
10 |
11 | Pystache is a Python implementation of Mustache. Pystache requires
12 | Python 2.6.
13 |
14 | Documentation
15 | =============
16 |
17 | The different Mustache tags are documented at `mustache(5)`_.
18 |
19 | Install It
20 | ==========
21 |
22 | ::
23 |
24 | pip install pystache
25 |
26 |
27 | Use It
28 | ======
29 |
30 | ::
31 |
32 | >>> import pystache
33 | >>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
34 | 'Hi Mom!'
35 |
36 | You can also create dedicated view classes to hold your view logic.
37 |
38 | Here's your simple.py::
39 |
40 | import pystache
41 | class Simple(pystache.View):
42 | def thing(self):
43 | return "pizza"
44 |
45 | Then your template, simple.mustache::
46 |
47 | Hi {{thing}}!
48 |
49 | Pull it together::
50 |
51 | >>> Simple().render()
52 | 'Hi pizza!'
53 |
54 |
55 | Test It
56 | =======
57 |
58 | nose_ works great! ::
59 |
60 | pip install nose
61 | cd pystache
62 | nosetests
63 |
64 |
65 | Author
66 | ======
67 |
68 | ::
69 |
70 | context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
71 | pystache.render("{{author}} :: {{email}}", context)
72 |
73 |
74 | .. _ctemplate: http://code.google.com/p/google-ctemplate/
75 | .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
76 | .. _Mustache: http://defunkt.github.com/mustache/
77 | .. _mustache(5): http://mustache.github.com/mustache.5.html
78 | .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
--------------------------------------------------------------------------------
/tests/test_examples.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import unittest
4 | import pystache
5 |
6 | from examples.comments import Comments
7 | from examples.double_section import DoubleSection
8 | from examples.escaped import Escaped
9 | from examples.unescaped import Unescaped
10 | from examples.template_partial import TemplatePartial
11 | from examples.delimiters import Delimiters
12 | from examples.unicode_output import UnicodeOutput
13 | from examples.unicode_input import UnicodeInput
14 | from examples.nested_context import NestedContext
15 |
16 | class TestView(unittest.TestCase):
17 | def test_comments(self):
18 | self.assertEquals(Comments().render(), """A Comedy of Errors
19 | """)
20 |
21 | def test_double_section(self):
22 | self.assertEquals(DoubleSection().render(), """* first
23 | * second
24 | * third""")
25 |
26 | def test_unicode_output(self):
27 | self.assertEquals(UnicodeOutput().render(), u'Name: Henri Poincaré
')
28 |
29 | def test_encoded_output(self):
30 | self.assertEquals(UnicodeOutput().render('utf8'), 'Name: Henri Poincar\xc3\xa9
')
31 |
32 | def test_unicode_input(self):
33 | self.assertEquals(UnicodeInput().render(),
34 | u'If alive today, Henri Poincaré would be 156 years old.
')
35 |
36 | def test_escaped(self):
37 | self.assertEquals(Escaped().render(), "Bear > Shark
")
38 |
39 | def test_unescaped(self):
40 | self.assertEquals(Unescaped().render(), "Bear > Shark
")
41 |
42 | def test_unescaped_sigil(self):
43 | view = Escaped(template="{{& thing}}
", context={
44 | 'thing': 'Bear > Giraffe'
45 | })
46 | self.assertEquals(view.render(), "Bear > Giraffe
")
47 |
48 | def test_template_partial(self):
49 | self.assertEquals(TemplatePartial().render(), """Welcome
50 | Again, Welcome!""")
51 |
52 | def test_template_partial_extension(self):
53 | view = TemplatePartial()
54 | view.template_extension = 'txt'
55 | self.assertEquals(view.render(), """Welcome
56 | -------
57 |
58 | Again, Welcome!
59 | """)
60 |
61 |
62 | def test_delimiters(self):
63 | self.assertEquals(Delimiters().render(), """
64 | * It worked the first time.
65 |
66 | * And it worked the second time.
67 |
68 | * Then, surprisingly, it worked the third time.
69 | """)
70 |
71 | def test_nested_context(self):
72 | self.assertEquals(NestedContext().render(), "one and foo and two")
73 |
74 | if __name__ == '__main__':
75 | unittest.main()
76 |
--------------------------------------------------------------------------------
/tests/test_pystache.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import unittest
4 | import pystache
5 |
6 | class TestPystache(unittest.TestCase):
7 | def test_basic(self):
8 | ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
9 | self.assertEquals(ret, "Hi world!")
10 |
11 | def test_kwargs(self):
12 | ret = pystache.render("Hi {{thing}}!", thing='world')
13 | self.assertEquals(ret, "Hi world!")
14 |
15 | def test_less_basic(self):
16 | template = "It's a nice day for {{beverage}}, right {{person}}?"
17 | ret = pystache.render(template, { 'beverage': 'soda', 'person': 'Bob' })
18 | self.assertEquals(ret, "It's a nice day for soda, right Bob?")
19 |
20 | def test_even_less_basic(self):
21 | template = "I think {{name}} wants a {{thing}}, right {{name}}?"
22 | ret = pystache.render(template, { 'name': 'Jon', 'thing': 'racecar' })
23 | self.assertEquals(ret, "I think Jon wants a racecar, right Jon?")
24 |
25 | def test_ignores_misses(self):
26 | template = "I think {{name}} wants a {{thing}}, right {{name}}?"
27 | ret = pystache.render(template, { 'name': 'Jon' })
28 | self.assertEquals(ret, "I think Jon wants a , right Jon?")
29 |
30 | def test_render_zero(self):
31 | template = 'My value is {{value}}.'
32 | ret = pystache.render(template, { 'value': 0 })
33 | self.assertEquals(ret, 'My value is 0.')
34 |
35 | def test_comments(self):
36 | template = "What {{! the }} what?"
37 | ret = pystache.render(template)
38 | self.assertEquals(ret, "What what?")
39 |
40 | def test_false_sections_are_hidden(self):
41 | template = "Ready {{#set}}set {{/set}}go!"
42 | ret = pystache.render(template, { 'set': False })
43 | self.assertEquals(ret, "Ready go!")
44 |
45 | def test_true_sections_are_shown(self):
46 | template = "Ready {{#set}}set{{/set}} go!"
47 | ret = pystache.render(template, { 'set': True })
48 | self.assertEquals(ret, "Ready set go!")
49 |
50 | def test_non_strings(self):
51 | template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
52 | stats = []
53 | stats.append({'key': 123, 'value': ['something']})
54 | stats.append({'key': u"chris", 'value': 0.900})
55 |
56 | ret = pystache.render(template, { 'stats': stats })
57 | self.assertEquals(ret, """(123 & ['something'])(chris & 0.9)""")
58 |
59 | def test_unicode(self):
60 | template = 'Name: {{name}}; Age: {{age}}'
61 | ret = pystache.render(template, { 'name': u'Henri Poincaré',
62 | 'age': 156 })
63 | self.assertEquals(ret, u'Name: Henri Poincaré; Age: 156')
64 |
65 | def test_sections(self):
66 | template = """
67 |
68 | {{#users}}
69 | - {{name}}
70 | {{/users}}
71 |
72 | """
73 |
74 | context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] }
75 | ret = pystache.render(template, context)
76 | self.assertEquals(ret, """
77 |
80 | """)
81 |
82 | if __name__ == '__main__':
83 | unittest.main()
84 |
--------------------------------------------------------------------------------
/tests/test_view.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import pystache
3 |
4 | from examples.simple import Simple
5 | from examples.complex_view import ComplexView
6 | from examples.lambdas import Lambdas
7 | from examples.inverted import Inverted
8 |
9 | class TestView(unittest.TestCase):
10 | def test_basic(self):
11 | view = Simple("Hi {{thing}}!", { 'thing': 'world' })
12 | self.assertEquals(view.render(), "Hi world!")
13 |
14 | def test_kwargs(self):
15 | view = Simple("Hi {{thing}}!", thing='world')
16 | self.assertEquals(view.render(), "Hi world!")
17 |
18 | def test_template_load(self):
19 | view = Simple(thing='world')
20 | self.assertEquals(view.render(), "Hi world!")
21 |
22 | def test_template_load_from_multiple_path(self):
23 | path = Simple.template_path
24 | Simple.template_path = ('examples/nowhere','examples',)
25 | try:
26 | view = Simple(thing='world')
27 | self.assertEquals(view.render(), "Hi world!")
28 | finally:
29 | Simple.template_path = path
30 |
31 | def test_template_load_from_multiple_path_fail(self):
32 | path = Simple.template_path
33 | Simple.template_path = ('examples/nowhere',)
34 | try:
35 | view = Simple(thing='world')
36 | self.assertRaises(IOError, view.render)
37 | finally:
38 | Simple.template_path = path
39 |
40 | def test_basic_method_calls(self):
41 | view = Simple()
42 | self.assertEquals(view.render(), "Hi pizza!")
43 |
44 | def test_non_callable_attributes(self):
45 | view = Simple()
46 | view.thing = 'Chris'
47 | self.assertEquals(view.render(), "Hi Chris!")
48 |
49 | def test_view_instances_as_attributes(self):
50 | other = Simple(name='chris')
51 | other.template = '{{name}}'
52 | view = Simple()
53 | view.thing = other
54 | self.assertEquals(view.render(), "Hi chris!")
55 |
56 | def test_complex(self):
57 | self.assertEquals(ComplexView().render(), """Colors
58 |
62 | """)
63 |
64 | def test_higher_order_replace(self):
65 | view = Lambdas()
66 | self.assertEquals(view.render(),
67 | 'bar != bar. oh, it does!')
68 |
69 | def test_higher_order_rot13(self):
70 | view = Lambdas()
71 | view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'
72 | self.assertEquals(view.render(), 'nopqrstuvwxyz')
73 |
74 | def test_higher_order_lambda(self):
75 | view = Lambdas()
76 | view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'
77 | self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz')
78 |
79 | def test_partials_with_lambda(self):
80 | view = Lambdas()
81 | view.template = '{{>partial_with_lambda}}'
82 | self.assertEquals(view.render(), 'nopqrstuvwxyz')
83 |
84 | def test_hierarchical_partials_with_lambdas(self):
85 | view = Lambdas()
86 | view.template = '{{>partial_with_partial_and_lambda}}'
87 | self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz')
88 |
89 | def test_inverted(self):
90 | view = Inverted()
91 | self.assertEquals(view.render(), """one, two, three""")
92 |
93 |
94 | if __name__ == '__main__':
95 | unittest.main()
96 |
--------------------------------------------------------------------------------
/pystache/view.py:
--------------------------------------------------------------------------------
1 | from pystache import Template
2 | import os.path
3 | import re
4 | from types import *
5 |
6 | class View(object):
7 | # Path where this view's template(s) live
8 | template_path = '.'
9 |
10 | # Extension for templates
11 | template_extension = 'mustache'
12 |
13 | # The name of this template. If none is given the View will try
14 | # to infer it based on the class name.
15 | template_name = None
16 |
17 | # Absolute path to the template itself. Pystache will try to guess
18 | # if it's not provided.
19 | template_file = None
20 |
21 | # Contents of the template.
22 | template = None
23 |
24 | # Character encoding of the template file. If None, Pystache will not
25 | # do any decoding of the template.
26 | template_encoding = None
27 |
28 | def __init__(self, template=None, context=None, **kwargs):
29 | self.template = template
30 | self.context = context or {}
31 |
32 | # If the context we're handed is a View, we want to inherit
33 | # its settings.
34 | if isinstance(context, View):
35 | self.inherit_settings(context)
36 |
37 | if kwargs:
38 | self.context.update(kwargs)
39 |
40 | def inherit_settings(self, view):
41 | """Given another View, copies its settings."""
42 | if view.template_path:
43 | self.template_path = view.template_path
44 |
45 | if view.template_name:
46 | self.template_name = view.template_name
47 |
48 | def load_template(self):
49 | if self.template:
50 | return self.template
51 |
52 | if self.template_file:
53 | return self._load_template()
54 |
55 | name = self.get_template_name() + '.' + self.template_extension
56 |
57 | if isinstance(self.template_path, basestring):
58 | self.template_file = os.path.join(self.template_path, name)
59 | return self._load_template()
60 |
61 | for path in self.template_path:
62 | self.template_file = os.path.join(path, name)
63 | if os.path.exists(self.template_file):
64 | return self._load_template()
65 |
66 | raise IOError('"%s" not found in "%s"' % (name, ':'.join(self.template_path),))
67 |
68 |
69 | def _load_template(self):
70 | f = open(self.template_file, 'r')
71 | try:
72 | template = f.read()
73 | if self.template_encoding:
74 | template = unicode(template, self.template_encoding)
75 | finally:
76 | f.close()
77 | return template
78 |
79 | def get_template_name(self, name=None):
80 | """TemplatePartial => template_partial
81 | Takes a string but defaults to using the current class' name or
82 | the `template_name` attribute
83 | """
84 | if self.template_name:
85 | return self.template_name
86 |
87 | if not name:
88 | name = self.__class__.__name__
89 |
90 | def repl(match):
91 | return '_' + match.group(0).lower()
92 |
93 | return re.sub('[A-Z]', repl, name)[1:]
94 |
95 | def __contains__(self, needle):
96 | return needle in self.context or hasattr(self, needle)
97 |
98 | def __getitem__(self, attr):
99 | val = self.get(attr, None)
100 | if not val:
101 | raise KeyError("No such key.")
102 | return val
103 |
104 | def get(self, attr, default):
105 | attr = self.context.get(attr, getattr(self, attr, default))
106 |
107 | if hasattr(attr, '__call__') and type(attr) is UnboundMethodType:
108 | return attr()
109 | else:
110 | return attr
111 |
112 | def render(self, encoding=None):
113 | template = self.load_template()
114 | return Template(template, self).render(encoding=encoding)
115 |
116 | def __str__(self):
117 | return self.render()
118 |
--------------------------------------------------------------------------------
/pystache/template.py:
--------------------------------------------------------------------------------
1 | import re
2 | import cgi
3 | import collections
4 |
5 | modifiers = {}
6 | def modifier(symbol):
7 | """Decorator for associating a function with a Mustache tag modifier.
8 |
9 | @modifier('P')
10 | def render_tongue(self, tag_name=None, context=None):
11 | return ":P %s" % tag_name
12 |
13 | {{P yo }} => :P yo
14 | """
15 | def set_modifier(func):
16 | modifiers[symbol] = func
17 | return func
18 | return set_modifier
19 |
20 |
21 | def get_or_attr(obj, name, default=None):
22 | try:
23 | return obj[name]
24 | except KeyError:
25 | return default
26 | except:
27 | try:
28 | return getattr(obj, name)
29 | except AttributeError:
30 | return default
31 |
32 |
33 | class Template(object):
34 | # The regular expression used to find a #section
35 | section_re = None
36 |
37 | # The regular expression used to find a tag.
38 | tag_re = None
39 |
40 | # Opening tag delimiter
41 | otag = '{{'
42 |
43 | # Closing tag delimiter
44 | ctag = '}}'
45 |
46 | def __init__(self, template, context=None):
47 | self.template = template
48 | self.context = context or {}
49 | self.compile_regexps()
50 |
51 | def render(self, template=None, context=None, encoding=None):
52 | """Turns a Mustache template into something wonderful."""
53 | template = template or self.template
54 | context = context or self.context
55 |
56 | template = self.render_sections(template, context)
57 | result = self.render_tags(template, context)
58 | if encoding is not None:
59 | result = result.encode(encoding)
60 | return result
61 |
62 | def compile_regexps(self):
63 | """Compiles our section and tag regular expressions."""
64 | tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) }
65 |
66 | section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?)\s*%(otag)s/\1%(ctag)s"
67 | self.section_re = re.compile(section % tags, re.M|re.S)
68 |
69 | tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
70 | self.tag_re = re.compile(tag % tags)
71 |
72 | def render_sections(self, template, context):
73 | """Expands sections."""
74 | while 1:
75 | match = self.section_re.search(template)
76 | if match is None:
77 | break
78 |
79 | section, section_name, inner = match.group(0, 1, 2)
80 | section_name = section_name.strip()
81 |
82 | it = get_or_attr(context, section_name, None)
83 | replacer = ''
84 | if it and isinstance(it, collections.Callable):
85 | replacer = it(inner)
86 | elif it and not hasattr(it, '__iter__'):
87 | if section[2] != '^':
88 | replacer = inner
89 | elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
90 | if section[2] != '^':
91 | replacer = self.render(inner, it)
92 | elif it:
93 | insides = []
94 | for item in it:
95 | insides.append(self.render(inner, item))
96 | replacer = ''.join(insides)
97 | elif not it and section[2] == '^':
98 | replacer = inner
99 |
100 | template = template.replace(section, replacer)
101 |
102 | return template
103 |
104 | def render_tags(self, template, context):
105 | """Renders all the tags in a template for a context."""
106 | while 1:
107 | match = self.tag_re.search(template)
108 | if match is None:
109 | break
110 |
111 | tag, tag_type, tag_name = match.group(0, 1, 2)
112 | tag_name = tag_name.strip()
113 | func = modifiers[tag_type]
114 | replacement = func(self, tag_name, context)
115 | template = template.replace(tag, replacement)
116 |
117 | return template
118 |
119 | @modifier(None)
120 | def render_tag(self, tag_name, context):
121 | """Given a tag name and context, finds, escapes, and renders the tag."""
122 | raw = get_or_attr(context, tag_name, '')
123 | if not raw and raw is not 0:
124 | return ''
125 | return cgi.escape(unicode(raw))
126 |
127 | @modifier('!')
128 | def render_comment(self, tag_name=None, context=None):
129 | """Rendering a comment always returns nothing."""
130 | return ''
131 |
132 | @modifier('{')
133 | @modifier('&')
134 | def render_unescaped(self, tag_name=None, context=None):
135 | """Render a tag without escaping it."""
136 | return unicode(get_or_attr(context, tag_name, ''))
137 |
138 | @modifier('>')
139 | def render_partial(self, tag_name=None, context=None):
140 | """Renders a partial within the current context."""
141 | # Import view here to avoid import loop
142 | from pystache.view import View
143 |
144 | view = View(context=context)
145 | view.template_name = tag_name
146 |
147 | return view.render()
148 |
149 | @modifier('=')
150 | def render_delimiter(self, tag_name=None, context=None):
151 | """Changes the Mustache delimiter."""
152 | self.otag, self.ctag = tag_name.split(' ')
153 | self.compile_regexps()
154 | return ''
155 |
--------------------------------------------------------------------------------