diff options
author | Chris Jerdonek <chris.jerdonek@gmail.com> | 2012-05-05 17:21:21 -0700 |
---|---|---|
committer | Chris Jerdonek <chris.jerdonek@gmail.com> | 2012-05-05 17:21:21 -0700 |
commit | c8a1f2e13c6e483e992cdf5c78e6c9940331e791 (patch) | |
tree | 1ef5eb072129f7c2637f3bc78584bcec995e5c1f /pystache | |
parent | 7eef0a68507d1f01464d9ee980483ba2fca10c28 (diff) | |
parent | 5dd7fa7c8df7f186abda7ac463c44fc70953cc27 (diff) | |
download | pystache-c8a1f2e13c6e483e992cdf5c78e6c9940331e791.tar.gz |
Merge branch 'decouple-parser' into 'development':
Parsing a template no longer requires a RenderEngine instance.
Diffstat (limited to 'pystache')
-rw-r--r-- | pystache/defaults.py | 3 | ||||
-rw-r--r-- | pystache/parsed.py | 9 | ||||
-rw-r--r-- | pystache/parser.py | 177 | ||||
-rw-r--r-- | pystache/renderengine.py | 171 | ||||
-rw-r--r-- | pystache/tests/test_parser.py | 3 |
5 files changed, 195 insertions, 168 deletions
diff --git a/pystache/defaults.py b/pystache/defaults.py index a510cfe..0a20328 100644 --- a/pystache/defaults.py +++ b/pystache/defaults.py @@ -38,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding() # strings that arise from files. FILE_ENCODING = sys.getdefaultencoding() +# The delimiters to start with when parsing. +DELIMITERS = (u'{{', u'}}') + # How to handle missing tags when rendering a template. MISSING_TAGS = MissingTags.ignore diff --git a/pystache/parsed.py b/pystache/parsed.py index d791be4..eb138cd 100644 --- a/pystache/parsed.py +++ b/pystache/parsed.py @@ -38,18 +38,17 @@ class ParsedTemplate(object): def add(self, node): self._parse_tree.append(node) - def render(self, context): + def render(self, engine, context): """ Returns: a string of type unicode. """ # We avoid use of the ternary operator for Python 2.4 support. def get_unicode(val): - if callable(val): - return val(context) - return val + if type(val) is unicode: + return val + return val.render(engine, context) parts = map(get_unicode, self._parse_tree) s = ''.join(parts) return unicode(s) - diff --git a/pystache/parser.py b/pystache/parser.py index 99f8d13..9bfc30c 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -1,29 +1,41 @@ # coding: utf-8 """ -Provides a class for parsing template strings. - -This module is only meant for internal use by the renderengine module. +Exposes a parse() function to parse template strings. """ import re +from pystache.defaults import DELIMITERS from pystache.parsed import ParsedTemplate -DEFAULT_DELIMITERS = (u'{{', u'}}') 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: - delimiters = DEFAULT_DELIMITERS + Return a regular expresssion object (re.RegexObject) instance. + """ # The possible tag type characters following the opening tag, # excluding "=" and "{". tag_types = "!>&/#^" @@ -52,25 +64,131 @@ class ParsingError(Exception): pass -class Parser(object): +## Node types - _delimiters = None - _template_re = None - def __init__(self, engine, delimiters=None): - """ - Construct an instance. +class _CommentNode(object): - 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: - delimiters = DEFAULT_DELIMITERS + delimiters = DELIMITERS self._delimiters = delimiters - self.engine = engine def _compile_delimiters(self): self._template_re = _compile_template_re(self._delimiters) @@ -172,8 +290,9 @@ class Parser(object): parsed_template.add(node) - # Add the remainder of the template. - parsed_template.add(template[start_index:]) + # Avoid adding spurious empty strings to the parse tree. + if start_index != len(template): + parsed_template.add(template[start_index:]) return parsed_template @@ -184,21 +303,21 @@ class Parser(object): """ # TODO: switch to using a dictionary instead of a bunch of ifs and elifs. if tag_type == '!': - return u'' + return _CommentNode() if tag_type == '=': delimiters = tag_key.split() self._change_delimiters(delimiters) - return u'' + return _ChangeNode(delimiters) if tag_type == '': - return self.engine._make_get_escaped(tag_key) + return _TagNode(tag_key) if tag_type == '&': - return self.engine._make_get_literal(tag_key) + return _LiteralNode(tag_key) 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)) @@ -209,10 +328,10 @@ class Parser(object): """ if tag_type == '#': - return self.engine._make_get_section(tag_key, parsed_section, self._delimiters, - template, section_start_index, section_end_index) + return _SectionNode(tag_key, parsed_section, self._delimiters, + template, section_start_index, section_end_index) 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)) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 16cd940..9259544 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -8,10 +8,7 @@ Defines a class responsible for rendering logic. import re from pystache.common import is_string -from pystache.parser import Parser - - -NON_BLANK_RE = re.compile(ur'^(.)', re.M) +from pystache.parser import parse def context_get(stack, name): @@ -90,12 +87,12 @@ class RenderEngine(object): # The returned value MUST be rendered against the default delimiters, # 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. """ - val = self.resolve_context(context, tag_name) + val = self.resolve_context(context, name) if callable(val): # Return because _render_value() is already a string. @@ -106,134 +103,40 @@ class RenderEngine(object): return val - def _make_get_literal(self, name): - def get_literal(context): - """ - Returns: a string of type unicode. - - """ - s = self._get_string_value(context, name) - return self.literal(s) - - return get_literal - - def _make_get_escaped(self, name): - get_literal = self._make_get_literal(name) - - def get_escaped(context): - """ - 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: + def fetch_section_data(self, context, name): + data = self.resolve_context(context, name) + + # From the spec: + # + # If the data is not of a list type, it is coerced into a list + # 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: + data = [] + else: + # The least brittle way to determine whether something + # supports iteration is by trying to call iter() on it: # - # If the data is not of a list type, it is coerced into a list - # as follows: if the data is truthy (e.g. `!!data == true`), - # use a single-element list containing the data, otherwise use - # an empty list. + # http://docs.python.org/library/functions.html#iter # - if not data: - data = [] + # 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] else: - # The least brittle way to determine whether something - # supports iteration is by trying to call iter() on it: - # - # 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. + if is_string(data) or isinstance(data, dict): + # Do not treat strings and dicts (which are iterable) as lists. data = [data] - else: - if is_string(data) or isinstance(data, dict): - # Do not treat strings and dicts (which are iterable) as lists. - 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 + # Otherwise, treat the value as a list. + + return data def _render_value(self, val, context, delimiters=None): """ @@ -247,6 +150,9 @@ class RenderEngine(object): val = self.literal(val) 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): """ Render a unicode template string, and return as unicode. @@ -259,7 +165,6 @@ class RenderEngine(object): context_stack: a ContextStack instance. """ - parser = Parser(self, delimiters=delimiters) - parsed_template = parser.parse(template) + parsed_template = parse(template, delimiters) - return parsed_template.render(context_stack) + return self.render_parsed(parsed_template, context_stack) diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py index 4aa0959..92248ea 100644 --- a/pystache/tests/test_parser.py +++ b/pystache/tests/test_parser.py @@ -7,6 +7,7 @@ Unit tests of parser.py. import unittest +from pystache.defaults import DELIMITERS from pystache.parser import _compile_template_re as make_re @@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase): Test getting a key from a dictionary. """ - re = make_re() + re = make_re(DELIMITERS) match = re.search("b {{test}}") self.assertEqual(match.start(), 1) |