add pystache

This commit is contained in:
Damien Elmes 2010-11-27 14:16:29 +09:00
parent f247133ed8
commit 9e790ce747
5 changed files with 375 additions and 0 deletions

20
pystache/LICENSE Normal file
View file

@ -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.

78
pystache/README.rst Normal file
View file

@ -0,0 +1,78 @@
========
Pystache
========
Inspired by ctemplate_ and et_, Mustache_ is a
framework-agnostic way to render logic-free views.
As ctemplates says, "It emphasizes separating logic from presentation:
it is impossible to embed application logic in this template language."
Pystache is a Python implementation of Mustache. Pystache requires
Python 2.6.
Documentation
=============
The different Mustache tags are documented at `mustache(5)`_.
Install It
==========
::
pip install pystache
Use It
======
::
>>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
'Hi Mom!'
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):
return "pizza"
Then your template, simple.mustache::
Hi {{thing}}!
Pull it together::
>>> Simple().render()
'Hi pizza!'
Test It
=======
nose_ works great! ::
pip install nose
cd pystache
nosetests
Author
======
::
context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
pystache.render("{{author}} :: {{email}}", context)
.. _ctemplate: http://code.google.com/p/google-ctemplate/
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _Mustache: http://defunkt.github.com/mustache/
.. _mustache(5): http://defunkt.github.com/mustache/mustache.5.html
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html

7
pystache/__init__.py Normal file
View file

@ -0,0 +1,7 @@
from pystache.template import Template
from pystache.view import View
def render(template, context=None, **kwargs):
context = context and context.copy() or {}
context.update(kwargs)
return Template(template, context).render()

154
pystache/template.py Normal file
View file

@ -0,0 +1,154 @@
import re
import cgi
import collections
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
def get_or_attr(obj, name, default=None):
try:
return obj[name]
except KeyError:
return default
except:
try:
return getattr(obj, name)
except AttributeError:
return default
class Template(object):
# 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, encoding=None):
"""Turns a Mustache template into something wonderful."""
template = template or self.template
context = context or self.context
template = self.render_sections(template, context)
result = self.render_tags(template, context)
if encoding is not None:
result = result.encode(encoding)
return result
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 = 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 = get_or_attr(context, section_name, None)
replacer = ''
if it and isinstance(it, collections.Callable):
replacer = it(inner)
elif it and not hasattr(it, '__iter__'):
if section[2] != '^':
replacer = inner
elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
if section[2] != '^':
replacer = self.render(inner, it)
elif it:
insides = []
for item in it:
insides.append(self.render(inner, item))
replacer = ''.join(insides)
elif not it and section[2] == '^':
replacer = inner
template = template.replace(section, replacer)
return template
def render_tags(self, template, context):
"""Renders all the tags in a template for a context."""
while 1:
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 = 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, escapes, and renders the tag."""
raw = get_or_attr(context, tag_name, '')
if not raw and raw is not 0:
return ''
return cgi.escape(unicode(raw))
@modifier('!')
def render_comment(self, tag_name=None, context=None):
"""Rendering a comment always returns nothing."""
return ''
@modifier('{')
@modifier('&')
def render_unescaped(self, tag_name=None, context=None):
"""Render a tag without escaping it."""
return unicode(get_or_attr(context, 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 ''

116
pystache/view.py Normal file
View file

@ -0,0 +1,116 @@
from pystache import Template
import os.path
import re
class View(object):
# Path where this view's template(s) live
template_path = '.'
# 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
# Contents of the template.
template = None
# Character encoding of the template file. If None, Pystache will not
# do any decoding of the template.
template_encoding = None
def __init__(self, template=None, context=None, **kwargs):
self.template = template
self.context = context or {}
# 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 load_template(self):
if self.template:
return self.template
if self.template_file:
return self._load_template()
name = self.get_template_name() + '.' + self.template_extension
if isinstance(self.template_path, basestring):
self.template_file = os.path.join(self.template_path, name)
return self._load_template()
for path in self.template_path:
self.template_file = os.path.join(path, name)
if os.path.exists(self.template_file):
return self._load_template()
raise IOError('"%s" not found in "%s"' % (name, ':'.join(self.template_path),))
def _load_template(self):
f = open(self.template_file, 'r')
try:
template = f.read()
if self.template_encoding:
template = unicode(template, self.template_encoding)
finally:
f.close()
return template
def get_template_name(self, name=None):
"""TemplatePartial => template_partial
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__
def repl(match):
return '_' + match.group(0).lower()
return re.sub('[A-Z]', repl, name)[1:]
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:
raise KeyError("No such key.")
return val
def get(self, attr, default):
attr = self.context.get(attr, getattr(self, attr, default))
if hasattr(attr, '__call__'):
return attr()
else:
return attr
def render(self, encoding=None):
template = self.load_template()
return Template(template, self).render(encoding=encoding)
def __str__(self):
return self.render()