Commit c8a1f2e1 by Chris Jerdonek

Merge branch 'decouple-parser' into 'development':

Parsing a template no longer requires a RenderEngine instance.
parents 7eef0a68 5dd7fa7c
...@@ -38,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding() ...@@ -38,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding()
# strings that arise from files. # strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding() FILE_ENCODING = sys.getdefaultencoding()
# The delimiters to start with when parsing.
DELIMITERS = (u'{{', u'}}')
# How to handle missing tags when rendering a template. # How to handle missing tags when rendering a template.
MISSING_TAGS = MissingTags.ignore MISSING_TAGS = MissingTags.ignore
......
...@@ -38,18 +38,17 @@ class ParsedTemplate(object): ...@@ -38,18 +38,17 @@ class ParsedTemplate(object):
def add(self, node): def add(self, node):
self._parse_tree.append(node) self._parse_tree.append(node)
def render(self, context): def render(self, engine, context):
""" """
Returns: a string of type unicode. Returns: a string of type unicode.
""" """
# We avoid use of the ternary operator for Python 2.4 support. # We avoid use of the ternary operator for Python 2.4 support.
def get_unicode(val): def get_unicode(val):
if callable(val): if type(val) is unicode:
return val(context) return val
return val return val.render(engine, context)
parts = map(get_unicode, self._parse_tree) parts = map(get_unicode, self._parse_tree)
s = ''.join(parts) s = ''.join(parts)
return unicode(s) return unicode(s)
# coding: utf-8 # coding: utf-8
""" """
Provides a class for parsing template strings. Exposes a parse() function to parse template strings.
This module is only meant for internal use by the renderengine module.
""" """
import re import re
from pystache.defaults import DELIMITERS
from pystache.parsed import ParsedTemplate from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = (u'{{', u'}}')
END_OF_LINE_CHARACTERS = [u'\r', u'\n'] END_OF_LINE_CHARACTERS = [u'\r', u'\n']
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
def _compile_template_re(delimiters=None): # TODO: add some unit tests for this.
def parse(template, delimiters=None):
""" """
Return a regular expresssion object (re.RegexObject) instance. Parse a unicode template string and return a ParsedTemplate instance.
Arguments:
template: a unicode template string.
delimiters: a 2-tuple of delimiters. Defaults to the package default.
"""
parser = _Parser(delimiters)
return parser.parse(template)
def _compile_template_re(delimiters):
""" """
if delimiters is None: Return a regular expresssion object (re.RegexObject) instance.
delimiters = DEFAULT_DELIMITERS
"""
# The possible tag type characters following the opening tag, # The possible tag type characters following the opening tag,
# excluding "=" and "{". # excluding "=" and "{".
tag_types = "!>&/#^" tag_types = "!>&/#^"
...@@ -52,25 +64,131 @@ class ParsingError(Exception): ...@@ -52,25 +64,131 @@ class ParsingError(Exception):
pass pass
class Parser(object): ## Node types
_delimiters = None
_template_re = None
def __init__(self, engine, delimiters=None): class _CommentNode(object):
"""
Construct an instance.
Arguments: def render(self, engine, context):
return u''
engine: a RenderEngine instance.
""" class _ChangeNode(object):
def __init__(self, delimiters):
self.delimiters = delimiters
def render(self, engine, context):
return u''
class _TagNode(object):
def __init__(self, key):
self.key = key
def render(self, engine, context):
s = engine.fetch_string(context, self.key)
return engine.escape(s)
class _LiteralNode(object):
def __init__(self, key):
self.key = key
def render(self, engine, context):
s = engine.fetch_string(context, self.key)
return engine.literal(s)
class _PartialNode(object):
def __init__(self, key, indent):
self.key = key
self.indent = indent
def render(self, engine, context):
template = engine.resolve_partial(self.key)
# Indent before rendering.
template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template)
return engine.render(template, context)
class _InvertedNode(object):
def __init__(self, key, parsed_section):
self.key = key
self.parsed_section = parsed_section
def render(self, engine, context):
# TODO: is there a bug because we are not using the same
# logic as in fetch_string()?
data = engine.resolve_context(context, self.key)
# Note that lambdas are considered truthy for inverted sections
# per the spec.
if data:
return u''
return engine.render_parsed(self.parsed_section, context)
class _SectionNode(object):
# TODO: the template_ and parsed_template_ arguments don't both seem
# to be necessary. Can we remove one of them? For example, if
# callable(data) is True, then the initial parsed_template isn't used.
def __init__(self, key, parsed_contents, delimiters, template, section_begin_index, section_end_index):
self.delimiters = delimiters
self.key = key
self.parsed_contents = parsed_contents
self.template = template
self.section_begin_index = section_begin_index
self.section_end_index = section_end_index
def render(self, engine, context):
data = engine.fetch_section_data(context, self.key)
parts = []
for val in data:
if callable(val):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
val = val(self.template[self.section_begin_index:self.section_end_index])
val = engine._render_value(val, context, delimiters=self.delimiters)
parts.append(val)
continue
context.push(val)
parts.append(engine.render_parsed(self.parsed_contents, context))
context.pop()
return unicode(''.join(parts))
class _Parser(object):
_delimiters = None
_template_re = None
def __init__(self, delimiters=None):
if delimiters is None: if delimiters is None:
delimiters = DEFAULT_DELIMITERS delimiters = DELIMITERS
self._delimiters = delimiters self._delimiters = delimiters
self.engine = engine
def _compile_delimiters(self): def _compile_delimiters(self):
self._template_re = _compile_template_re(self._delimiters) self._template_re = _compile_template_re(self._delimiters)
...@@ -172,8 +290,9 @@ class Parser(object): ...@@ -172,8 +290,9 @@ class Parser(object):
parsed_template.add(node) parsed_template.add(node)
# Add the remainder of the template. # Avoid adding spurious empty strings to the parse tree.
parsed_template.add(template[start_index:]) if start_index != len(template):
parsed_template.add(template[start_index:])
return parsed_template return parsed_template
...@@ -184,21 +303,21 @@ class Parser(object): ...@@ -184,21 +303,21 @@ class Parser(object):
""" """
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs. # TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!': if tag_type == '!':
return u'' return _CommentNode()
if tag_type == '=': if tag_type == '=':
delimiters = tag_key.split() delimiters = tag_key.split()
self._change_delimiters(delimiters) self._change_delimiters(delimiters)
return u'' return _ChangeNode(delimiters)
if tag_type == '': if tag_type == '':
return self.engine._make_get_escaped(tag_key) return _TagNode(tag_key)
if tag_type == '&': if tag_type == '&':
return self.engine._make_get_literal(tag_key) return _LiteralNode(tag_key)
if tag_type == '>': if tag_type == '>':
return self.engine._make_get_partial(tag_key, leading_whitespace) return _PartialNode(tag_key, leading_whitespace)
raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type)) raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))
...@@ -209,10 +328,10 @@ class Parser(object): ...@@ -209,10 +328,10 @@ class Parser(object):
""" """
if tag_type == '#': if tag_type == '#':
return self.engine._make_get_section(tag_key, parsed_section, self._delimiters, return _SectionNode(tag_key, parsed_section, self._delimiters,
template, section_start_index, section_end_index) template, section_start_index, section_end_index)
if tag_type == '^': if tag_type == '^':
return self.engine._make_get_inverse(tag_key, parsed_section) return _InvertedNode(tag_key, parsed_section)
raise Exception("Invalid symbol for section tag: %s" % repr(tag_type)) raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
...@@ -8,10 +8,7 @@ Defines a class responsible for rendering logic. ...@@ -8,10 +8,7 @@ Defines a class responsible for rendering logic.
import re import re
from pystache.common import is_string from pystache.common import is_string
from pystache.parser import Parser from pystache.parser import parse
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
def context_get(stack, name): def context_get(stack, name):
...@@ -90,12 +87,12 @@ class RenderEngine(object): ...@@ -90,12 +87,12 @@ class RenderEngine(object):
# The returned value MUST be rendered against the default delimiters, # The returned value MUST be rendered against the default delimiters,
# then interpolated in place of the lambda. # then interpolated in place of the lambda.
# #
def _get_string_value(self, context, tag_name): def fetch_string(self, context, name):
""" """
Get a value from the given context as a basestring instance. Get a value from the given context as a basestring instance.
""" """
val = self.resolve_context(context, tag_name) val = self.resolve_context(context, name)
if callable(val): if callable(val):
# Return because _render_value() is already a string. # Return because _render_value() is already a string.
...@@ -106,134 +103,40 @@ class RenderEngine(object): ...@@ -106,134 +103,40 @@ class RenderEngine(object):
return val return val
def _make_get_literal(self, name): def fetch_section_data(self, context, name):
def get_literal(context): data = self.resolve_context(context, name)
"""
Returns: a string of type unicode. # From the spec:
#
""" # If the data is not of a list type, it is coerced into a list
s = self._get_string_value(context, name) # as follows: if the data is truthy (e.g. `!!data == true`),
return self.literal(s) # use a single-element list containing the data, otherwise use
# an empty list.
return get_literal #
if not data:
def _make_get_escaped(self, name): data = []
get_literal = self._make_get_literal(name) else:
# The least brittle way to determine whether something
def get_escaped(context): # supports iteration is by trying to call iter() on it:
"""
Returns: a string of type unicode.
"""
s = self._get_string_value(context, name)
return self.escape(s)
return get_escaped
def _make_get_partial(self, tag_key, leading_whitespace):
template = self.resolve_partial(tag_key)
# Indent before rendering.
template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
def get_partial(context):
"""
Returns: a string of type unicode.
"""
# TODO: can we do the parsing before calling this function?
return self.render(template, context)
return get_partial
def _make_get_inverse(self, name, parsed_template):
def get_inverse(context):
"""
Returns a string with type unicode.
"""
# TODO: is there a bug because we are not using the same
# logic as in _get_string_value()?
data = self.resolve_context(context, name)
# Per the spec, lambdas in inverted sections are considered truthy.
if data:
return u''
return parsed_template.render(context)
return get_inverse
# TODO: the template_ and parsed_template_ arguments don't both seem
# to be necessary. Can we remove one of them? For example, if
# callable(data) is True, then the initial parsed_template isn't used.
def _make_get_section(self, name, parsed_template, delims,
template, section_start_index, section_end_index):
def get_section_value(context):
"""
Returns: a string of type unicode.
"""
data = self.resolve_context(context, name)
# From the spec:
# #
# If the data is not of a list type, it is coerced into a list # http://docs.python.org/library/functions.html#iter
# as follows: if the data is truthy (e.g. `!!data == true`),
# use a single-element list containing the data, otherwise use
# an empty list.
# #
if not data: # It is not sufficient, for example, to check whether the item
data = [] # implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try:
iter(data)
except TypeError:
# Then the value does not support iteration.
data = [data]
else: else:
# The least brittle way to determine whether something if is_string(data) or isinstance(data, dict):
# supports iteration is by trying to call iter() on it: # Do not treat strings and dicts (which are iterable) as lists.
#
# http://docs.python.org/library/functions.html#iter
#
# It is not sufficient, for example, to check whether the item
# implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try:
iter(data)
except TypeError:
# Then the value does not support iteration.
data = [data] data = [data]
else: # Otherwise, treat the value as a list.
if is_string(data) or isinstance(data, dict):
# Do not treat strings and dicts (which are iterable) as lists. return data
data = [data]
# Otherwise, treat the value as a list.
parts = []
for val in data:
if callable(val):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
val = val(template[section_start_index:section_end_index])
val = self._render_value(val, context, delimiters=delims)
parts.append(val)
continue
context.push(val)
parts.append(parsed_template.render(context))
context.pop()
return unicode(''.join(parts))
return get_section_value
def _render_value(self, val, context, delimiters=None): def _render_value(self, val, context, delimiters=None):
""" """
...@@ -247,6 +150,9 @@ class RenderEngine(object): ...@@ -247,6 +150,9 @@ class RenderEngine(object):
val = self.literal(val) val = self.literal(val)
return self.render(val, context, delimiters) return self.render(val, context, delimiters)
def render_parsed(self, parsed_template, context_stack):
return parsed_template.render(self, context_stack)
def render(self, template, context_stack, delimiters=None): def render(self, template, context_stack, delimiters=None):
""" """
Render a unicode template string, and return as unicode. Render a unicode template string, and return as unicode.
...@@ -259,7 +165,6 @@ class RenderEngine(object): ...@@ -259,7 +165,6 @@ class RenderEngine(object):
context_stack: a ContextStack instance. context_stack: a ContextStack instance.
""" """
parser = Parser(self, delimiters=delimiters) parsed_template = parse(template, delimiters)
parsed_template = parser.parse(template)
return parsed_template.render(context_stack) return self.render_parsed(parsed_template, context_stack)
...@@ -7,6 +7,7 @@ Unit tests of parser.py. ...@@ -7,6 +7,7 @@ Unit tests of parser.py.
import unittest import unittest
from pystache.defaults import DELIMITERS
from pystache.parser import _compile_template_re as make_re from pystache.parser import _compile_template_re as make_re
...@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase): ...@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
Test getting a key from a dictionary. Test getting a key from a dictionary.
""" """
re = make_re() re = make_re(DELIMITERS)
match = re.search("b {{test}}") match = re.search("b {{test}}")
self.assertEqual(match.start(), 1) self.assertEqual(match.start(), 1)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment