├── 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 | 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 | 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 | --------------------------------------------------------------------------------