Skip to content
Browse files

Merge branch 'master' of git://github.com/defunkt/pystache

  • Loading branch information...
2 parents 038abdb + 613a8cd commit 1f8eacb58d27514a9776d669cfb0dfb62d6cd794 @mikeal committed Nov 16, 2009
View
3 .gitignore
@@ -1 +1,4 @@
*.pyc
+build
+MANIFEST
+dist
View
8 HISTORY.md
@@ -0,0 +1,8 @@
+## 0.1.1 (2009-11-13)
+
+* Ensure we're dealing with strings, always
+* Tests can be run by executing the test file directly
+
+## 0.1.0 (2009-11-12)
+
+* First release
View
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Chris Wanstrath
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
2 README.md
@@ -29,7 +29,7 @@ You can also create dedicated view classes to hold your view logic.
Here's your simple.py:
import pystache
class Simple(pystache.View):
- def thing(self):
+ def thing(self):
return "pizza"
Then your template, simple.mustache:
View
3 TODO
@@ -1,4 +1 @@
-[ ] Partials
[ ] Sphinx docs
-[ ] Pypi Package
-[ ] More examples
View
1 examples/comments.mustache
@@ -0,0 +1 @@
+<h1>{{title}}{{! just something interesting... #or not... }}</h1>
View
7 examples/comments.py
@@ -0,0 +1,7 @@
+import pystache
+
+class Comments(pystache.View):
+ template_path = 'examples'
+
+ def title(self):
+ return "A Comedy of Errors"
View
6 examples/complex_view.mustache
@@ -1,10 +1,10 @@
-<h1>{{header}}</h1>
+<h1>{{ header }}</h1>
{{#list}}
<ul>
{{#item}}
- {{#current}}
+ {{# current }}
<li><strong>{{name}}</strong></li>
- {{/current}}
+ {{/ current }}
{{#link}}
<li><a href="{{url}}">{{name}}</a></li>
{{/link}}
View
6 examples/delimiters.mustache
@@ -0,0 +1,6 @@
+{{=<% %>=}}
+* <% first %>
+<%=| |=%>
+* | second |
+|={{ }}=|
+* {{ third }}
View
13 examples/delimiters.py
@@ -0,0 +1,13 @@
+import pystache
+
+class Delimiters(pystache.View):
+ template_path = 'examples'
+
+ def first(self):
+ return "It worked the first time."
+
+ def second(self):
+ return "And it worked the second time."
+
+ def third(self):
+ return "Then, surprisingly, it worked the third time."
View
7 examples/double_section.mustache
@@ -0,0 +1,7 @@
+{{#t}}
+ * first
+{{/t}}
+* {{two}}
+{{#t}}
+ * third
+{{/t}}
View
10 examples/double_section.py
@@ -0,0 +1,10 @@
+import pystache
+
+class DoubleSection(pystache.View):
+ template_path = 'examples'
+
+ def t(self):
+ return True
+
+ def two(self):
+ return "second"
View
1 examples/escaped.mustache
@@ -0,0 +1 @@
+<h1>{{title}}</h1>
View
7 examples/escaped.py
@@ -0,0 +1,7 @@
+import pystache
+
+class Escaped(pystache.View):
+ template_path = 'examples'
+
+ def title(self):
+ return "Bear > Shark"
View
1 examples/inner_partial.mustache
@@ -0,0 +1 @@
+Again, {{title}}!
View
1 examples/inner_partial.txt
@@ -0,0 +1 @@
+## Again, {{title}}! ##
View
2 examples/template_partial.mustache
@@ -0,0 +1,2 @@
+<h1>{{title}}</h1>
+{{>inner_partial}}
View
10 examples/template_partial.py
@@ -0,0 +1,10 @@
+import pystache
+
+class TemplatePartial(pystache.View):
+ template_path = 'examples'
+
+ def title(self):
+ return "Welcome"
+
+ def title_bars(self):
+ return '-' * len(self.title())
View
4 examples/template_partial.txt
@@ -0,0 +1,4 @@
+{{title}}
+{{title_bars}}
+
+{{>inner_partial}}
View
1 examples/unescaped.mustache
@@ -0,0 +1 @@
+<h1>{{{title}}}</h1>
View
7 examples/unescaped.py
@@ -0,0 +1,7 @@
+import pystache
+
+class Unescaped(pystache.View):
+ template_path = 'examples'
+
+ def title(self):
+ return "Bear > Shark"
View
86 pystache/template.py
@@ -1,17 +1,38 @@
import re
+import cgi
-SECTION_RE = re.compile(r"{{\#([^\}]*)}}\s*(.+?)\s*{{/\1}}", re.M | re.S)
-TAG_RE = re.compile(r"{{(#|=|!|<|>|\{)?(.+?)\1?}}+")
+modifiers = {}
+def modifier(symbol):
+ """Decorator for associating a function with a Mustache tag modifier.
+
+ @modifier('P')
+ def render_tongue(self, tag_name=None, context=None):
+ return ":P %s" % tag_name
+
+ {{P yo }} => :P yo
+ """
+ def set_modifier(func):
+ modifiers[symbol] = func
+ return func
+ return set_modifier
class Template(object):
- tag_types = {
- None: 'tag',
- '!': 'comment'
- }
+ # The regular expression used to find a #section
+ section_re = None
+
+ # The regular expression used to find a tag.
+ tag_re = None
+
+ # Opening tag delimiter
+ otag = '{{'
+
+ # Closing tag delimiter
+ ctag = '}}'
def __init__(self, template, context=None):
self.template = template
self.context = context or {}
+ self.compile_regexps()
def render(self, template=None, context=None):
"""Turns a Mustache template into something wonderful."""
@@ -21,14 +42,25 @@ def render(self, template=None, context=None):
template = self.render_sections(template, context)
return self.render_tags(template, context)
+ def compile_regexps(self):
+ """Compiles our section and tag regular expressions."""
+ 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, context):
"""Expands sections."""
while 1:
- match = SECTION_RE.search(template)
+ 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 = context.get(section_name, None)
replacer = ''
@@ -47,23 +79,47 @@ def render_sections(self, template, context):
def render_tags(self, template, context):
"""Renders all the tags in a template for a context."""
while 1:
- match = TAG_RE.search(template)
+ match = self.tag_re.search(template)
if match is None:
break
tag, tag_type, tag_name = match.group(0, 1, 2)
- func = 'render_' + self.tag_types[tag_type]
-
- if hasattr(self, func):
- replacement = getattr(self, func)(tag_name, context)
- template = template.replace(tag, replacement)
+ tag_name = tag_name.strip()
+ func = modifiers[tag_type]
+ replacement = func(self, tag_name, context)
+ template = template.replace(tag, replacement)
return template
+ @modifier(None)
def render_tag(self, tag_name, context):
- """Given a tag name and context, finds and renders the tag."""
- return context.get(tag_name, '')
+ """Given a tag name and context, finds, escapes, and renders the tag."""
+ return cgi.escape(str(context.get(tag_name, '')))
+ @modifier('!')
def render_comment(self, tag_name=None, context=None):
"""Rendering a comment always returns nothing."""
return ''
+
+ @modifier('{')
+ def render_unescaped(self, tag_name=None, context=None):
+ """Render a tag without escaping it."""
+ return context.get(tag_name, '')
+
+ @modifier('>')
+ def render_partial(self, tag_name=None, context=None):
+ """Renders a partial within the current context."""
+ # Import view here to avoid import loop
+ from pystache.view import View
+
+ view = View(context=context)
+ view.template_name = tag_name
+
+ return view.render()
+
+ @modifier('=')
+ def render_delimiter(self, tag_name=None, context=None):
+ """Changes the Mustache delimiter."""
+ self.otag, self.ctag = tag_name.split(' ')
+ self.compile_regexps()
+ return ''
View
36 pystache/view.py
@@ -9,6 +9,10 @@ class View(object):
# Extension for templates
template_extension = 'mustache'
+ # The name of this template. If none is given the View will try
+ # to infer it based on the class name.
+ template_name = None
+
# Absolute path to the template itself. Pystache will try to guess
# if it's not provided.
template_file = None
@@ -19,25 +23,49 @@ class View(object):
def __init__(self, template=None, context=None, **kwargs):
self.template = template
self.context = context or {}
- self.context.update(kwargs)
+
+ # If the context we're handed is a View, we want to inherit
+ # its settings.
+ if isinstance(context, View):
+ self.inherit_settings(context)
+
+ if kwargs:
+ self.context.update(kwargs)
+
+ def inherit_settings(self, view):
+ """Given another View, copies its settings."""
+ if view.template_path:
+ self.template_path = view.template_path
+
+ if view.template_name:
+ self.template_name = view.template_name
+
+ def __contains__(self, needle):
+ return hasattr(self, needle)
+
+ def __getitem__(self, attr):
+ return getattr(self, attr)()
def load_template(self):
if self.template:
return self.template
if not self.template_file:
- name = self.template_name() + '.' + self.template_extension
+ name = self.get_template_name() + '.' + self.template_extension
self.template_file = os.path.join(self.template_path, name)
f = open(self.template_file, 'r')
template = f.read()
f.close()
return template
- def template_name(self, name=None):
+ def get_template_name(self, name=None):
"""TemplatePartial => template_partial
- Takes a string but defaults to using the current class' name.
+ Takes a string but defaults to using the current class' name or
+ the `template_name` attribute
"""
+ if self.template_name:
+ return self.template_name
if not name:
name = self.__class__.__name__
View
13 setup.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+
+setup(name='pystache',
+ version='0.1.1',
+ description='Mustache for Python',
+ author='Chris Wanstrath',
+ author_email='chris@ozmm.org',
+ url='http://github.com/defunkt/pystache',
+ packages=['pystache'],
+ license='MIT'
+ )
View
51 tests/test_examples.py
@@ -0,0 +1,51 @@
+import unittest
+import pystache
+
+from examples.comments import Comments
+from examples.double_section import DoubleSection
+from examples.escaped import Escaped
+from examples.unescaped import Unescaped
+from examples.template_partial import TemplatePartial
+from examples.delimiters import Delimiters
+
+class TestView(unittest.TestCase):
+ def test_comments(self):
+ self.assertEquals(Comments().render(), """<h1>A Comedy of Errors</h1>
+""")
+
+ def test_double_section(self):
+ self.assertEquals(DoubleSection().render(), """* first
+* second
+* third""")
+
+ def test_escaped(self):
+ self.assertEquals(Escaped().render(), "<h1>Bear &gt; Shark</h1>")
+
+ def test_unescaped(self):
+ self.assertEquals(Unescaped().render(), "<h1>Bear > Shark</h1>")
+
+ def test_template_partial(self):
+ self.assertEquals(TemplatePartial().render(), """<h1>Welcome</h1>
+Again, Welcome!""")
+
+ def test_template_partial_extension(self):
+ view = TemplatePartial()
+ view.template_extension = 'txt'
+ self.assertEquals(view.render(), """Welcome
+-------
+
+Again, Welcome!
+""")
+
+
+ def test_delimiters(self):
+ self.assertEquals(Delimiters().render(), """
+* It worked the first time.
+
+* And it worked the second time.
+
+* Then, surprisingly, it worked the third time.
+""")
+
+if __name__ == '__main__':
+ unittest.main()
View
12 tests/test_pystache.py
@@ -35,6 +35,15 @@ def test_true_sections_are_shown(self):
ret = pystache.render(template, { 'set': True })
self.assertEquals(ret, "Ready set go!")
+ 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)""")
+
def test_sections(self):
template = """
<ul>
@@ -51,3 +60,6 @@ def test_sections(self):
<li>Chris</li><li>Tom</li><li>PJ</li>
</ul>
""")
+
+if __name__ == '__main__':
+ unittest.main()
View
10 tests/test_view.py
@@ -1,5 +1,6 @@
import unittest
import pystache
+
from examples.simple import Simple
from examples.complex_view import ComplexView
@@ -23,8 +24,11 @@ def test_basic_method_calls(self):
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><strong>red</strong></li>\n \n <li><a href="#Green">green</a></li>
<li><a href="#Blue">blue</a></li>
- </ul>
+ </ul>
""")
+
+
+if __name__ == '__main__':
+ unittest.main()

0 comments on commit 1f8eacb

Please sign in to comment.
Something went wrong with that request. Please try again.