From 8fb76bb28f3e6d1fb201d995d7fefade6e366482 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Tue, 11 Jan 2011 15:47:07 +0000 Subject: [PATCH] First test passing with {{ }} --- pystache/template.py | 151 +++++++++++-------------------------------- pystache/view.py | 109 +------------------------------ tests/test_simple.py | 9 +++ 3 files changed, 49 insertions(+), 220 deletions(-) create mode 100644 tests/test_simple.py diff --git a/pystache/template.py b/pystache/template.py index b2abdaa..43f648d 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -17,138 +17,61 @@ def modifier(symbol): 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): + + def __init__(self, template=None, context=None, **kwargs): + from view import View + 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) + + if kwargs: + context.update(kwargs) + + self.view = context if isinstance(context, View) else View(context=context) + + self._compile_regexps() + + def _compile_regexps(self): + tags = { + 'otag': re.escape(self.otag), + 'ctag': re.escape(self.ctag) + } 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) - + + def _render_sections(self, template, view): return template - - def render_tags(self, template, context): - """Renders all the tags in a template for a context.""" - while 1: + + def _render_tags(self, template, view): + while True: match = self.tag_re.search(template) if match is None: break - + tag, tag_type, tag_name = match.group(0, 1, 2) tag_name = tag_name.strip() func = modifiers[tag_type] - replacement = func(self, tag_name, context) + replacement = func(self, tag_name, view) 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 '' + def _render_tag(self, tag_name, view): + raw = view.get(tag_name, '') + 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 '' + def render(self): + template = self._render_sections(self.template, self.view) + result = self._render_tags(template, self.view) + + return result \ No newline at end of file diff --git a/pystache/view.py b/pystache/view.py index b6293ec..c336eeb 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -4,114 +4,11 @@ import re from types import * 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 + self.context.update(**kwargs) - 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__') and type(attr) is UnboundMethodType: - 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() + def get(self, attr, default=None): + return self.context.get(attr, getattr(self, attr, default)) \ No newline at end of file diff --git a/tests/test_simple.py b/tests/test_simple.py new file mode 100644 index 0000000..d60a0ed --- /dev/null +++ b/tests/test_simple.py @@ -0,0 +1,9 @@ +import unittest +import pystache + +class TestSimple(unittest.TestCase): + + def test_simple_render(self): + tmpl = '{{derp}}' + + self.assertEqual('herp', pystache.Template(tmpl, {'derp': 'herp'}).render()) \ No newline at end of file